Repository: pushpalroy/jetlime Branch: main Commit: 0fcfa7f366d3 Files: 262 Total size: 4.7 MB Directory structure: gitextract_33wmcawt/ ├── .claude/ │ ├── commands/ │ │ ├── bump-version.md │ │ ├── ci-local.md │ │ ├── dokka.md │ │ └── spotless.md │ └── skills/ │ ├── compose-expert/ │ │ ├── SKILL.md │ │ └── references/ │ │ ├── accessibility.md │ │ ├── animation.md │ │ ├── atomic-design.md │ │ ├── auto-init.md │ │ ├── composition-locals.md │ │ ├── deprecated-patterns.md │ │ ├── design-to-compose.md │ │ ├── lists-scrolling.md │ │ ├── material3-motion.md │ │ ├── modifiers.md │ │ ├── multiplatform.md │ │ ├── navigation.md │ │ ├── performance.md │ │ ├── platform-specifics.md │ │ ├── pr-review.md │ │ ├── production-crash-playbook.md │ │ ├── side-effects.md │ │ ├── source-code/ │ │ │ ├── cmp-source.md │ │ │ ├── foundation-source.md │ │ │ ├── material3-source.md │ │ │ ├── navigation-source.md │ │ │ ├── runtime-source.md │ │ │ └── ui-source.md │ │ ├── state-management.md │ │ ├── styles-experimental.md │ │ ├── theming-material3.md │ │ ├── tv-compose.md │ │ └── view-composition.md │ ├── edge-to-edge/ │ │ └── SKILL.md │ └── r8-analyzer/ │ ├── SKILL.md │ └── references/ │ ├── CONFIGURATION.md │ ├── KEEP-RULES-IMPACT-HIERARCHY.md │ ├── REDUNDANT-RULES.md │ ├── REFLECTION-GUIDE.md │ └── android/ │ └── topic/ │ └── performance/ │ └── app-optimization/ │ └── enable-app-optimization.md ├── .editorconfig ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── publish.yml │ └── setup/ │ ├── ios-setup/ │ │ └── action.yml │ └── java-setup/ │ └── action.yml ├── .gitignore ├── CLAUDE.md ├── CONTRIBUTING.md ├── LICENSE ├── PUBLISHING.md ├── README.md ├── build.gradle.kts ├── docs/ │ ├── index.html │ ├── jetlime/ │ │ ├── com.pushpal.jetlime/ │ │ │ ├── -event-point-animation/ │ │ │ │ ├── animation-spec.html │ │ │ │ ├── equals.html │ │ │ │ ├── hash-code.html │ │ │ │ ├── index.html │ │ │ │ ├── initial-value.html │ │ │ │ └── target-value.html │ │ │ ├── -event-point-type/ │ │ │ │ ├── -companion/ │ │ │ │ │ ├── -default.html │ │ │ │ │ ├── -e-m-p-t-y.html │ │ │ │ │ ├── custom.html │ │ │ │ │ ├── filled.html │ │ │ │ │ └── index.html │ │ │ │ ├── equals.html │ │ │ │ ├── fill-percent.html │ │ │ │ ├── hash-code.html │ │ │ │ ├── icon.html │ │ │ │ ├── index.html │ │ │ │ ├── is-custom.html │ │ │ │ ├── is-empty-or-filled.html │ │ │ │ ├── is-filled.html │ │ │ │ ├── tint.html │ │ │ │ └── type.html │ │ │ ├── -event-position/ │ │ │ │ ├── -companion/ │ │ │ │ │ ├── dynamic.html │ │ │ │ │ └── index.html │ │ │ │ ├── equals.html │ │ │ │ ├── hash-code.html │ │ │ │ ├── index.html │ │ │ │ ├── is-not-end.html │ │ │ │ ├── is-not-start.html │ │ │ │ └── name.html │ │ │ ├── -horizontal-alignment/ │ │ │ │ ├── -b-o-t-t-o-m/ │ │ │ │ │ └── index.html │ │ │ │ ├── -t-o-p/ │ │ │ │ │ └── index.html │ │ │ │ ├── entries.html │ │ │ │ ├── index.html │ │ │ │ ├── value-of.html │ │ │ │ └── values.html │ │ │ ├── -items-list/ │ │ │ │ ├── -items-list.html │ │ │ │ ├── equals.html │ │ │ │ ├── hash-code.html │ │ │ │ ├── index.html │ │ │ │ └── items.html │ │ │ ├── -jet-lime-column.html │ │ │ ├── -jet-lime-defaults/ │ │ │ │ ├── column-style.html │ │ │ │ ├── index.html │ │ │ │ ├── line-gradient-brush.html │ │ │ │ ├── line-solid-brush.html │ │ │ │ └── row-style.html │ │ │ ├── -jet-lime-event-defaults/ │ │ │ │ ├── event-style.html │ │ │ │ ├── index.html │ │ │ │ └── point-animation.html │ │ │ ├── -jet-lime-event-style/ │ │ │ │ ├── equals.html │ │ │ │ ├── hash-code.html │ │ │ │ ├── index.html │ │ │ │ ├── point-animation.html │ │ │ │ ├── point-color.html │ │ │ │ ├── point-fill-color.html │ │ │ │ ├── point-placement.html │ │ │ │ ├── point-radius.html │ │ │ │ ├── point-stroke-color.html │ │ │ │ ├── point-stroke-width.html │ │ │ │ ├── point-type.html │ │ │ │ ├── position.html │ │ │ │ ├── set-point-placement.html │ │ │ │ └── set-position.html │ │ │ ├── -jet-lime-event.html │ │ │ ├── -jet-lime-extended-event.html │ │ │ ├── -jet-lime-row.html │ │ │ ├── -jet-lime-style/ │ │ │ │ ├── content-distance.html │ │ │ │ ├── equals.html │ │ │ │ ├── hash-code.html │ │ │ │ ├── index.html │ │ │ │ ├── item-spacing.html │ │ │ │ ├── line-brush.html │ │ │ │ ├── line-horizontal-alignment.html │ │ │ │ ├── line-thickness.html │ │ │ │ ├── line-vertical-alignment.html │ │ │ │ └── path-effect.html │ │ │ ├── -local-jet-lime-style.html │ │ │ ├── -point-placement/ │ │ │ │ ├── -c-e-n-t-e-r/ │ │ │ │ │ └── index.html │ │ │ │ ├── -e-n-d/ │ │ │ │ │ └── index.html │ │ │ │ ├── -s-t-a-r-t/ │ │ │ │ │ └── index.html │ │ │ │ ├── entries.html │ │ │ │ ├── index.html │ │ │ │ ├── value-of.html │ │ │ │ └── values.html │ │ │ ├── -vertical-alignment/ │ │ │ │ ├── -l-e-f-t/ │ │ │ │ │ └── index.html │ │ │ │ ├── -r-i-g-h-t/ │ │ │ │ │ └── index.html │ │ │ │ ├── entries.html │ │ │ │ ├── index.html │ │ │ │ ├── value-of.html │ │ │ │ └── values.html │ │ │ └── index.html │ │ └── package-list │ ├── navigation.html │ ├── scripts/ │ │ ├── main.js │ │ ├── navigation-loader.js │ │ ├── pages.json │ │ ├── platform-content-handler.js │ │ ├── prism.js │ │ ├── safe-local-storage_blocking.js │ │ └── sourceset_dependencies.js │ └── styles/ │ ├── logo-styles.css │ ├── main.css │ ├── prism.css │ └── style.css ├── dokkaModule.md ├── dokkaPackage.md ├── gradle/ │ ├── libs.versions.toml │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── jetlime/ │ ├── .gitignore │ ├── build.gradle.kts │ ├── consumer-rules.pro │ ├── dokkaModule.md │ ├── dokkaPackage.md │ ├── jetlime.podspec │ ├── proguard-rules.pro │ └── src/ │ ├── androidMain/ │ │ └── AndroidManifest.xml │ ├── androidTest/ │ │ └── java/ │ │ └── com/ │ │ └── pushpal/ │ │ └── jetlime/ │ │ ├── ExampleInstrumentedTest.kt │ │ ├── JetLimeColumnTest.kt │ │ └── JetLimeRowTest.kt │ ├── commonMain/ │ │ └── kotlin/ │ │ └── com/ │ │ └── pushpal/ │ │ └── jetlime/ │ │ ├── EventPointAnimation.kt │ │ ├── EventPointType.kt │ │ ├── EventPosition.kt │ │ ├── ItemsList.kt │ │ ├── JetLimeDefaults.kt │ │ ├── JetLimeEvent.kt │ │ ├── JetLimeEventDefaults.kt │ │ ├── JetLimeEventStyle.kt │ │ ├── JetLimeExtendedEvent.kt │ │ ├── JetLimeList.kt │ │ └── JetLimeStyle.kt │ └── test/ │ └── java/ │ └── com/ │ └── pushpal/ │ └── jetlime/ │ └── ExampleUnitTest.kt ├── public-key.asc ├── sample/ │ ├── composeApp/ │ │ ├── build.gradle.kts │ │ ├── composeApp.podspec │ │ └── src/ │ │ ├── androidMain/ │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin/ │ │ │ │ └── com/ │ │ │ │ └── pushpal/ │ │ │ │ └── jetlime/ │ │ │ │ └── sample/ │ │ │ │ ├── JetLimePreviews.kt │ │ │ │ └── MainActivity.kt │ │ │ └── res/ │ │ │ ├── drawable/ │ │ │ │ └── ic_launcher_background.xml │ │ │ ├── drawable-v24/ │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── mipmap-anydpi-v26/ │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── values/ │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ └── values-night/ │ │ │ └── themes.xml │ │ ├── commonMain/ │ │ │ ├── composeResources/ │ │ │ │ └── drawable/ │ │ │ │ ├── icon_change.xml │ │ │ │ └── icon_check.xml │ │ │ └── kotlin/ │ │ │ ├── App.kt │ │ │ ├── Home.kt │ │ │ ├── data/ │ │ │ │ └── Item.kt │ │ │ ├── theme/ │ │ │ │ ├── Color.kt │ │ │ │ ├── Shape.kt │ │ │ │ └── Theme.kt │ │ │ └── timelines/ │ │ │ ├── BasicDashedTimeLine.kt │ │ │ ├── BasicHorizontalTimeLine.kt │ │ │ ├── BasicVerticalTimeLine.kt │ │ │ ├── CustomizedHorizontalTimeLine.kt │ │ │ ├── CustomizedVerticalTimeLine.kt │ │ │ ├── ExtendedVerticalTimeLine.kt │ │ │ ├── VerticalDynamicTimeLine.kt │ │ │ └── event/ │ │ │ └── EventContent.kt │ │ ├── desktopMain/ │ │ │ └── kotlin/ │ │ │ └── Main.kt │ │ ├── iosMain/ │ │ │ └── kotlin/ │ │ │ └── MainViewController.kt │ │ ├── jsMain/ │ │ │ ├── kotlin/ │ │ │ │ └── Main.kt │ │ │ └── resources/ │ │ │ ├── index.html │ │ │ └── styles.css │ │ └── wasmJsMain/ │ │ ├── kotlin/ │ │ │ └── Main.kt │ │ └── resources/ │ │ ├── index.html │ │ └── styles.css │ └── iosApp/ │ ├── Configuration/ │ │ └── Config.xcconfig │ ├── Podfile │ ├── Pods/ │ │ ├── Pods.xcodeproj/ │ │ │ ├── project.pbxproj │ │ │ └── xcuserdata/ │ │ │ └── pushpalroy.xcuserdatad/ │ │ │ └── xcschemes/ │ │ │ ├── Pods-iosApp.xcscheme │ │ │ └── xcschememanagement.plist │ │ └── Target Support Files/ │ │ └── Pods-iosApp/ │ │ ├── Pods-iosApp-Info.plist │ │ ├── Pods-iosApp-acknowledgements.markdown │ │ ├── Pods-iosApp-acknowledgements.plist │ │ ├── Pods-iosApp-dummy.m │ │ ├── Pods-iosApp-umbrella.h │ │ ├── Pods-iosApp.debug.xcconfig │ │ ├── Pods-iosApp.modulemap │ │ └── Pods-iosApp.release.xcconfig │ ├── iosApp/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ ├── Preview Content/ │ │ │ └── Preview Assets.xcassets/ │ │ │ └── Contents.json │ │ └── iOSApp.swift │ ├── iosApp.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcuserdata/ │ │ └── pushpalroy.xcuserdatad/ │ │ └── xcschemes/ │ │ ├── iosApp.xcscheme │ │ └── xcschememanagement.plist │ └── iosApp.xcworkspace/ │ ├── contents.xcworkspacedata │ ├── xcshareddata/ │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata/ │ └── pushpalroy.xcuserdatad/ │ ├── UserInterfaceState.xcuserstate │ └── xcschemes/ │ └── xcschememanagement.plist ├── scripts/ │ ├── add_git_tag.sh │ ├── build_android.sh │ ├── build_ios.sh │ ├── build_macos.sh │ ├── build_web_js.sh │ ├── build_web_wasm.sh │ ├── run_dokka.sh │ └── run_spotless.sh ├── settings.gradle.kts └── spotless/ └── copyright.kt ================================================ FILE CONTENTS ================================================ ================================================ FILE: .claude/commands/bump-version.md ================================================ --- description: Upgrade the library version across all files that contain it argument-hint: "" --- Bump the JetLime library version from its current value to `$ARGUMENTS` (e.g. `4.2.0`) across every file that embeds it. ## Files to update Update **all** occurrences of the old version string with the new version `$ARGUMENTS` in: 1. `jetlime/build.gradle.kts` - `mavenPublishing.coordinates(...)` third argument - `cocoapods { version = "..." }` 2. `jetlime/jetlime.podspec` - `spec.version = '...'` 3. `scripts/add_git_tag.sh` - `TAG="..."` 4. `README.md` - The `implementation("io.github.pushpalroy:jetlime:...")` snippet ## Steps 1. Read each file listed above and locate the current version string. 2. Replace every occurrence of the current version with `$ARGUMENTS` using the Edit tool. 3. After all edits, grep the repo for the old version to confirm no stray occurrences remain: ``` grep -r "" --include="*.kts" --include="*.kt" --include="*.sh" --include="*.md" --include="*.podspec" . ``` If any hits remain outside `build/`, `docs/`, or `.git/`, report them and ask whether to update them too. 4. Print a summary table — one row per file — showing the old value replaced and the new value written. ## Constraints - Do NOT commit or tag. The user will review the diff and commit manually (or run `/dokka` and publish first). - Do NOT modify `CHANGELOG`, `docs/`, or any generated output — those are updated separately. - If `$ARGUMENTS` is missing or doesn't look like a semver string (`X.Y.Z`), stop and ask the user to provide it. ================================================ FILE: .claude/commands/ci-local.md ================================================ --- description: Run the full CI pipeline locally (spotless + all platform builds) to catch failures before pushing argument-hint: "[fast|full]" --- Mirror the GitHub Actions `build.yml` pipeline locally so the user can verify everything CI runs before pushing. CI matrix: `spotlessCheck`, `build_android.sh`, `build_web_js.sh`, `build_web_wasm.sh`, `build_macos.sh`, `build_ios.sh`. ## Behavior Run these in sequence with fail-fast semantics. As soon as one step fails, stop and show the failing step's last ~30 lines of output — do NOT continue to later steps, because cascaded failures hide the real error. ### `$1 == "fast"` (or when user wants a quick pre-push check) Run only the cheap/fast steps: 1. `./gradlew spotlessCheck` 2. `./scripts/build_android.sh` 3. `./scripts/build_web_js.sh` Skip wasm, desktop, and iOS. ### `$1 == "full"` or no argument (default — matches CI exactly) Run every CI step, in order: 1. `./gradlew spotlessCheck` 2. `./scripts/build_android.sh` 3. `./scripts/build_web_js.sh` 4. `./scripts/build_web_wasm.sh` 5. `./scripts/build_macos.sh` 6. `./scripts/build_ios.sh` — only on macOS; skip with a note on other OSes. ## Handling the common failures - **`spotlessCheck` fails**: tell the user to run `/spotless apply` (or `./gradlew spotlessApply`), review the diff, and re-run. Do NOT auto-apply — they should see what changed. - **`kotlinStoreYarnLock` fails during a web build** ("Lock file was changed..."): run `./gradlew kotlinUpgradeYarnLock`, then tell the user to commit `kotlin-js-store/yarn.lock` (and `kotlin-js-store/wasm/yarn.lock` if it changed). Do NOT commit automatically. - **Gradle daemon / class-cast weirdness after a Kotlin or AGP bump**: suggest `./gradlew --stop` and a re-run. - **iOS build on non-macOS host**: skip with a clear message — CI runs this only on `macos-latest`. ## Final report After all steps succeed, print a single-line summary naming each step that ran and passed. No celebratory wall of text. If a step failed, the summary is: which step, which file/task, and the recommended next command — nothing more. ================================================ FILE: .claude/commands/dokka.md ================================================ --- description: Generate API documentation using Dokka V2 and sync to docs folder --- Generate HTML API documentation for the JetLime library. This command runs the Dokka V2 generation task and synchronizes the output to the root `docs/` directory, which is served via GitHub Pages. Behavior: 1. Run `./scripts/run_dokka.sh`. This script executes the `:jetlime:syncDokkaToDocs` Gradle task. 2. The task generates HTML documentation using Dokka V2 from the source files. 3. It incorporates additional documentation from `dokkaModule.md` and `dokkaPackage.md` if present. 4. It clears the existing `docs/` directory and copies the new documentation there. Notes: - The output in `docs/` should be reviewed if significant API changes were made. - Dokka V2 is used, as configured in `jetlime/build.gradle.kts`. - The synchronization task ensures that the latest documentation is ready for deployment to GitHub Pages. - If the command fails, check the Gradle output for configuration or source errors. ================================================ FILE: .claude/commands/spotless.md ================================================ --- description: Run Spotless formatting check and auto-fix any violations argument-hint: "[check|apply]" --- Run Spotless on this project. CI runs `spotlessCheck`, so formatting violations block merges — the goal of this command is to keep the branch clean. Behavior based on `$1`: - **`check`** (or no argument): run `./gradlew spotlessCheck`. If it passes, report "Spotless passes" and stop. If it fails, show the violating files from the output — do NOT auto-apply. - **`apply`**: run `./gradlew spotlessApply`, then `./gradlew spotlessCheck` to confirm. If files were modified, list them with `git status --short` so the user can review and commit. Notes: - Spotless uses ktlint + `io.nlopez.compose.rules:ktlint` and a mandatory MIT license header from `spotless/copyright.kt`. Do not hand-edit formatting — let `spotlessApply` own it. - After `apply`, do not commit or push automatically. Show the diff summary and let the user decide. - If the Gradle build itself errors (not just formatting), surface the real error rather than retrying. ================================================ FILE: .claude/skills/compose-expert/SKILL.md ================================================ --- name: compose-expert description: > Compose and Compose Multiplatform expert skill for UI development across Android, Desktop, iOS, and Web. Guides state management, view composition, animations, navigation, performance, design-to-code workflows, and production crash patterns. Backed by actual source code analysis from both androidx/androidx and JetBrains/compose-multiplatform-core. Use this skill whenever the user mentions Compose, @Composable, remember, LaunchedEffect, Scaffold, NavHost, MaterialTheme, LazyColumn, Modifier, recomposition, Style, styleable, MutableStyleState, Compose Multiplatform, CMP, KMP, commonMain, expect, actual, ComposeUIViewController, Window composable, UIKitView, ComposeViewport, Res.drawable, Res.string, or any Compose API. Also trigger when the user says "Android UI", "Kotlin UI", "compose layout", "compose navigation", "compose animation", "material3", "compose styles", "compose multiplatform", "desktop compose", "iOS compose", "compose web", "design to compose", "build this UI", "implement this design", "Android TV", "Google TV", "tv-material", "tv-foundation", "Carousel", "NavigationDrawer", "D-pad", "focus indication", "10-foot UI", "living room", "tv compose", "review this PR", "review this code", "check this diff", or any GitHub PR URL (github.com/.*/pull/), "design system", "component library", "atomic", "reusable component", "design tokens", "atoms", "molecules", or asks about modern Kotlin UI development patterns. Even casual mentions like "my compose screen is slow" or "how do I pass data between screens" or "how do I build a TV app" should trigger this skill. Also trigger on session_start to auto-detect Compose projects — see references/auto-init.md. version: 2.1.2 --- > **Installation notice:** This skill is now distributed as a plugin. > If you copied files into `~/.claude/skills/` manually, you are on an > unmaintained install path and will not receive updates. Migrate via: > > /plugin marketplace add aldefy/compose-skill > /plugin install compose-expert > > See [MIGRATION.md](../docs/MIGRATION.md) for Codex and Copilot CLI instructions. > This banner will remain through v2.x and escalate in v3.0. # Compose Expert Skill Non-opinionated, practical guidance for writing correct, performant Compose code — across Android, Desktop, iOS, and Web. Covers Jetpack Compose and Compose Multiplatform. Backed by analysis of actual source code from `androidx/androidx` and `JetBrains/compose-multiplatform-core`. ## Review Mode **Activate when** the input contains a GitHub PR URL (`github.com/.+/pull/\d+`) or explicit review phrases: "review this PR", "review this diff", "check this code", "what's wrong with this". When Review Mode activates: 1. Do **not** follow the generation workflow below 2. Read `references/pr-review.md` and follow its workflow exclusively 3. Output a structured local review report — do not post to GitHub ## Workflow When helping with Compose code, follow this checklist: ### 1. Understand the request - What Compose layer is involved? (Runtime, UI, Foundation, Material3, Navigation) - Is this a state problem, layout problem, performance problem, or architecture question? - Is this Android-only or Compose Multiplatform (CMP)? ### 2. Analyze the design (if visual reference provided) - If the user shares a Figma frame, screenshot, or design spec, consult `references/design-to-compose.md` - Decompose the design into a composable tree using the 5-step methodology - Map design tokens to MaterialTheme, spacing to CompositionLocals - Identify animation needs and consult `references/animation.md` for recipes ### 3. Consult the right reference Read the relevant reference file(s) from `references/` before answering: | Topic | Reference File | |-------|---------------| | `@State`, `remember`, `mutableStateOf`, state hoisting, `derivedStateOf`, `snapshotFlow` | `references/state-management.md` | | Structuring composables, slots, extraction, preview | `references/view-composition.md` — for design system structure, also see `references/atomic-design.md` | | Modifier ordering, custom modifiers, `Modifier.Node` | `references/modifiers.md` | | `LaunchedEffect`, `DisposableEffect`, `SideEffect`, `rememberCoroutineScope` | `references/side-effects.md` | | `CompositionLocal`, `LocalContext`, `LocalDensity`, custom locals | `references/composition-locals.md` | | `LazyColumn`, `LazyRow`, `LazyGrid`, `Pager`, keys, content types | `references/lists-scrolling.md` | | `NavHost`, type-safe routes, deep links, shared element transitions | `references/navigation.md` | | `animate*AsState`, `AnimatedVisibility`, `Crossfade`, transitions | `references/animation.md` — for M3 token selection, also see `references/material3-motion.md` | | `MaterialTheme`, `ColorScheme`, dynamic color, `Typography`, shapes | `references/theming-material3.md` — for motion, see `references/material3-motion.md`; for design tokens, see `references/atomic-design.md` | | Recomposition skipping, stability, baseline profiles, benchmarking | `references/performance.md` | | Semantics, content descriptions, traversal order, testing | `references/accessibility.md` | | Removed/replaced APIs, migration paths from older Compose versions | `references/deprecated-patterns.md` | | **Styles API** (experimental): `Style {}`, `MutableStyleState`, `Modifier.styleable()` | `references/styles-experimental.md` | | Figma/screenshot decomposition, design tokens, spacing, modifier ordering | `references/design-to-compose.md` | | Production crash patterns, defensive coding, state/performance rules | `references/production-crash-playbook.md` | | Compose Multiplatform, `expect`/`actual`, resources (`Res.*`), migration | `references/multiplatform.md` | | Desktop (Window, Tray, MenuBar), iOS (UIKitView), Web (ComposeViewport) | `references/platform-specifics.md` | | TV Compose: Surface, Carousel, NavigationDrawer, Cards, focus, D-pad | `references/tv-compose.md` | | M3 motion tokens, `MotionTokens`, `MotionScheme`, animation duration, easing | `references/material3-motion.md` | | PR URL, code review, "review this PR", "what's wrong with this" | `references/pr-review.md` | | Session start, project detection | `references/auto-init.md` | | Atomic design, design system, reusable component, component library, design tokens | `references/atomic-design.md` | ### 4. Apply and verify - Write code that follows the patterns in the reference - Flag any anti-patterns you see in the user's existing code - Suggest the minimal correct solution — don't over-engineer ### 4a. Component building mode When the request involves building a component (composable that renders UI): - Consult `references/atomic-design.md` - Classify the component level (atom, molecule, organism, template) - Apply the "Ask" prompt from Section 5 before scaffolding code - Ensure the component satisfies the atom contract (modifier, slots, tokens, defaults) ### 5. Cite the source When referencing Compose internals, point to the exact source file: ``` // See: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt ``` ## Key Principles 1. **Compose thinks in three phases**: Composition → Layout → Drawing. State reads in each phase only trigger work for that phase and later ones. 2. **Recomposition is frequent and cheap** — but only if you help the compiler skip unchanged scopes. Use stable types, avoid allocations in composable bodies. 3. **Modifier order matters**. `Modifier.padding(16.dp).background(Color.Red)` is visually different from `Modifier.background(Color.Red).padding(16.dp)`. 4. **State should live as low as possible** and be hoisted only as high as needed. Don't put everything in a ViewModel just because you can. 5. **Side effects exist to bridge Compose's declarative world with imperative APIs**. Use the right one for the job — misusing them causes bugs that are hard to trace. 6. **Compose Multiplatform shares the runtime but not the platform**. UI code in `commonMain` is portable. Platform-specific APIs (`LocalContext`, `BackHandler`, `Window`) require `expect`/`actual` or conditional source sets. ## Source Code Receipts Beyond the guidance docs, this skill bundles the **actual source code** from `androidx/androidx` (branch: `androidx-main`) and `JetBrains/compose-multiplatform-core` (branch: `jb-main`). When you need to verify how something works internally, or the user asks "show me the actual implementation", read the raw source from `references/source-code/`: | Module | Source Reference | Key Files Inside | |--------|-----------------|------------------| | Runtime | `references/source-code/runtime-source.md` | Composer.kt, Recomposer.kt, State.kt, Effects.kt, CompositionLocal.kt, Remember.kt, SlotTable.kt, Snapshot.kt | | UI | `references/source-code/ui-source.md` | AndroidCompositionLocals.android.kt, Modifier.kt, Layout.kt, LayoutNode.kt, ModifierNodeElement.kt, DrawModifier.kt | | Foundation | `references/source-code/foundation-source.md` | LazyList.kt, LazyGrid.kt, BasicTextField.kt, Clickable.kt, Scrollable.kt, Pager.kt | | Material3 | `references/source-code/material3-source.md` | MaterialTheme.kt, ColorScheme.kt, Button.kt, Scaffold.kt, TextField.kt, NavigationBar.kt | | Navigation | `references/source-code/navigation-source.md` | NavHost.kt, ComposeNavigator.kt, NavGraphBuilder.kt, DialogNavigator.kt | | CMP | `references/source-code/cmp-source.md` | Window.kt, ComposeUIViewController.kt, UIKitView.kt, ComposeViewport.kt, ResourceReader.kt | ### Two-layer approach 1. **Start with guidance** — read the topic-specific reference (e.g., `references/state-management.md`) 2. **Go deeper with source** — if the user wants receipts or you need to verify, read from `references/source-code/` ### Source tree map ``` androidx/androidx (branch: androidx-main) ├── compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ ├── compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/ ├── compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ ├── compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/ ├── compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ ├── compose/navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/ ├── tv/tv-material/src/main/java/androidx/tv/material3/ └── tv/tv-foundation/src/main/java/androidx/tv/foundation/ compose-multiplatform-core (branch: jb-main) ├── compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/ ├── compose/ui/ui/src/iosMain/kotlin/androidx/compose/ui/window/ ├── compose/ui/ui/src/webMain/kotlin/androidx/compose/ui/window/ ├── compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/ └── compose/foundation/foundation/src/skikoMain/kotlin/androidx/compose/foundation/ compose-multiplatform (resources library) └── components/resources/library/src/commonMain/ ``` ================================================ FILE: .claude/skills/compose-expert/references/accessibility.md ================================================ # Accessibility Reference ## Semantics — Exposing UI to Accessibility Services Semantics describe UI elements to screen readers, voice commands, and testing tools. Compose generates semantics automatically for built-in components, but custom views require manual annotation. ```kotlin // Built-in components have semantics Button(onClick = { }) { Text("Click me") } // Auto-announces as button // Custom composables need explicit semantics Box( modifier = Modifier.semantics { role = Role.Button onClick(label = "Activate") { true } } ) { Text("Custom button") } ``` **Source**: `androidx/compose/ui/semantics/` --- ## contentDescription — Labels for Accessibility Every meaningful visual element needs a label for screen readers. ### When to Set contentDescription **DO**: Images, icons, buttons without text, decorative with meaningful purpose ```kotlin // Icon in a button: set contentDescription Button(onClick = { }) { Icon(Icons.Default.Settings, contentDescription = "App settings") } // Text-only button: contentDescription auto-generated Button(onClick = { }) { Text("Save") } // Standalone image: always set description Image( painter = painterResource(R.drawable.product), contentDescription = "Product photo: wireless headphones" ) ``` ### When to Set contentDescription = null **Purely decorative images** that convey no information: ```kotlin // Decorative divider line Divider(modifier = Modifier.padding(vertical = 8.dp)) // Purely decorative background Box( modifier = Modifier .background(Color.Gray) .size(100.dp) ) { Image( painter = painterResource(R.drawable.background_pattern), contentDescription = null // Purely decorative ) } // Icon next to label: skip icon description Row { Icon( painter = painterResource(R.drawable.verified), contentDescription = null, // Label below describes it tint = Color.Green ) Text("Verified") } ``` Omitting `contentDescription` or using `null` tells the screen reader to skip the element. --- ## Modifier.semantics — Merging and Overriding ### Default Merging By default, child semantics merge with parents: ```kotlin // Child text is included in parent semantics Column(modifier = Modifier.semantics { heading() }) { Text("Section Title") // Screen reader: "Section Title, heading" } ``` ### Clearing and Setting Semantics Use `clearAndSetSemantics` to override all child semantics: ```kotlin // Screen reader ignores children, announces custom label Box( modifier = Modifier.clearAndSetSemantics { contentDescription = "Custom audio player with play/pause" } ) { Icon(Icons.Default.PlayArrow, contentDescription = null) Text("00:30") // Not read Icon(Icons.Default.VolumeUp, contentDescription = null) // Not read } ``` ### Merging Disabled Prevent children from merging: ```kotlin Box( modifier = Modifier.semantics(mergeDescendants = false) { heading() } ) { Text("Heading") // Announced separately, not merged } ``` --- ## Touch Target Sizing Minimum touch target is 48dp × 48dp per Material Design and WCAG guidelines. ```kotlin // Small button without sufficient touch target Button(modifier = Modifier.size(32.dp)) { } // TOO SMALL // Proper touch target Button( modifier = Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp) ) { } // Alternative: add padding Box( modifier = Modifier .size(32.dp) .sizeIn(minWidth = 48.dp, minHeight = 48.dp) ) { Icon(Icons.Default.Edit, contentDescription = "Edit") } // For clickable elements without Button Box( modifier = Modifier .clip(RoundedCornerShape(8.dp)) .clickable { /* ... */ } .sizeIn(minWidth = 48.dp, minHeight = 48.dp), contentAlignment = Alignment.Center ) { Icon(Icons.Default.Close, contentDescription = "Close") } ``` --- ## Headings — Screen Reader Navigation Headings allow users to navigate by section. Set heading semantic: ```kotlin // Level 1 heading Text( text = "Products", modifier = Modifier.semantics { heading() }, style = MaterialTheme.typography.headlineLarge ) // Subheading (no formal levels in Compose; use structure) Text( text = "New Arrivals", modifier = Modifier.semantics { heading() }, style = MaterialTheme.typography.headlineMedium ) ``` Screen readers announce "heading" and allow jumping between sections. Use headings to structure content logically, not for styling. --- ## Custom Actions Allow screen readers to trigger complex interactions: ```kotlin @Composable fun SlideToUnlock() { Box( modifier = Modifier .fillMaxWidth() .height(64.dp) .semantics { customActions = listOf( CustomAccessibilityAction(label = "Unlock") { unlock() true }, CustomAccessibilityAction(label = "Emergency call") { emergencyCall() true } ) } .background(MaterialTheme.colorScheme.primary) ) { Text("Slide to Unlock", color = Color.White) } } ``` Custom actions appear in the accessibility menu. Avoid for standard interactions (Button, Checkbox). --- ## Traversal Order Control screen reader navigation order explicitly when needed: ```kotlin // Default: top-to-bottom, left-to-right Row { Button(onClick = { }) { Text("First") } Button(onClick = { }) { Text("Second") } } // Custom order (right-to-left) Row { Button( onClick = { }, modifier = Modifier.semantics { traversalIndex = 1f } ) { Text("Read Second") } Button( onClick = { }, modifier = Modifier.semantics { traversalIndex = 0f } ) { Text("Read First") } } // Group items as single traversal unit Column( modifier = Modifier.semantics(mergeDescendants = false) { isTraversalGroup = true } ) { Text("Label") Text("Value") } ``` Use `traversalIndex` sparingly. Good structure is usually sufficient. --- ## State Descriptions Inform users of component state (enabled/disabled, checked/unchecked): ```kotlin @Composable fun AccessibleCheckbox( checked: Boolean, onCheckedChange: (Boolean) -> Unit, label: String ) { Row( modifier = Modifier .clip(RoundedCornerShape(8.dp)) .clickable { onCheckedChange(!checked) } .semantics { this.contentDescription = label this.stateDescription = if (checked) "Checked" else "Unchecked" role = Role.Checkbox } .sizeIn(minWidth = 48.dp, minHeight = 48.dp) ) { Icon( if (checked) Icons.Default.CheckBox else Icons.Default.CheckBoxOutlineBlank, contentDescription = null, tint = MaterialTheme.colorScheme.primary ) Text(label, modifier = Modifier.padding(start = 12.dp)) } } ``` Screen reader announces: "Label, Checkbox, Checked" or "Label, Checkbox, Unchecked". --- ## Live Regions Announce dynamic content changes without requiring interaction: ```kotlin @Composable fun LiveMessage(message: String, modifier: Modifier = Modifier) { Text( text = message, modifier = modifier.semantics { liveRegion = LiveRegionMode.Assertive // Polite or Assertive } ) } // Usage var status by remember { mutableStateOf("Loading...") } LaunchedEffect(Unit) { delay(2000) status = "Done loading" // Screen reader announces immediately } LiveMessage(message = status) ``` **Assertive**: Interrupts current speech. Use for critical updates (error, alert). **Polite**: Queues after current speech. Use for status updates. --- ## Testing Semantics Use Compose Test APIs to verify accessibility: ```kotlin @get:Rule val composeTestRule = createComposeRule() @Test fun testContentDescription() { composeTestRule.setContent { Icon( Icons.Default.Settings, contentDescription = "App settings" ) } composeTestRule .onNodeWithContentDescription("App settings") .assertIsDisplayed() } @Test fun testHeading() { composeTestRule.setContent { Text("Main Title", modifier = Modifier.semantics { heading() }) } composeTestRule .onNode(isHeading()) .assertIsDisplayed() .assertTextEquals("Main Title") } @Test fun testTouchTarget() { composeTestRule.setContent { Button(modifier = Modifier.size(32.dp)) { Text("Too small") } } composeTestRule .onNodeWithText("Too small") .assertHeightIsAtLeast(48.dp) // Fails: 32.dp < 48.dp } @Test fun testCustomAction() { composeTestRule.setContent { Box( modifier = Modifier.semantics { customActions = listOf( CustomAccessibilityAction("Unlock") { true } ) } ) } composeTestRule .onNode(hasCustomAccessibilityAction("Unlock")) .performCustomAccessibilityAction("Unlock") } ``` --- ## Anti-Patterns ### Decorative Images Without null contentDescription ```kotlin // DON'T: Screen reader reads useless description Image( painter = painterResource(R.drawable.separator_line), contentDescription = "Line" // No value ) // DO Image( painter = painterResource(R.drawable.separator_line), contentDescription = null ) ``` ### Clickable Without Semantics ```kotlin // DON'T: No semantic role or description Box( modifier = Modifier .clickable { deleteItem() } .size(32.dp) ) { Icon(Icons.Default.Delete, contentDescription = null) } // DO Box( modifier = Modifier .clickable { deleteItem() } .sizeIn(minWidth = 48.dp, minHeight = 48.dp) .semantics { role = Role.Button contentDescription = "Delete item" }, contentAlignment = Alignment.Center ) { Icon(Icons.Default.Delete, contentDescription = null) } ``` ### Hardcoded Content Descriptions Without Context ```kotlin // DON'T: Generic, doesn't describe purpose Icon(Icons.Default.Star, contentDescription = "Icon") // DO: Specific Icon(Icons.Default.Star, contentDescription = "Add to favorites") ``` ### Missing Heading Structure ```kotlin // DON'T: No navigation structure Column { Text("Section 1") Text("Section 2") Text("Section 3") } // DO Column { Text("Section 1", modifier = Modifier.semantics { heading() }) Text("Section 2", modifier = Modifier.semantics { heading() }) Text("Section 3", modifier = Modifier.semantics { heading() }) } ``` --- ## Resources - **Compose Accessibility**: https://developer.android.com/develop/ui/compose/accessibility - **Material Design Accessibility**: https://m3.material.io/foundations/accessible-design - **WCAG 2.1**: https://www.w3.org/WAI/WCAG21/quickref/ - **Testing A11y in Compose**: https://developer.android.com/develop/ui/compose/testing#a11y ================================================ FILE: .claude/skills/compose-expert/references/animation.md ================================================ # Animation in Jetpack Compose Reference: `androidx/compose/animation/animation/src/commonMain/kotlin/androidx/compose/animation/` ## State-Based Animations ### animate*AsState Animate individual properties by targeting a value. The animation starts when the value changes. ```kotlin val size by animateDpAsState( targetValue = if (isExpanded) 200.dp else 100.dp, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), label = "size" ) Box(modifier = Modifier.size(size)) ``` Common variants: ```kotlin animateColorAsState(targetValue = Color.Blue) animateFloatAsState(targetValue = 1f) animateIntAsState(targetValue = 100) animateOffsetAsState(targetValue = Offset(10f, 20f)) ``` Each automatically handles coroutines and recomposition. Use the `label` parameter for debugging. ## AnimatedVisibility Controls appear/disappear animations with enter and exit transitions. ```kotlin var visible by remember { mutableStateOf(true) } AnimatedVisibility(visible = visible) { Text("Hello!") } // Trigger Button(onClick = { visible = !visible }) { Text("Toggle") } ``` ### Enter/Exit Transitions ```kotlin AnimatedVisibility( visible = visible, enter = slideInHorizontally(initialOffsetX = { -it }) + fadeIn(), exit = slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() ) { Text("Animated!") } ``` Built-in transitions: - `slideInVertically`, `slideOutVertically` - `slideInHorizontally`, `slideOutHorizontally` - `expandVertically`, `shrinkVertically` - `expandHorizontally`, `shrinkHorizontally` - `fadeIn`, `fadeOut` - `scaleIn`, `scaleOut` - Combine with `+`: `slideInVertically() + fadeIn()` ### Advanced: Custom animation specs ```kotlin AnimatedVisibility( visible = visible, enter = slideInVertically( initialOffsetY = { fullHeight -> fullHeight }, animationSpec = spring() ), exit = slideOutVertically( targetOffsetY = { fullHeight -> fullHeight }, animationSpec = tween(durationMillis = 300) ) ) { Box(Modifier.fillMaxWidth().height(100.dp).background(Color.Blue)) } ``` ## AnimatedContent Replace content with smooth transitions. ```kotlin var count by remember { mutableStateOf(0) } AnimatedContent(targetState = count) { target -> Text(text = "Count: $target") } Button(onClick = { count++ }) { Text("Increment") } ``` ### Custom transitionSpec ```kotlin AnimatedContent( targetState = count, transitionSpec = { slideInVertically(initialOffsetY = { it }) with slideOutVertically(targetOffsetY = { -it }) } ) { target -> Text("$target") } ``` Use `with` to specify exit and enter together. This runs exits and entries simultaneously. ### Sequencing transitions ```kotlin AnimatedContent( targetState = count, transitionSpec = { slideInVertically(initialOffsetY = { it }) with slideOutVertically(targetOffsetY = { -it }) using SizeTransform(clip = false) } ) { target -> Text( "Count: $target", modifier = Modifier.fillMaxWidth() ) } ``` `SizeTransform` animates container size smoothly during content changes. ## Crossfade Simple content swap with fade effect. ```kotlin var showFirst by remember { mutableStateOf(true) } Crossfade(targetState = showFirst) { state -> if (state) { Text("First") } else { Text("Second") } } ``` Lightweight alternative to `AnimatedContent` for simple visibility toggles. ## updateTransition Coordinate multiple animated values with a single state. ```kotlin var expanded by remember { mutableStateOf(false) } val transition = updateTransition(targetState = expanded) val size by transition.animateDp { if (it) 200.dp else 100.dp } val color by transition.animateColor { if (it) Color.Blue else Color.Red } Box( modifier = Modifier .size(size) .background(color) .clickable { expanded = !expanded } ) ``` All animations run in sync, controlled by a single state change. Useful for complex components with multiple animated properties. ## rememberInfiniteTransition Create looping animations. ```kotlin val infiniteTransition = rememberInfiniteTransition(label = "infinite") val alpha by infiniteTransition.animateFloat( initialValue = 0f, targetValue = 1f, animationSpec = infiniteRepeatable( animation = tween(1000), repeatMode = RepeatMode.Reverse ), label = "alpha" ) Text("Pulsing", modifier = Modifier.alpha(alpha)) ``` Runs continuously until the composable is removed. Perfect for loading states, pulsing indicators. ## Animatable Imperative animation control in coroutines. Use for fine-grained control. ```kotlin val animatable = remember { Animatable(0f) } LaunchedEffect(trigger) { animatable.animateTo( targetValue = 100f, animationSpec = spring() ) } Box(Modifier.graphicsLayer(translationX = animatable.value)) ``` Useful for responding to gestures or complex conditions: ```kotlin val animatable = remember { Animatable(0f) } LaunchedEffect(Unit) { animatable.animateTo(targetValue = 360f, animationSpec = tween(2000)) } Box( Modifier .size(100.dp) .background(Color.Blue) .graphicsLayer(rotationZ = animatable.value) ) ``` ## Animation Specifications ### spring — Realistic, physics-based ```kotlin val size by animateDpAsState( targetValue = 200.dp, animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy, stiffness = Spring.StiffnessLow) ) ``` - `dampingRatio`: `NoBouncy` (1f), `LowBouncy` (0.75f), `MediumBouncy` (0.5f), `HighBouncy` (0.2f) - `stiffness`: `Low`, `Medium`, `High` Use for interactive feedback, familiar to users. ### tween — Time-based ```kotlin val color by animateColorAsState( targetValue = Color.Blue, animationSpec = tween(durationMillis = 500, easing = EaseInOutCubic) ) ``` Easing functions: `EaseInQuad`, `EaseOutQuad`, `EaseInOutQuad`, `LinearEasing`, `FastOutSlowInEasing`. Predictable timing, good for sequential animations. ### keyframes — Frame-by-frame control ```kotlin val position by animateFloatAsState( targetValue = 100f, animationSpec = keyframes { 0f at 0 using EaseInQuad 50f at 150 using EaseOutQuad 100f at 300 } ) ``` Define exact values at specific timestamps. Use for complex choreography. ## Automatic Size Animation ### animateContentSize Smoothly animate Box size when content changes. ```kotlin var expanded by remember { mutableStateOf(false) } Box( modifier = Modifier .animateContentSize() .background(Color.Blue) .clickable { expanded = !expanded } ) { Column { Text("Header") if (expanded) { Text("Expanded content...") } } } ``` No need for explicit `AnimatedVisibility` or layout transitions. Handles the container automatically. ## Layout Animation in LazyLists ### animateItem — Replaces animateItemPlacement Animate item appearance, removal, and reordering. ```kotlin LazyColumn { items(items, key = { it.id }) { item -> Box( modifier = Modifier .fillMaxWidth() .animateItem() .padding(8.dp) .background(Color.Gray) ) { Text(item.name) } } } ``` Automatically animates: - New items sliding in - Removed items sliding out - Reordered items moving to new positions Called on items in Lazy layouts (LazyColumn, LazyRow, LazyVerticalGrid). ## Shared Element Transitions Animate elements seamlessly across screen boundaries using `SharedTransitionLayout` and Navigation Compose. ### sharedElement() vs sharedBounds() | Aspect | `sharedElement()` | `sharedBounds()` | |---|---|---| | **Content** | Identical on both screens (same image, same icon) | Different content in source and target (e.g., card expands to detail) | | **Use case** | Hero image, avatar, thumbnail | Container transform, card-to-page | | **During transition** | Only the target composable is rendered | Both source and target are visible and crossfade | ### Complete Working Example ```kotlin @Composable fun App() { SharedTransitionLayout { NavHost(navController = navController, startDestination = "list") { composable("list") { ListScreen( onItemClick = { id -> navController.navigate("detail/$id") }, sharedTransitionScope = this@SharedTransitionLayout, animatedVisibilityScope = this@composable ) } composable("detail/{id}") { backStackEntry -> val id = backStackEntry.arguments?.getString("id") ?: return@composable DetailScreen( itemId = id, sharedTransitionScope = this@SharedTransitionLayout, animatedVisibilityScope = this@composable ) } } } } @Composable fun ListScreen( onItemClick: (String) -> Unit, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { with(sharedTransitionScope) { Row( modifier = Modifier .clickable { onItemClick(item.id) } // sharedBounds wraps the entire card container (different content at source/target) .sharedBounds( sharedContentState = rememberSharedContentState(key = "card-${item.id}"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = BoundsTransform { initialBounds, targetBounds -> keyframes { durationMillis = 500 initialBounds at 0 using ArcMode.ArcBelow targetBounds at 500 } } ) ) { Image( painter = painterResource(item.imageRes), contentDescription = null, modifier = Modifier .size(80.dp) // sharedElement for the identical image across screens .sharedElement( state = rememberSharedContentState(key = "image-${item.id}"), animatedVisibilityScope = animatedVisibilityScope ) ) Text( text = item.title, modifier = Modifier .sharedElement( state = rememberSharedContentState(key = "title-${item.id}"), animatedVisibilityScope = animatedVisibilityScope ) // Prevent text reflow during transition by snapping to final size .skipToLookaheadSize() ) } } } @Composable fun DetailScreen( itemId: String, sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { with(sharedTransitionScope) { Column( modifier = Modifier .sharedBounds( sharedContentState = rememberSharedContentState(key = "card-$itemId"), animatedVisibilityScope = animatedVisibilityScope ) ) { Image( painter = painterResource(item.imageRes), contentDescription = null, modifier = Modifier .fillMaxWidth() .height(300.dp) .sharedElement( state = rememberSharedContentState(key = "image-$itemId"), animatedVisibilityScope = animatedVisibilityScope ) ) Text( text = item.title, style = MaterialTheme.typography.headlineMedium, modifier = Modifier .sharedElement( state = rememberSharedContentState(key = "title-$itemId"), animatedVisibilityScope = animatedVisibilityScope ) .skipToLookaheadSize() ) // Non-shared content fades in Text( text = item.description, modifier = Modifier.animateEnterExit( enter = fadeIn() + slideInVertically { it / 3 }, exit = fadeOut() ) ) } } } ``` ### BoundsTransform for Arc Motion Control the animation path between source and target bounds: ```kotlin val arcBoundsTransform = BoundsTransform { initialBounds, targetBounds -> keyframes { durationMillis = 500 initialBounds at 0 using ArcMode.ArcBelow targetBounds at 500 } } // Apply to sharedElement or sharedBounds Modifier.sharedElement( state = rememberSharedContentState(key = "hero"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = arcBoundsTransform ) ``` ### Overlay Rendering Keep shared elements above all other content during the transition: ```kotlin Modifier.sharedElement( state = rememberSharedContentState(key = "fab"), animatedVisibilityScope = animatedVisibilityScope, renderInSharedTransitionScopeOverlay = true // Renders above navigation transitions ) ``` ### Preventing Text Reflow Use `skipToLookaheadSize()` so text composables snap to their final size immediately, avoiding awkward line-break changes mid-transition: ```kotlin Text( text = item.title, modifier = Modifier .sharedElement( state = rememberSharedContentState(key = "title-${item.id}"), animatedVisibilityScope = animatedVisibilityScope ) .skipToLookaheadSize() // Text uses target size immediately, no reflow ) ``` ## Performance: graphicsLayer for Transforms Animate transforms using `graphicsLayer` instead of layout changes. ```kotlin // ✅ Correct: Uses GPU-accelerated graphicsLayer val offset by animateFloatAsState(targetValue = 100f) Box(modifier = Modifier.graphicsLayer(translationX = offset)) // ❌ Avoid: Causes recomposition and relayout val offset by animateFloatAsState(targetValue = 100f) Box(modifier = Modifier.offset(x = offset.dp)) ``` Use `graphicsLayer` for: - Translation (`translationX`, `translationY`) - Rotation (`rotationX`, `rotationY`, `rotationZ`) - Scale (`scaleX`, `scaleY`) - Alpha (opacity) ## Anti-Patterns ### Don't: Animate visibility with if ```kotlin // ❌ Anti-pattern @Composable fun MyScreen() { if (visible) { Text("Content") // Jumps in/out without animation } } // ✅ Correct @Composable fun MyScreen() { AnimatedVisibility(visible = visible) { Text("Content") } } ``` ### Don't: Create Animatable in composition ```kotlin // ❌ Anti-pattern @Composable fun MyScreen() { val animatable = Animatable(0f) // Recreated every recomposition! LaunchedEffect(Unit) { animatable.animateTo(100f) } } // ✅ Correct @Composable fun MyScreen() { val animatable = remember { Animatable(0f) } // Preserved across recompositions LaunchedEffect(Unit) { animatable.animateTo(100f) } } ``` ### Don't: Animate in composition phase ```kotlin // ❌ Anti-pattern @Composable fun MyScreen() { var position by remember { mutableStateOf(0f) } position = position + 10f // Infinite recomposition loop! } // ✅ Correct @Composable fun MyScreen() { var position by remember { mutableStateOf(0f) } LaunchedEffect(Unit) { repeat(10) { position += 10f delay(16) } } } ``` ### Don't: Forget label parameter ```kotlin // ❌ Anti-pattern (harder to debug) val size by animateDpAsState(targetValue = 100.dp) // ✅ Correct val size by animateDpAsState( targetValue = 100.dp, label = "box_size" ) ``` Labels help with debugging layout inspector and animation inspection tools. --- ## Animation Decision Tree ### When to Use Which API | API | Use When | |---|---| | `animate*AsState` | Animating a single property (size, color, alpha) driven by state | | `AnimatedVisibility` | Showing or hiding a composable with enter/exit transitions | | `AnimatedContent` / `Crossfade` | Switching between different composables (content swap) | | `updateTransition` | Multiple properties that must animate in sync from the same state | | `Animatable` | Gesture-driven or imperative control (coroutine-based, supports `snapTo`, `animateDecay`) | | `rememberInfiniteTransition` | Infinite looping animations (pulsing, rotating, shimmer) | | `animateContentSize` | Smoothly animating a container's size when its content changes | | `animateItem` | List item appearance, disappearance, and reordering in Lazy layouts | ### Which Phase Each Animation Affects Compose rendering has three phases: **Composition** (what to show), **Layout** (where to place), **Draw** (how to render). Animations should read state in the latest possible phase to minimize work. ```kotlin // BEST: Draw phase only — no relayout, no recomposition val alpha by animateFloatAsState(targetValue = if (visible) 1f else 0f, label = "alpha") Box( modifier = Modifier.graphicsLayer { this.alpha = alpha } ) // GOOD: Layout phase only — relayout but no recomposition val offsetPx by animateIntAsState(targetValue = if (moved) 300 else 0, label = "offset") Box( modifier = Modifier.offset { IntOffset(offsetPx, 0) } ) // MODERATE: Composition + Layout — triggers recomposition on every frame val offsetDp by animateDpAsState(targetValue = if (moved) 100.dp else 0.dp, label = "offset") Box( modifier = Modifier.offset(x = offsetDp) ) ``` **Rule:** Defer state reads to the latest possible phase. Use lambda-based modifiers (`graphicsLayer { }`, `offset { }`) instead of parameter-based modifiers (`graphicsLayer(alpha = ...)`, `offset(x = ...)`). --- ## Design-to-Animation Translation ### Figma Easing Curves to Compose | Figma Easing | Compose Equivalent | |---|---| | Linear | `LinearEasing` | | Ease In | `FastOutLinearInEasing` | | Ease Out | `LinearOutSlowInEasing` | | Ease In and Out | `FastOutSlowInEasing` | | Custom Bezier (x1, y1, x2, y2) | `CubicBezierEasing(x1, y1, x2, y2)` | ### M3 Motion Duration Tokens | Token | Duration | |---|---| | Short1 | 50ms | | Short2 | 100ms | | Short3 | 150ms | | Short4 | 200ms | | Medium1 | 250ms | | Medium2 | 300ms | | Medium3 | 350ms | | Medium4 | 400ms | | Long1 | 450ms | | Long2 | 500ms | | Long3 | 550ms | | Long4 | 600ms | | ExtraLong1 | 700ms | | ExtraLong2 | 800ms | | ExtraLong3 | 900ms | | ExtraLong4 | 1000ms | ### M3 Easing Tokens | Token | Compose Value | |---|---| | Emphasized | `CubicBezierEasing(0.2f, 0f, 0f, 1f)` | | EmphasizedDecelerate | `CubicBezierEasing(0.05f, 0.7f, 0.1f, 1f)` | | EmphasizedAccelerate | `CubicBezierEasing(0.3f, 0f, 0.8f, 0.15f)` | | Standard | `FastOutSlowInEasing` | | StandardDecelerate | `LinearOutSlowInEasing` | | StandardAccelerate | `FastOutLinearInEasing` | ### Spring Parameter Intuition **Stiffness** (how fast the animation moves toward its target): | Value | Constant | Feel | |---|---|---| | ~26f | — | Slow, heavy, lethargic | | 200f | `Spring.StiffnessLow` | Gentle, relaxed | | 400f | `Spring.StiffnessMediumLow` | Casual, comfortable | | 1500f | `Spring.StiffnessMedium` | Responsive, default | | 10000f | `Spring.StiffnessHigh` | Snappy, immediate | **Damping Ratio** (how much bounce): | Value | Constant | Feel | |---|---|---| | 1.0f | `Spring.DampingRatioNoBouncy` | No overshoot, settles directly | | 0.75f | `Spring.DampingRatioLowBouncy` | Subtle bounce, professional | | 0.5f | `Spring.DampingRatioMediumBouncy` | Playful, noticeable bounce | | 0.2f | `Spring.DampingRatioHighBouncy` | Exaggerated, cartoonish bounce | ### Figma Spring to Compose Conversion ```kotlin fun figmaSpringToCompose(mass: Float, stiffness: Float, damping: Float): SpringSpec { val dampingRatio = damping / (2f * sqrt(stiffness * mass)) return spring(dampingRatio = dampingRatio, stiffness = stiffness) } ``` ### Production-Validated Spring Specs ```kotlin val figmaMatchedSpring = spring(dampingRatio = 0.444f, stiffness = 26.5f) val responsiveSpring = spring(dampingRatio = 0.7f, stiffness = 800f) val snappySpring = spring(dampingRatio = 0.6f, stiffness = 1000f) ``` --- ## Gesture-Driven Animations ### Swipe-to-Dismiss with Animatable ```kotlin fun Modifier.swipeToDismiss(onDismiss: () -> Unit): Modifier = composed { val offsetX = remember { Animatable(0f) } val decay = rememberSplineBasedDecay() pointerInput(Unit) { coroutineScope { while (true) { val velocityTracker = VelocityTracker() // Wait for touch down val pointerId = awaitPointerEventScope { awaitFirstDown().id } // Cancel any ongoing animation offsetX.stop() awaitPointerEventScope { horizontalDrag(pointerId) { change -> val horizontalDragOffset = offsetX.value + change.positionChange().x launch { offsetX.snapTo(horizontalDragOffset) } velocityTracker.addPosition(change.uptimeMillis, change.position) change.consume() } } val velocity = velocityTracker.calculateVelocity().x val targetOffsetX = decay.calculateTargetValue(offsetX.value, velocity) offsetX.updateBounds( lowerBound = -size.width.toFloat(), upperBound = size.width.toFloat() ) launch { if (abs(targetOffsetX) >= size.width * 0.5f) { // Fling far enough — dismiss offsetX.animateDecay(velocity, decay) onDismiss() } else { // Snap back offsetX.animateTo( targetValue = 0f, initialVelocity = velocity ) } } } } }.offset { IntOffset(offsetX.value.roundToInt(), 0) } } ``` ### AnchoredDraggable Snap Points ```kotlin enum class DragValue { Start, Center, End } @Composable fun AnchoredDraggableExample() { val density = LocalDensity.current val anchors = with(density) { DraggableAnchors { DragValue.Start at -200.dp.toPx() DragValue.Center at 0f DragValue.End at 200.dp.toPx() } } val state = remember { AnchoredDraggableState( initialValue = DragValue.Center, anchors = anchors, positionalThreshold = { totalDistance -> totalDistance * 0.5f }, velocityThreshold = { with(density) { 125.dp.toPx() } }, animationSpec = spring() ) } Box( modifier = Modifier .offset { IntOffset(state.requireOffset().roundToInt(), 0) } .anchoredDraggable(state, Orientation.Horizontal) .size(80.dp) .background(Color.Blue, RoundedCornerShape(16.dp)) ) } ``` ### Transformable: Pinch, Zoom, Rotate ```kotlin @Composable fun TransformableExample() { var scale by remember { mutableFloatStateOf(1f) } var rotation by remember { mutableFloatStateOf(0f) } var offset by remember { mutableStateOf(Offset.Zero) } val transformableState = rememberTransformableState { zoomChange, offsetChange, rotationChange -> scale = (scale * zoomChange).coerceIn(0.5f, 5f) rotation += rotationChange offset += offsetChange } Box( modifier = Modifier .graphicsLayer { scaleX = scale scaleY = scale rotationZ = rotation translationX = offset.x translationY = offset.y } .transformable(state = transformableState) .size(200.dp) .background(Color.Blue) ) } ``` --- ## Animation Recipes ### Shimmer / Skeleton Loading ```kotlin fun Modifier.shimmerEffect(): Modifier = composed { val transition = rememberInfiniteTransition(label = "shimmer") val translateAnim by transition.animateFloat( initialValue = -1000f, targetValue = 1000f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 1200, easing = LinearEasing), repeatMode = RepeatMode.Restart ), label = "shimmer_translate" ) val shimmerBrush = Brush.linearGradient( colors = listOf( Color.LightGray.copy(alpha = 0.6f), Color.LightGray.copy(alpha = 0.2f), Color.LightGray.copy(alpha = 0.6f) ), start = Offset(translateAnim, 0f), end = Offset(translateAnim + 500f, 0f) ) background(shimmerBrush) } @Composable fun SkeletonCard() { Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .fillMaxWidth() .height(200.dp) .clip(RoundedCornerShape(12.dp)) .shimmerEffect() ) Spacer(modifier = Modifier.height(8.dp)) Box( modifier = Modifier .fillMaxWidth(0.7f) .height(20.dp) .clip(RoundedCornerShape(4.dp)) .shimmerEffect() ) } } @Composable fun ContentWithLoading(isLoading: Boolean, content: @Composable () -> Unit) { Crossfade(targetState = isLoading, label = "loading_crossfade") { loading -> if (loading) { SkeletonCard() } else { content() } } } ``` ### Staggered List Entrance ```kotlin @Composable fun StaggeredListEntrance(items: List) { Column { items.forEachIndexed { index, item -> val animatable = remember { Animatable(0f) } LaunchedEffect(Unit) { delay(index * 100L) animatable.animateTo( targetValue = 1f, animationSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessMediumLow ) ) } Text( text = item, modifier = Modifier .graphicsLayer { alpha = animatable.value translationX = (1f - animatable.value) * 100f } .padding(8.dp) ) } } } ``` ### Swipe-to-Dismiss (Material 3) ```kotlin @Composable fun SwipeToDismissItem( onDismiss: () -> Unit, content: @Composable () -> Unit ) { val dismissState = rememberSwipeToDismissBoxState( confirmValueChange = { value -> if (value != SwipeToDismissBoxValue.Settled) { onDismiss() true } else false } ) SwipeToDismissBox( state = dismissState, backgroundContent = { val color by animateColorAsState( targetValue = when (dismissState.targetValue) { SwipeToDismissBoxValue.StartToEnd -> Color.Green SwipeToDismissBoxValue.EndToStart -> Color.Red SwipeToDismissBoxValue.Settled -> Color.Transparent }, label = "dismiss_bg" ) Box( modifier = Modifier .fillMaxSize() .background(color) .padding(horizontal = 20.dp), contentAlignment = when (dismissState.targetValue) { SwipeToDismissBoxValue.StartToEnd -> Alignment.CenterStart else -> Alignment.CenterEnd } ) { Icon( imageVector = when (dismissState.targetValue) { SwipeToDismissBoxValue.StartToEnd -> Icons.Default.Done else -> Icons.Default.Delete }, contentDescription = null, tint = Color.White ) } } ) { content() } } ``` ### Expandable Card ```kotlin @Composable fun ExpandableCard(title: String, description: String) { var expanded by remember { mutableStateOf(false) } val arrowRotation by animateFloatAsState( targetValue = if (expanded) 180f else 0f, label = "arrow_rotation" ) Card( modifier = Modifier .fillMaxWidth() .animateContentSize(animationSpec = spring(stiffness = Spring.StiffnessMediumLow)) .clickable { expanded = !expanded } ) { Column(modifier = Modifier.padding(16.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Text(text = title, style = MaterialTheme.typography.titleMedium, modifier = Modifier.weight(1f)) Icon( imageVector = Icons.Default.KeyboardArrowDown, contentDescription = if (expanded) "Collapse" else "Expand", modifier = Modifier.graphicsLayer { rotationZ = arrowRotation } ) } AnimatedVisibility(visible = expanded) { Text( text = description, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(top = 8.dp) ) } } } } ``` ### Pull-to-Refresh Custom ```kotlin @Composable fun CustomPullToRefresh( isRefreshing: Boolean, onRefresh: () -> Unit, content: @Composable () -> Unit ) { PullToRefreshBox( isRefreshing = isRefreshing, onRefresh = onRefresh, indicator = { state -> val distanceFraction = state.distanceFraction.coerceIn(0f, 1f) Box( modifier = Modifier .fillMaxWidth() .padding(top = 16.dp), contentAlignment = Alignment.TopCenter ) { Icon( imageVector = Icons.Default.Refresh, contentDescription = "Refreshing", modifier = Modifier .size(32.dp) .graphicsLayer { scaleX = distanceFraction scaleY = distanceFraction rotationZ = distanceFraction * 360f } ) } } ) { content() } } ``` ### FAB Morph **Pattern 1: ExtendedFloatingActionButton with scroll-driven expand/collapse** ```kotlin @Composable fun CollapsibleFab(listState: LazyListState) { val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } ExtendedFloatingActionButton( onClick = { /* action */ }, expanded = expandedFab, icon = { Icon(Icons.Default.Add, contentDescription = "Add") }, text = { Text("New Item") } ) } ``` **Pattern 2: Exploding FAB with updateTransition** ```kotlin @Composable fun ExplodingFab(isExpanded: Boolean, onClick: () -> Unit) { val transition = updateTransition(targetState = isExpanded, label = "fab_explode") val size by transition.animateDp(label = "size") { if (it) 200.dp else 56.dp } val cornerRadius by transition.animateDp(label = "corner") { if (it) 16.dp else 28.dp } val color by transition.animateColor(label = "color") { if (it) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.primaryContainer } val contentAlpha by transition.animateFloat(label = "alpha") { if (it) 1f else 0f } Surface( modifier = Modifier.size(size).clickable { onClick() }, shape = RoundedCornerShape(cornerRadius), color = color ) { Box(contentAlignment = Alignment.Center) { if (!isExpanded) { Icon(Icons.Default.Add, contentDescription = "Add") } Column( modifier = Modifier.graphicsLayer { alpha = contentAlpha }, horizontalAlignment = Alignment.CenterHorizontally ) { // Expanded content Text("Option 1") Text("Option 2") Text("Option 3") } } } } ``` ### Bottom Sheet Drag ```kotlin enum class SheetValue { Hidden, Collapsed, Expanded } @Composable fun DraggableBottomSheet(content: @Composable () -> Unit) { val density = LocalDensity.current val anchors = with(density) { DraggableAnchors { SheetValue.Hidden at 0f SheetValue.Collapsed at -200.dp.toPx() SheetValue.Expanded at -600.dp.toPx() } } val state = remember { AnchoredDraggableState( initialValue = SheetValue.Hidden, anchors = anchors, positionalThreshold = { totalDistance -> totalDistance * 0.5f }, velocityThreshold = { with(density) { 125.dp.toPx() } }, animationSpec = spring(stiffness = Spring.StiffnessMediumLow) ) } Box(modifier = Modifier.fillMaxSize()) { content() Surface( modifier = Modifier .fillMaxWidth() .align(Alignment.BottomCenter) .offset { IntOffset(0, (state.requireOffset()).roundToInt()) } .anchoredDraggable(state, Orientation.Vertical), shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), shadowElevation = 8.dp ) { Column(modifier = Modifier.fillMaxWidth().height(600.dp).padding(16.dp)) { // Drag handle Box( modifier = Modifier .align(Alignment.CenterHorizontally) .width(40.dp) .height(4.dp) .background(Color.Gray, RoundedCornerShape(2.dp)) ) Spacer(modifier = Modifier.height(16.dp)) Text("Sheet Content") } } } } ``` ### Parallax Scroll Header ```kotlin @Composable fun ParallaxHeader(scrollState: ScrollState) { val scrollOffset = scrollState.value.toFloat() Box( modifier = Modifier .fillMaxWidth() .height(300.dp) .graphicsLayer { translationY = scrollOffset * 0.6f // Parallax factor scaleX = 1f + (scrollOffset * 0.001f).coerceAtLeast(0f) scaleY = 1f + (scrollOffset * 0.001f).coerceAtLeast(0f) alpha = (1f - (scrollOffset / 600f)).coerceIn(0f, 1f) } ) { Image( painter = painterResource(R.drawable.header), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } } ``` ### Animated Tab Switch ```kotlin @Composable fun AnimatedTabContent(selectedTabIndex: Int) { AnimatedContent( targetState = selectedTabIndex, transitionSpec = { val direction = if (targetState > initialState) 1 else -1 slideInHorizontally( initialOffsetX = { fullWidth -> direction * fullWidth }, animationSpec = tween(300) ) + fadeIn(animationSpec = tween(300)) togetherWith slideOutHorizontally( targetOffsetX = { fullWidth -> -direction * fullWidth }, animationSpec = tween(300) ) + fadeOut(animationSpec = tween(300)) using SizeTransform(clip = false) }, label = "tab_content" ) { tabIndex -> when (tabIndex) { 0 -> TabOneContent() 1 -> TabTwoContent() 2 -> TabThreeContent() } } } ``` --- ## Sequential/Parallel Animation Choreography ### Sequential (Coroutine Chaining) Each `animateTo` suspends until complete, so chaining them creates sequential animation: ```kotlin val alpha = remember { Animatable(0f) } val translateY = remember { Animatable(100f) } val scale = remember { Animatable(0.5f) } LaunchedEffect(Unit) { alpha.animateTo(1f, animationSpec = tween(300)) translateY.animateTo(0f, animationSpec = spring()) scale.animateTo(1f, animationSpec = tween(200)) } ``` ### Parallel (Multiple launch blocks) ```kotlin val alpha = remember { Animatable(0f) } val translateY = remember { Animatable(100f) } LaunchedEffect(Unit) { coroutineScope { launch { alpha.animateTo(1f, animationSpec = tween(300)) } launch { translateY.animateTo(0f, animationSpec = spring()) } } // Code here runs after BOTH animations complete } ``` ### Staggered Delays ```kotlin val items = remember { List(5) { Animatable(0f) } } LaunchedEffect(Unit) { items.forEachIndexed { index, animatable -> launch { delay(index * 80L) animatable.animateTo(1f, animationSpec = spring()) } } } ``` ### Mixed Sequential + Parallel ```kotlin LaunchedEffect(Unit) { // Phase 1: Sequential — fade in first alpha.animateTo(1f, animationSpec = tween(200)) // Phase 2: Parallel — move and scale at the same time coroutineScope { launch { translateY.animateTo(0f, animationSpec = spring()) } launch { scale.animateTo(1f, animationSpec = spring()) } } // Phase 3: Sequential — final flourish after Phase 2 completes rotation.animateTo(360f, animationSpec = tween(400)) } ``` --- ## Predictive Back Gesture Animation (Android 14+) ### NavHost Transitions ```kotlin NavHost( navController = navController, startDestination = "home", enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn(animationSpec = tween(300)) }, exitTransition = { slideOutHorizontally(targetOffsetX = { -it / 3 }) + fadeOut(animationSpec = tween(300)) }, popEnterTransition = { slideInHorizontally(initialOffsetX = { -it / 3 }) + fadeIn(animationSpec = tween(300)) }, popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut(animationSpec = tween(300)) } ) { composable("home") { HomeScreen() } composable("detail") { DetailScreen() } } ``` ### PredictiveBackHandler ```kotlin @Composable fun PredictiveBackExample(onBack: () -> Unit) { var boxScale by remember { mutableFloatStateOf(1f) } PredictiveBackHandler(enabled = true) { progress: Flow -> try { progress.collect { backEvent -> boxScale = 1f - (0.3f * backEvent.progress) } onBack() } catch (e: CancellationException) { boxScale = 1f throw e } } Box( modifier = Modifier .fillMaxSize() .graphicsLayer { scaleX = boxScale scaleY = boxScale } ) { Text("Swipe back to see scale animation") } } ``` ### M3 Automatic Predictive Back These Material 3 components animate with predictive back gestures out of the box (no extra code needed): - `SearchBar` — collapses back on swipe - `ModalBottomSheet` — slides down with gesture progress - `ModalNavigationDrawer` — slides closed with gesture progress --- ## Additional Anti-Patterns ### Don't: Read animated state in composition when draw-phase suffices ```kotlin // BAD: Reads alpha during composition, triggers recomposition every frame val alpha by animateFloatAsState(targetValue = 0.5f, label = "alpha") Box(modifier = Modifier.alpha(alpha)) // GOOD: Reads alpha during draw phase only, skips recomposition val alpha by animateFloatAsState(targetValue = 0.5f, label = "alpha") Box(modifier = Modifier.graphicsLayer { this.alpha = alpha }) ``` ### Don't: Use offset(x, y) for animated movement ```kotlin // BAD: Parameter-based offset triggers recomposition + relayout val animatedDp by animateDpAsState(targetValue = 100.dp, label = "x") Box(modifier = Modifier.offset(x = animatedDp)) // BETTER: Lambda offset — layout phase only, no recomposition val animatedPx by animateIntAsState(targetValue = 300, label = "x") Box(modifier = Modifier.offset { IntOffset(animatedPx, 0) }) // BEST: graphicsLayer — draw phase only val animatedPx by animateFloatAsState(targetValue = 300f, label = "x") Box(modifier = Modifier.graphicsLayer { translationX = animatedPx }) ``` ### Don't: Use updateTransition for independent properties ```kotlin // BAD: Properties don't need synchronization but are coupled val transition = updateTransition(targetState = state, label = "t") val alpha by transition.animateFloat(label = "a") { if (it) 1f else 0f } val size by transition.animateDp(label = "s") { if (it) 200.dp else 100.dp } // GOOD: Independent properties use separate animate*AsState val alpha by animateFloatAsState(targetValue = if (state) 1f else 0f, label = "alpha") val size by animateDpAsState(targetValue = if (state) 200.dp else 100.dp, label = "size") ``` ### Don't: Hardcode arbitrary durations ```kotlin // BAD: Arbitrary duration with no design rationale val anim by animateFloatAsState( targetValue = 1f, animationSpec = tween(durationMillis = 347), label = "anim" ) // GOOD: Use M3 motion tokens for consistency val anim by animateFloatAsState( targetValue = 1f, animationSpec = tween(durationMillis = MotionTokens.DurationMedium2.toInt()), label = "anim" ) // BETTER: Use spring() for interruptible, natural-feeling animations val anim by animateFloatAsState( targetValue = 1f, animationSpec = spring(stiffness = Spring.StiffnessMedium), label = "anim" ) ``` ================================================ FILE: .claude/skills/compose-expert/references/atomic-design.md ================================================ # Atomic Design System Reference Building reusable, hierarchical component systems in Jetpack Compose and Compose Multiplatform. Based on Brad Frost's atomic design methodology, mapped to Compose primitives. --- ## 1. The 5-Level Hierarchy Mapped to Compose | Level | Compose Equivalent | Examples | |-------|-------------------|----------| | **Tokens** | `MaterialTheme.colorScheme.*`, `MaterialTheme.typography.*`, custom `CompositionLocal` tokens (spacing, elevation, brand colors) | `AppTheme.spacing.medium`, `AppTheme.colors.brandPrimary` | | **Atoms** | Single-purpose composables with one responsibility, slot API, modifier param. Either wrap M3 or build custom. | `AppButton`, `AppTextField`, `AppAvatar`, `AppIcon` | | **Molecules** | Composables that combine 2+ atoms into a functional unit | `SearchBar` (icon + text field), `MovieCard` (image + text), `UserListItem` | | **Organisms** | Screen sections combining molecules into a UI region | `MovieCatalogRow` (header + LazyRow of MovieCards), `NavigationDrawerWithContent` | | **Templates** | Screen layouts defining content areas without data — `Scaffold` + slot composition | `MainScreenTemplate(topBar, content, bottomBar)`, `DetailScreenTemplate(hero, body, actions)` | **Dependency rule:** each level depends only on levels below it. An organism should not use raw `Text()` — it should use an atom. A molecule should not hardcode colors — it should use tokens via `MaterialTheme` or custom `CompositionLocal`. ``` Template └── Organism └── Molecule └── Atom └── Token (MaterialTheme / CompositionLocal) ``` --- ## 2. Token Layer Tokens are the foundation. Every visual property (color, typography, spacing, shape, motion) should come from a token — never hardcoded in a composable body. ### M3 tokens (use directly) These are already provided by `MaterialTheme`: - `MaterialTheme.colorScheme` — primary, secondary, surface, error, etc. - `MaterialTheme.typography` — displayLarge through labelSmall - `MaterialTheme.shapes` — extraSmall through extraLarge - `MaterialTheme.motionScheme` — `defaultSpatialSpec()`, `defaultEffectsSpec()` ### App-level custom tokens Create when M3 doesn't cover your need. Use `CompositionLocal` + a wrapper theme. **Spacing scale:** ```kotlin object AppSpacing { val xxs = 2.dp val xs = 4.dp val sm = 8.dp val md = 16.dp val lg = 24.dp val xl = 32.dp } val LocalAppSpacing = staticCompositionLocalOf { AppSpacing } ``` **Brand colors (beyond M3 colorScheme):** ```kotlin data class AppBrandColors( val accent: Color, val onAccent: Color, val surface: Color, ) val LocalAppBrandColors = staticCompositionLocalOf { AppBrandColors( accent = Color.Unspecified, onAccent = Color.Unspecified, surface = Color.Unspecified, ) } ``` **Access pattern — wrap in `AppTheme`:** ```kotlin @Composable fun AppTheme(content: @Composable () -> Unit) { CompositionLocalProvider( LocalAppSpacing provides AppSpacing, LocalAppBrandColors provides AppBrandColors( accent = Color(0xFF1A73E8), onAccent = Color.White, surface = Color(0xFFF5F5F5), ) ) { MaterialTheme( colorScheme = /* your color scheme */, typography = /* your typography */, shapes = /* your shapes */, ) { content() } } } // Usage anywhere in the tree: val spacing = LocalAppSpacing.current val brandColors = LocalAppBrandColors.current ``` **When to create a custom token vs. use M3 directly:** - M3 covers it → use `MaterialTheme.*` directly - App-specific concept (brand accent, spacing scale, elevation scale) → custom `CompositionLocal` - One-off value needed in a single component → not a token, just a local constant --- ## 3. Atom Patterns Atoms are the smallest reusable UI units. Every atom must satisfy the **atom contract**. ### Atom Contract Every atom (public composable that renders UI) must satisfy: 1. **`modifier: Modifier = Modifier` parameter** — caller controls layout 2. **Slot APIs for variable content** — `@Composable () -> Unit` or scoped like `@Composable RowScope.() -> Unit` 3. **Token-based styling** — no hardcoded `Color(0xFF...)`, `14.sp`, `FontWeight.Bold` 4. **Sensible defaults** — works without configuration 5. **Preview composable** — `@Preview` function for visual verification ### Two atom types **1. M3 wrapper atoms** — wrap an M3 component with brand defaults: ```kotlin @Composable fun AppButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, content: @Composable RowScope.() -> Unit, ) { Button( onClick = onClick, modifier = modifier, enabled = enabled, colors = ButtonDefaults.buttonColors( containerColor = LocalAppBrandColors.current.accent, contentColor = LocalAppBrandColors.current.onAccent, ), content = content, ) } ``` **2. Custom atoms** — when no M3 equivalent exists: ```kotlin @Composable fun AppAvatar( imageUrl: String, size: AvatarSize = AvatarSize.Medium, modifier: Modifier = Modifier, contentDescription: String? = null, ) { AsyncImage( model = imageUrl, contentDescription = contentDescription, contentScale = ContentScale.Crop, modifier = modifier .size(size.dp) .clip(CircleShape) ) } enum class AvatarSize(val dp: Dp) { Small(24.dp), Medium(40.dp), Large(56.dp) } ``` ### Naming rule Name by what the component **IS**, not where it's used. | Bad | Good | Why | |-----|------|-----| | `ButtonWithBoldCTA` | `AppButton` | The boldness is a style variant, not a component | | `RedBorderCard` | `HighlightCard` or `AppCard` | Named by visual appearance, not function | | `HomeMovieCard` | `MovieCard` | Named by screen, not reusable | | `ButtonForSettings` | `AppButton` | Named by context, not function | --- ## 4. Molecule, Organism, and Template Patterns ### Molecule — composes 2+ atoms A molecule combines atoms into a functional unit. It accepts data and callbacks, not ViewModels. ```kotlin @Composable fun MovieCard( movie: Movie, onClick: () -> Unit, modifier: Modifier = Modifier, ) { AppCard(onClick = onClick, modifier = modifier) { AppImage(url = movie.posterUrl, contentDescription = movie.title) AppText(text = movie.title, style = MaterialTheme.typography.titleSmall) AppText(text = movie.year.toString(), style = MaterialTheme.typography.bodySmall) } } ``` ### Organism — composes molecules into a UI region An organism is a screen section. It still accepts data as parameters — never reads from a ViewModel directly. ```kotlin @Composable fun MovieCatalogRow( title: String, movies: List, onMovieClick: (Movie) -> Unit, modifier: Modifier = Modifier, ) { Column(modifier = modifier) { AppText(text = title, style = MaterialTheme.typography.headlineSmall) LazyRow( horizontalArrangement = Arrangement.spacedBy(LocalAppSpacing.current.sm) ) { items(movies, key = { it.id }) { movie -> MovieCard(movie = movie, onClick = { onMovieClick(movie) }) } } } } ``` ### Template — defines screen layout via slot composition Templates define where content goes, not what it is. They accept slot parameters, no data. ```kotlin @Composable fun CatalogScreenTemplate( topBar: @Composable () -> Unit, hero: @Composable () -> Unit, sections: @Composable () -> Unit, modifier: Modifier = Modifier, ) { Scaffold(topBar = topBar, modifier = modifier) { padding -> LazyColumn(contentPadding = padding) { item { hero() } item { sections() } } } } ``` ### Level summary | Level | Accepts | Composes | ViewModel? | |-------|---------|----------|-----------| | Atom | Primitives, slots, modifier | M3 components or raw Compose | No | | Molecule | Data classes, callbacks, modifier | Atoms | No | | Organism | Data, callbacks, modifier | Molecules + atoms | No | | Template | Slots only, modifier | Scaffold + layout | No | | Screen | ViewModel | Template + organisms + molecules | Yes — this is the only level that touches ViewModel | --- ## 5. The "Ask" Prompt When the skill detects component-building intent (user asks to "build a card", "create a button", "implement this component"), **before scaffolding code**, ask: > "This looks like a **[molecule/organism]**. Should I also scaffold the **[lower-level] atoms** > it needs, or does your codebase already have them?" The developer can answer: | Answer | Skill behavior | |--------|---------------| | "Yes, scaffold everything" | Create from token layer up — define spacing/color tokens, atoms, then the requested component | | "Just build the card" | Build the requested component using atomic principles (slots, modifier, tokens) but don't create lower-level atoms | | "We already have AppButton, AppImage" | Reuse those atoms, only build the new molecule/organism | **The skill always applies atomic principles regardless of the answer.** The question is only about whether to scaffold lower levels. Every component gets: - `modifier: Modifier = Modifier` - Slot APIs where appropriate - Token-based styling (no hardcoded values) - Sensible defaults --- ## 6. Anti-Patterns | Anti-Pattern | Why It's Wrong | Fix | |-------------|---------------|-----| | `Color(0xFF1A73E8)` inside a composable body | Hardcoded color — not themeable, not dark-mode-safe | Use `MaterialTheme.colorScheme.*` or app brand token | | `fontSize = 14.sp`, `fontWeight = FontWeight.Bold` | Hardcoded typography breaks consistency | Use `MaterialTheme.typography.*` | | `Modifier.padding(16.dp)` without spacing token | Magic number spacing — inconsistent across app | Use `LocalAppSpacing.current.md` (or define a spacing scale) | | Composable named `ButtonForSettings` / `CardWithRedBorder` | Named by context, not by function — not reusable | Name by what it IS: `AppButton`, `HighlightCard` | | Public composable with no `modifier` parameter | Caller cannot control layout | Add `modifier: Modifier = Modifier`, pass to root element | | Composable rendering UI with no slot parameters | Content is hardcoded, not composable | Add slot APIs for variable content | | Raw `Text()` / `Button()` / `Icon()` in an organism | Skips atomic levels — loses theming and consistency | Use app-level atom wrappers | | Organism that directly reads ViewModel | Couples UI to data layer — not reusable, not previewable | Accept data and callbacks as parameters; let the screen call ViewModel | | Molecule with more than 3–4 responsibilities | Too much in one component — hard to reuse parts | Decompose into smaller molecules or extract atoms | ================================================ FILE: .claude/skills/compose-expert/references/auto-init.md ================================================ # Auto-Init: Compose Project Detection Activate on `session_start`. Detect whether the current project uses Compose and silently activate the skill with a brief announcement. --- ## Detection Gate Run in order. Stop on first match. ### Step 1 — Gradle scan Look for `build.gradle.kts`, `build.gradle`, or `libs.versions.toml` in the working directory or one level up. Check file contents for any of: - `compose` - `androidx.compose` - `org.jetbrains.compose` - `compose-multiplatform` ```bash # Check working directory and parent for dir in . ..; do for file in build.gradle.kts build.gradle libs.versions.toml; do if [ -f "$dir/$file" ]; then grep -qi "compose\|androidx\.compose\|org\.jetbrains\.compose\|compose-multiplatform" "$dir/$file" && echo "DETECTED" && break 2 fi done done ``` ### Step 2 — Source scan fallback If no Gradle file found or no Compose reference in Gradle, scan Kotlin source files. ```bash # Find up to 10 .kt files (exclude build dirs), check for @Composable find . -name "*.kt" -not -path "*/build/*" -print -quit 2>/dev/null | head -10 | \ xargs grep -l "@Composable" 2>/dev/null | head -1 ``` If any file contains `@Composable`, detection succeeds. --- ## On Detection Print one line: ``` Compose project detected — compose-expert skill active. ``` Then proceed normally — wait for the user's request and follow the standard workflow in `SKILL.md`. ## On No Detection Do nothing. The skill remains available if the user explicitly triggers it via keyword (e.g., mentions `@Composable`, `LazyColumn`, `NavHost`, etc.) later in the session. ================================================ FILE: .claude/skills/compose-expert/references/composition-locals.md ================================================ # CompositionLocals: Implicit Data Passing in Jetpack Compose CompositionLocals provide a way to pass data implicitly down the composition tree without threading it through every function parameter. They're analogous to SwiftUI's `@Environment`. ## What Are CompositionLocals? A CompositionLocal is a slot in the composition that holds a value accessible to any descendant composable without explicit parameter passing. Values are provided using `CompositionLocalProvider` and accessed via `current`. ```kotlin val localAppTheme = compositionLocalOf { "Light" } @Composable fun MyScreen() { CompositionLocalProvider(localAppTheme provides "Dark") { DescendantComposable() // Can access "Dark" via localAppTheme.current } } @Composable fun DescendantComposable() { Text(localAppTheme.current) // Reads "Dark" } ``` **Source:** `androidx/compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt` ## compositionLocalOf vs staticCompositionLocalOf The key difference is **when recomposition is triggered** when a value changes. ### compositionLocalOf Causes recomposition of all descendants when the value changes. Use when children genuinely depend on the value. ```kotlin val LocalUserPreferences = compositionLocalOf { UserPreferences() } ``` **Recomposition behavior:** All consumers recompose. ### staticCompositionLocalOf No recomposition of descendants; only the direct reader is affected. Use when you're **confident descendants don't depend on updates**, or updates are infrequent. ```kotlin val LocalAppVersion = staticCompositionLocalOf { "1.0.0" } ``` **⚠️ Pitfall:** If a child reads `LocalAppVersion.current` and expects updates, you'll get stale data. Only use for truly static configuration. ### compositionLocalWithComputedDefaultOf Introduced for computed default values. The lambda is called each time the value is read when no provider is active. ```kotlin val LocalResources = compositionLocalWithComputedDefaultOf { context.resources } ``` This is more efficient than `compositionLocalOf { lazy { ... } }` because it avoids capturing state unnecessarily. ## Built-In CompositionLocals The Compose runtime and UI libraries provide standard locals: | Local | Type | Purpose | |-------|------|---------| | `LocalContext` | `Context` | Android Context (requires AndroidCompositionLocals) | | `LocalConfiguration` | `Configuration` | Screen size, orientation, density | | `LocalDensity` | `Density` | Pixel density for dp/px conversion | | `LocalLayoutDirection` | `LayoutDirection` | LTR/RTL directionality | | `LocalView` | `View` | Underlying Android View (if available) | | `LocalLifecycleOwner` | `LifecycleOwner` | Activity/Fragment lifecycle | | `LocalSavedStateRegistryOwner` | `SavedStateRegistryOwner` | For state persistence | **Source:** `androidx/compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt` ```kotlin @Composable fun MyComposable() { val context = LocalContext.current val density = LocalDensity.current val config = LocalConfiguration.current Text("Screen width: ${config.screenWidthDp}dp") } ``` ## Providing Values with CompositionLocalProvider Provide one or multiple local values: ```kotlin // Single local CompositionLocalProvider(LocalUserPreferences provides user) { Content() } // Multiple locals CompositionLocalProvider( LocalUserPreferences provides user, LocalTheme provides darkTheme, LocalLanguage provides "en" ) { Content() } ``` Values are **scoped** to descendants only: ```kotlin CompositionLocalProvider(LocalUserPreferences provides userA) { ComponentA() // Sees userA CompositionLocalProvider(LocalUserPreferences provides userB) { ComponentB() // Sees userB (overrides) } ComponentC() // Sees userA (original) } ``` ## Creating Custom CompositionLocals Create locals at top level, outside composable functions: ```kotlin data class AppTheme(val isDark: Boolean, val colors: Colors) val LocalAppTheme = compositionLocalOf { error("AppTheme not provided") } // For nullable defaults val LocalOptionalUser = compositionLocalOf { null } ``` **When to create a CompositionLocal:** - Value is needed by many descendants - Threading it as a parameter creates "prop drilling" - Value is configuration-like (theme, locale, permissions) **When NOT to use CompositionLocal:** - Only 1–2 levels of composables need it → use parameters - Value changes frequently and children need precise control → use State/ViewModel - It's a dependency that should be testable → prefer parameters or dependency injection ## Testing with CompositionLocals Provide test doubles to avoid real implementations: ```kotlin @Composable fun MyScreen() { val user = LocalUserRepository.current Text(user.name) } // In test @Test fun testMyScreen() { composeRule.setContent { CompositionLocalProvider( LocalUserRepository provides FakeUserRepository(User("Test User")) ) { MyScreen() } } composeRule.onNodeWithText("Test User").assertExists() } ``` ## Anti-Patterns ### ✗ Using CompositionLocal as Generic Dependency Injection ```kotlin // Bad: obscures dependencies, hard to test val LocalEverything = compositionLocalOf { AppContainer() } @Composable fun MyScreen() { val container = LocalEverything.current val repo = container.userRepo val cache = container.cache } ``` **Better:** Provide specific locals or pass dependencies as parameters. ### ✗ Reading LocalContext Repeatedly ```kotlin // Inefficient: reads on every recomposition @Composable fun MyComposable() { val context = LocalContext.current // Reading repeatedly // ... } ``` **Better:** Read once outside the lambda or cache in remember: ```kotlin @Composable fun MyComposable() { val context = LocalContext.current val effect = remember(context) { /* use context */ } } ``` ### ✗ Storing Mutable State in CompositionLocal ```kotlin // Bad: state changes won't trigger recomposition properly val LocalCounter = compositionLocalOf { mutableStateOf(0) } ``` **Better:** Store the State in a parent composable and provide the value, not the State: ```kotlin val LocalCount = compositionLocalOf { 0 } @Composable fun Parent() { var count by remember { mutableStateOf(0) } CompositionLocalProvider(LocalCount provides count) { Child() } } ``` ## Key Takeaways 1. Use `compositionLocalOf` for values that children read and depend on updates 2. Use `staticCompositionLocalOf` only for truly static values 3. Prefer parameters over CompositionLocals unless you have significant nesting 4. Always provide a sensible error default or nullable type 5. Test by providing fake implementations via `CompositionLocalProvider` 6. CompositionLocals are not a replacement for proper architecture — use them for configuration and environment data, not general dependency injection ================================================ FILE: .claude/skills/compose-expert/references/deprecated-patterns.md ================================================ # Deprecated Patterns & API Migrations in Jetpack Compose This guide covers major API changes and deprecations in Compose's evolution. Each section shows the old pattern → new approach with migration notes. --- ## String-Based Routes → Type-Safe `@Serializable` Routes **Old (pre-2.8):** ```kotlin NavHost(navController, startDestination = "home") { composable("home") { HomeScreen() } composable("details/{id}") { backStackEntry -> DetailsScreen(id = backStackEntry.arguments?.getString("id")) } } ``` **New (Navigation 2.8+):** ```kotlin @Serializable data class Home @Serializable data class Details(val id: String) NavHost(navController, startDestination = Home) { composable { HomeScreen() } composable
{ backStackEntry -> val args: Details = backStackEntry.toRoute() DetailsScreen(id = args.id) } } ``` **Migration notes:** Type-safe routes eliminate string typos and runtime crashes. Requires `kotlinx-serialization` plugin and `navigation-compose:2.8.0+`. Encode complex objects using custom serializers. --- ## `accompanist-systemuicontroller` → `enableEdgeToEdge()` **Old:** ```kotlin val systemUiController = rememberSystemUiController() systemUiController.setSystemBarsColor( color = Color.Transparent, darkIcons = false ) ``` **New (Compose 1.7+):** ```kotlin enableEdgeToEdge() // In Activity.onCreate() before setContent {} ``` **Migration notes:** Built-in since Compose 1.7. Automatically handles status bar, navigation bar, and IME behind content. Remove `accompanist-systemuicontroller` dependency entirely. --- ## `accompanist-pager` → `HorizontalPager`/`VerticalPager` **Old:** ```kotlin val pagerState = rememberPagerState() HorizontalPager(count = items.size, state = pagerState) { page -> PageContent(items[page]) } ``` **New (Foundation):** ```kotlin val pagerState = rememberPagerState(pageCount = { items.size }) HorizontalPager(state = pagerState) { page -> PageContent(items[page]) } ``` **Migration notes:** Native Pager in `foundation:1.6+` replaces accompanist. Removes external dependency. State initialization slightly different; pass lambda for dynamic page counts. --- ## `accompanist-swiperefresh` → `PullToRefreshBox` **Old:** ```kotlin SwipeRefresh(state = rememberSwipeRefreshState(isRefreshing), onRefresh = { load() }) { LazyColumn { items(data) { item -> ItemRow(item) } } } ``` **New (Material3):** ```kotlin PullToRefreshBox(isRefreshing = isRefreshing, onRefresh = { load() }) { LazyColumn { items(data) { item -> ItemRow(item) } } } ``` **Migration notes:** `PullToRefreshBox` in `material3:1.2+` is the official replacement. Cleaner API. Remove `accompanist-swiperefresh` dependency. --- ## `accompanist-flowlayout` → `FlowRow`/`FlowColumn` **Old:** ```kotlin FlowRow(mainAxisSize = SizeMode.Expand) { items.forEach { item -> Chip(text = item) } } ``` **New (Foundation):** ```kotlin FlowRow(modifier = Modifier.fillMaxWidth()) { items.forEach { item -> Chip(text = item) } } ``` **Migration notes:** FlowRow/FlowColumn in `foundation:1.6+`. API simplified; use standard modifiers instead of `SizeMode`. Better performance and less memory overhead. --- ## `LazyColumn { animateItemPlacement() }` → `LazyColumn { animateItem() }` **Old:** ```kotlin LazyColumn { items(items, key = { it.id }) { item -> ItemRow(item.name, Modifier.animateItemPlacement()) } } ``` **New:** ```kotlin LazyColumn { items(items, key = { it.id }) { item -> ItemRow(item.name, Modifier.animateItem()) } } ``` **Migration notes:** `animateItem()` is the modern API (Compose 1.7+). Returns animation state for finer control. `animateItemPlacement()` still works but is superseded. --- ## `Modifier.composed` Pattern → `Modifier.Node` API **Old:** ```kotlin fun Modifier.myModifier(value: Int) = composed { val state = remember { mutableStateOf(value) } Modifier.fillMaxWidth().padding(8.dp) } ``` **New:** ```kotlin fun Modifier.myModifier(value: Int) = this.then( Modifier .fillMaxWidth() .padding(8.dp) ) // Or for complex state: class MyModifierNode(val value: Int) : ModifierNodeElement() { override fun create() = MyNodeImpl(value) override fun update(node: MyNodeImpl) { node.value = value } } private class MyNodeImpl(var value: Int) : Modifier.Node ``` **Migration notes:** `composed {}` incurs overhead; avoid if no `remember` calls needed. For stateful modifiers, prefer `ModifierNode` API (Compose 1.8+). Benchmark before migrating existing code. --- ## Primitive State Optimization: `mutableStateOf(0)` → `mutableIntStateOf(0)` **Old:** ```kotlin var count by remember { mutableStateOf(0) } var temperature by remember { mutableStateOf(37.5f) } ``` **New:** ```kotlin var count by remember { mutableIntStateOf(0) } var temperature by remember { mutableFloatStateOf(37.5f) } ``` **Migration notes:** Primitive-specific functions (`mutableIntStateOf`, `mutableFloatStateOf`, `mutableLongStateOf`) avoid boxing. Negligible performance impact in UI code but best practice since Compose 1.4+. --- ## `collectAsState()` → `collectAsStateWithLifecycle()` **Old:** ```kotlin val state by viewModel.uiState.collectAsState() ``` **New:** ```kotlin val state by viewModel.uiState.collectAsStateWithLifecycle() ``` **Migration notes:** `collectAsStateWithLifecycle()` (Compose 1.6+) respects lifecycle—automatically stops collecting when activity is paused. Prevents memory leaks and redundant work. Requires `androidx.lifecycle:lifecycle-runtime-compose`. --- ## `@ExperimentalMaterial3Api` Graduation **Old:** ```kotlin @OptIn(ExperimentalMaterial3Api::class) fun MyScreen() { DatePicker(state = rememberDatePickerState()) } ``` **New (Compose 1.8+, Material3 1.3+):** ```kotlin fun MyScreen() { DatePicker(state = rememberDatePickerState()) } ``` **Migration notes:** DatePicker, TimePicker, ExposedDropdownMenuBox, and SearchBar graduated to stable in Material3 1.3+. Remove `@OptIn` annotations. APIs are stable—safe for production use. --- ## `Scaffold` Padding Enforcement **Old (problematic):** ```kotlin Scaffold(topBar = { TopAppBar() }) { LazyColumn { items(data) { item -> ItemRow(item) } } } ``` **New (required since 1.6+):** ```kotlin Scaffold(topBar = { TopAppBar() }) { innerPadding -> LazyColumn(modifier = Modifier.padding(innerPadding)) { items(data) { item -> ItemRow(item) } } } ``` **Migration notes:** Must use `innerPadding` parameter since Compose 1.6. Ignoring it causes content overlap under system bars. The compiler enforces this now—old pattern won't compile. --- ## Material 2 → Material 3 Migration **Old (Material):** ```kotlin Button(onClick = { }) { Text("Click") } TextField(value = text, onValueChange = { text = it }) Surface(color = MaterialTheme.colors.primary) { /* */ } ``` **New (Material3):** ```kotlin Button(onClick = { }) { Text("Click") } // Same signature TextField(value = text, onValueChange = { text = it }) // Same signature Surface(color = MaterialTheme.colorScheme.primary) { /* */ } ``` **Migration notes:** Most Composables are API-compatible. Main changes: `colors` → `colorScheme`, new shape system, updated ripple defaults. Use Compose BOM to align Material3 versions. --- ## `WindowInsets` & Edge-to-Edge **Old:** ```kotlin Surface(modifier = Modifier.systemBarsPadding()) { /* */ } ``` **New (API 35+ default edge-to-edge):** ```kotlin Surface(modifier = Modifier.padding(WindowInsets.systemBars.asPaddingValues())) { /* */ } // Or use enableEdgeToEdge() in Activity—handles automatically ``` **Migration notes:** Edge-to-edge is default on Android 15+. System bar colors are managed by `enableEdgeToEdge()`. Use `WindowInsets.safeDrawing` for notch-aware layouts. Deprecate manual `systemBarsPadding()` calls. --- ## `ObservableState` Pattern Changes **Old:** ```kotlin @Composable fun observe(state: ObservableState): State = produceState(state.value) { state.onChange { value = it } } ``` **New:** ```kotlin @Composable fun ObservableState.asState(): State = produceState(this.value) { snapshotFlow { value }.collect { value = it } } ``` **Migration notes:** `snapshotFlow {}` is preferred over direct listeners (Compose 1.6+). Integrates better with Compose's snapshot system. Use `distinctUntilChanged()` to avoid redundant recompositions. ================================================ FILE: .claude/skills/compose-expert/references/design-to-compose.md ================================================ # Design-to-Compose Translation Reference Translating visual designs (Figma mockups, screenshots, wireframes) into production Compose code. This guide provides a systematic decomposition algorithm, property mapping tables, and patterns that produce clean, theme-aware, accessible composables on the first pass. --- ## 1. Composable Decomposition Algorithm A divide-and-conquer approach for breaking any visual design into composable functions. Work top-down, outside-in. ### Step 1: Identify Root Layout Structure Look at the full screen first. What is the outermost structural pattern? | Visual Pattern | Compose Root | |---|---| | Top bar + content + bottom bar | `Scaffold` | | Scrollable vertical content | `Column` + `verticalScroll()` or `LazyColumn` | | Tabbed sections | `Scaffold` + `TabRow` + `HorizontalPager` | | Full-bleed background with overlays | `Box` | | Side drawer + content | `ModalNavigationDrawer` + `Scaffold` | | Bottom sheet over content | `ModalBottomSheet` or `Scaffold` + `BottomSheetScaffold` | ### Step 2: Decompose into Visual Sections (Top-Down) Scan the design from top to bottom. Draw horizontal lines between visually distinct sections. Each section becomes a composable or a block within the parent layout. ``` +---------------------------+ | Top App Bar | -> TopAppBar() +---------------------------+ | Hero Image | -> HeroSection() +---------------------------+ | Title + Subtitle | -> HeaderSection() +---------------------------+ | Horizontal card list | -> FeaturedCardsRow() +---------------------------+ | Vertical item list | -> ItemList() +---------------------------+ | Bottom navigation | -> NavigationBar() +---------------------------+ ``` **Do:** Name sections by their purpose (`FeaturedCardsRow`), not their layout (`HorizontalScrollRow`). **Don't:** Create a composable for every Figma frame. Flatten where possible. ### Step 3: For Each Section, Identify Layout Type ``` Is content stacked vertically? └─ Yes → Column └─ Is list dynamic/long? → LazyColumn Is content arranged horizontally? └─ Yes → Row └─ Does it scroll? → LazyRow └─ Does it wrap to next line? → FlowRow Is content overlapping/layered? └─ Yes → Box Is it a grid? └─ Fixed columns → LazyVerticalGrid └─ Fixed item size → LazyVerticalStaggeredGrid └─ Wrapping chips/tags → FlowRow ``` **Decision tree for layout selection:** ``` ┌─ Overlapping layers? ──→ Box │ Visual section ───┼─ Single axis? ──→ Vertical? ──→ Column / LazyColumn │ └─ Horizontal? ──→ Row / LazyRow │ └─ Grid / wrap? ──→ Fixed columns? ──→ LazyVerticalGrid └─ Flowing tags? ──→ FlowRow ``` ### Step 4: Extract Visual Properties For each element, read these from the design: - **Colors** — map to `MaterialTheme.colorScheme.*` tokens, not hex values - **Typography** — map to `MaterialTheme.typography.*` text styles - **Spacing** — padding and gaps in dp, map to theme spacing tokens - **Elevation** — shadow depth, map to `tonalElevation` or `shadowElevation` - **Corner radius** — map to `MaterialTheme.shapes.*` ### Step 5: Identify Interactive Elements and Map to M3 Components | Visual Element | Compose M3 Component | |---|---| | Rounded rectangle with text + click | `Button` / `OutlinedButton` / `TextButton` | | Card with image, title, subtitle | `Card` / `ElevatedCard` / `OutlinedCard` | | Text input field | `TextField` / `OutlinedTextField` | | Toggle switch | `Switch` | | Checkbox | `Checkbox` / `TriStateCheckbox` | | Chips / tags | `FilterChip` / `AssistChip` / `InputChip` / `SuggestionChip` | | Floating action button | `FloatingActionButton` / `ExtendedFloatingActionButton` | | Bottom navigation | `NavigationBar` + `NavigationBarItem` | | Side navigation | `NavigationRail` / `NavigationDrawer` | | Top bar | `TopAppBar` / `CenterAlignedTopAppBar` | | Dialog / modal | `AlertDialog` / `BasicAlertDialog` | | Progress indicator | `CircularProgressIndicator` / `LinearProgressIndicator` | | Divider line | `HorizontalDivider` | | Image with rounded corners | `Image` + `Modifier.clip()` | | Dropdown menu | `ExposedDropdownMenuBox` | | Slider | `Slider` / `RangeSlider` | --- ## 2. Figma-to-Compose Property Mapping Tables ### Layout Containers | Figma Concept | Compose Equivalent | |---|---| | Frame (no auto-layout) | `Box` | | Auto-layout Vertical | `Column` | | Auto-layout Horizontal | `Row` | | Auto-layout Wrap | `FlowRow` / `FlowColumn` | | Grid (fixed columns) | `LazyVerticalGrid(columns = GridCells.Fixed(n))` | | Absolute-positioned child | `Box` + `Modifier.offset(x, y)` or `Modifier.align()` | | Component with variants | Composable function with parameters | | Component instance | Function call site | | Section / Group (organizational) | No composable needed; flatten into parent | ### Sizing Modes | Figma Sizing | Compose Modifier | |---|---| | Fixed width/height | `Modifier.size(w.dp, h.dp)` or `.width(w.dp).height(h.dp)` | | Hug contents | Default behavior (wrap content) -- no modifier needed | | Fill container (horizontal) | `Modifier.fillMaxWidth()` | | Fill container (vertical) | `Modifier.fillMaxHeight()` | | Fill container (both) | `Modifier.fillMaxSize()` | | Fill with min width | `Modifier.fillMaxWidth().widthIn(min = minW.dp)` | | Fill with max width | `Modifier.fillMaxWidth().widthIn(max = maxW.dp)` | | Fill with min/max height | `Modifier.fillMaxHeight().heightIn(min = ..., max = ...)` | | Aspect ratio constraint | `Modifier.aspectRatio(ratio)` | ### Spacing Model **Key principle: the parent owns spacing.** | Figma Spacing | Compose Equivalent | |---|---| | Padding (all sides) | `Modifier.padding(all.dp)` on the container | | Padding (per side) | `Modifier.padding(start = ..., top = ..., end = ..., bottom = ...)` | | Gap between children (vertical auto-layout) | `Column(verticalArrangement = Arrangement.spacedBy(gap.dp))` | | Gap between children (horizontal auto-layout) | `Row(horizontalArrangement = Arrangement.spacedBy(gap.dp))` | | Space between (distribute) | `Arrangement.SpaceBetween` | | Space around | `Arrangement.SpaceAround` | **Do:** Use `start`/`end` instead of `left`/`right` for RTL language support. ```kotlin // Correct: RTL-aware Modifier.padding(start = 16.dp, end = 8.dp) // Wrong: breaks in RTL locales Modifier.padding(left = 16.dp, right = 8.dp) // Avoid ``` **Do:** Use `Arrangement.spacedBy()` for uniform gaps. Avoid inserting `Spacer` between every child. ```kotlin // Do: clean and uniform Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { Text("First") Text("Second") Text("Third") } // Don't: manual spacers everywhere Column { Text("First") Spacer(Modifier.height(12.dp)) Text("Second") Spacer(Modifier.height(12.dp)) Text("Third") } ``` ### Shadow Mapping (Compose 1.9+) Compose Foundation 1.9 introduced `dropShadow()` and `innerShadow()` as modifier extensions, replacing the legacy `shadow()` modifier for fine-grained control. ```kotlin // Drop shadow: place BEFORE background in the modifier chain Box( Modifier .dropShadow( shape = RoundedCornerShape(12.dp), color = Color.Black.copy(alpha = 0.15f), blur = 8.dp, offsetX = 0.dp, offsetY = 4.dp, spread = 0.dp ) .background(Color.White, RoundedCornerShape(12.dp)) .padding(16.dp) ) // Inner shadow: place AFTER background in the modifier chain Box( Modifier .background(Color.White, RoundedCornerShape(12.dp)) .innerShadow( shape = RoundedCornerShape(12.dp), color = Color.Black.copy(alpha = 0.1f), blur = 4.dp, offsetX = 0.dp, offsetY = 2.dp, spread = 0.dp ) .padding(16.dp) ) ``` **Figma shadow fields to Compose mapping:** | Figma Shadow Property | Compose Parameter | |---|---| | X offset | `offsetX` | | Y offset | `offsetY` | | Blur | `blur` | | Spread | `spread` | | Color + opacity | `color = Color(hex).copy(alpha = opacity)` | | Drop Shadow type | `Modifier.dropShadow()` | | Inner Shadow type | `Modifier.innerShadow()` | **Legacy approach** (pre-1.9, still valid for simple elevation shadows): ```kotlin // Simple elevation shadow Box( Modifier .shadow(elevation = 4.dp, shape = RoundedCornerShape(12.dp)) .background(Color.White) ) ``` ### Gradient Mapping | Figma Gradient Type | Compose Brush | |---|---| | Linear gradient | `Brush.linearGradient(colors, start, end)` | | Radial gradient | `Brush.radialGradient(colors, center, radius)` | | Angular/sweep gradient | `Brush.sweepGradient(colors, center)` | Figma uses normalized coordinates (0.0 to 1.0). Convert to pixel `Offset` values: ```kotlin // Figma linear gradient: start (0, 0) to end (1, 1), 45-degree diagonal Box( Modifier .fillMaxWidth() .height(200.dp) .background( Brush.linearGradient( colors = listOf(Color(0xFF6200EE), Color(0xFF03DAC6)), start = Offset.Zero, end = Offset.Infinite // diagonal ) ) ) // For precise Figma coordinates, use onSizeChanged or BoxWithConstraints: BoxWithConstraints( Modifier.background( Brush.linearGradient( colors = listOf(Color(0xFF6200EE), Color(0xFF03DAC6)), start = Offset(0f, 0f), end = Offset(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat()) ) ) ) { // content } // Radial gradient Box( Modifier .size(200.dp) .background( Brush.radialGradient( colors = listOf(Color.White, Color.Blue), center = Offset(100f, 100f), // center of 200dp box (approx) radius = 150f ) ) ) ``` ### Corner Radius | Figma Corner Radius | Compose Shape | |---|---| | All corners equal | `RoundedCornerShape(radius.dp)` | | Per-corner values | `RoundedCornerShape(topStart = ..., topEnd = ..., bottomEnd = ..., bottomStart = ...)` | | Fully rounded (pill) | `RoundedCornerShape(50)` or `CircleShape` | | No radius | `RectangleShape` | | Cut corners | `CutCornerShape(size.dp)` | ### Borders ```kotlin // Solid border Modifier.border(width.dp, Color(0xFFCCCCCC), RoundedCornerShape(8.dp)) // Gradient border Modifier.border( width = 2.dp, brush = Brush.linearGradient(listOf(Color.Red, Color.Blue)), shape = RoundedCornerShape(8.dp) ) ``` ### Opacity | Figma Property | Compose Equivalent | |---|---| | Layer opacity | `Modifier.alpha(0.5f)` | | Fill color opacity | `Color(0xFF000000).copy(alpha = 0.5f)` | | Blend mode | `Modifier.graphicsLayer { compositingStrategy = ... }` | ### Image Fill Modes | Figma Image Mode | Compose ContentScale | |---|---| | Fill (cover, may crop) | `ContentScale.Crop` | | Fit (contain, no crop) | `ContentScale.Fit` | | Stretch (distort) | `ContentScale.FillBounds` | | Tile | Custom `DrawScope` tiling | | Fill width | `ContentScale.FillWidth` | | Fill height | `ContentScale.FillHeight` | ```kotlin Image( painter = painterResource(R.drawable.hero), contentDescription = "Hero image", contentScale = ContentScale.Crop, modifier = Modifier .fillMaxWidth() .height(200.dp) .clip(RoundedCornerShape(12.dp)) ) ``` --- ## 3. Design Token to MaterialTheme Mapping | Design System Token | MaterialTheme API | |---|---| | Color styles (Primary, Surface, Error...) | `MaterialTheme.colorScheme` | | Text styles (Heading, Body, Caption...) | `MaterialTheme.typography` | | Corner radius (Small, Medium, Large...) | `MaterialTheme.shapes` | | Spacing scale (4, 8, 16, 24...) | Custom `CompositionLocal` (see below) | | Elevation scale | Custom `CompositionLocal` (see below) | ### Custom Spacing CompositionLocal Material 3 does not ship a spacing scale. Define one that mirrors your design system: ```kotlin @Immutable data class AppSpacing( val xxs: Dp = 2.dp, val xs: Dp = 4.dp, val sm: Dp = 8.dp, val md: Dp = 16.dp, val lg: Dp = 24.dp, val xl: Dp = 32.dp, val xxl: Dp = 48.dp ) val LocalAppSpacing = staticCompositionLocalOf { AppSpacing() } // Provide in your theme wrapper @Composable fun AppTheme(content: @Composable () -> Unit) { val spacing = AppSpacing() CompositionLocalProvider(LocalAppSpacing provides spacing) { MaterialTheme( colorScheme = lightColorScheme(), typography = Typography(), shapes = Shapes() ) { content() } } } // Usage at call site @Composable fun ProfileCard() { val spacing = LocalAppSpacing.current Card( modifier = Modifier.padding(spacing.md) ) { Column( modifier = Modifier.padding(spacing.md), verticalArrangement = Arrangement.spacedBy(spacing.sm) ) { Text("Name", style = MaterialTheme.typography.titleMedium) Text("Bio", style = MaterialTheme.typography.bodyMedium) } } } ``` ### Custom Elevation CompositionLocal ```kotlin @Immutable data class AppElevation( val none: Dp = 0.dp, val xs: Dp = 1.dp, val sm: Dp = 2.dp, val md: Dp = 4.dp, val lg: Dp = 8.dp, val xl: Dp = 16.dp ) val LocalAppElevation = staticCompositionLocalOf { AppElevation() } ``` ### Mapping Figma Text Styles to Typography ```kotlin // Figma design system: Compose Typography: // Heading/H1 36sp Bold → displaySmall or headlineLarge // Heading/H2 28sp Bold → headlineMedium // Heading/H3 22sp SemiBold→ titleLarge // Body/Large 16sp Regular → bodyLarge // Body/Small 14sp Regular → bodyMedium // Caption 12sp Regular → bodySmall or labelMedium // Button 14sp Medium → labelLarge val AppTypography = Typography( headlineLarge = TextStyle( fontFamily = yourFontFamily, fontWeight = FontWeight.Bold, fontSize = 36.sp, lineHeight = 44.sp ), // ... map each Figma text style ) ``` --- ## 4. Modifier Ordering Rules **Canonical principle:** outer to inner = layout/sizing first, then decoration, then interaction. Modifiers are applied left-to-right in the chain. Each modifier wraps everything that follows it. Think of it as layers from outside in. ### Correct Ordering Patterns ```kotlin // Pattern 1: Card-like surface Modifier .fillMaxWidth() // 1. Layout sizing .padding(horizontal = 16.dp, vertical = 8.dp) // 2. External margin (space from siblings) .dropShadow( // 3. Shadow (before background) shape = RoundedCornerShape(12.dp), color = Color.Black.copy(alpha = 0.1f), blur = 8.dp, offsetY = 4.dp ) .background(Color.White, RoundedCornerShape(12.dp)) // 4. Background fill .clip(RoundedCornerShape(12.dp)) // 5. Clip content to shape .clickable { } // 6. Interaction (inside clip for ripple bounds) .padding(16.dp) // 7. Internal padding (content inset) // Pattern 2: Clickable with large touch target Modifier .fillMaxWidth() .clickable { } // Clickable AFTER padding = larger touch target .padding(16.dp) // Internal content padding // Pattern 3: Background extends to edges, padding inside Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) .padding(16.dp) ``` ### Common Mistakes ```kotlin // Wrong: padding before fillMaxWidth clips the fill area Modifier .padding(16.dp) .fillMaxWidth() // Fills remaining width AFTER padding is applied // Correct: fillMaxWidth first, then pad inward Modifier .fillMaxWidth() .padding(16.dp) // Wrong: clickable before padding = small touch target Modifier .clickable { } .padding(16.dp) // Padding is outside the clickable area // Correct: clickable after padding = padding area is clickable too Modifier .padding(16.dp) .clickable { } // Entire padded region responds to clicks // Wrong: shadow after background (invisible or clipped) Modifier .background(Color.White, RoundedCornerShape(12.dp)) .dropShadow(...) // Shadow drawn inside the background layer // Correct: shadow before background Modifier .dropShadow(...) .background(Color.White, RoundedCornerShape(12.dp)) ``` --- ## 5. Semantic vs Literal Translation Figma designs express visual output. Compose code should express semantics. Always prefer Material 3 components over manually reconstructing their appearance with `Box` + modifiers. ### Anti-pattern: Literal Translation ```kotlin // Figma shows a card: rounded rect, shadow, image, title, subtitle // Literal translation -- DON'T Box( Modifier .shadow(4.dp, RoundedCornerShape(12.dp)) .background(Color.White, RoundedCornerShape(12.dp)) .clip(RoundedCornerShape(12.dp)) ) { Column(Modifier.padding(16.dp)) { Image(painter = painterResource(R.drawable.photo), contentDescription = null) Text("Title", fontSize = 18.sp, fontWeight = FontWeight.Bold, color = Color.Black) Text("Subtitle", fontSize = 14.sp, color = Color(0xFF666666)) } } ``` ### Correct: Semantic Translation ```kotlin // Semantic translation -- DO ElevatedCard( modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium ) { Column { Image( painter = painterResource(R.drawable.photo), contentDescription = "Photo description", contentScale = ContentScale.Crop, modifier = Modifier .fillMaxWidth() .height(180.dp) ) Column(Modifier.padding(16.dp)) { Text("Title", style = MaterialTheme.typography.titleMedium) Text( "Subtitle", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } } ``` ### Why This Matters | Concern | Literal Box+Modifiers | M3 Component | |---|---|---| | Dark theme | Breaks (hardcoded colors) | Automatic | | Elevation overlay | Missing | Built-in tonal elevation | | Ripple / feedback | Must add manually | Built-in | | Accessibility | Must add semantics manually | Roles + descriptions built-in | | Dynamic color (Material You) | Does not respond | Automatic | | State handling (disabled, focused) | Manual | Built-in styling per state | **Rule:** If a Material 3 component exists for the visual pattern, use it. Only build custom layouts for genuinely novel UI elements. ### Quick Mapping: Visual Pattern to M3 Component | "It looks like a..." | Use | |---|---| | Rounded card with shadow | `ElevatedCard` | | Outlined card | `OutlinedCard` | | Pill-shaped button | `Button(shape = CircleShape)` | | Icon + label row | `ListItem` | | Search bar | `SearchBar` / `DockedSearchBar` | | Segmented control | `SegmentedButton` (M3 1.2+) | | Banner notification | `Snackbar` | | Full-width separator | `HorizontalDivider` | | Pull-to-refresh | `PullToRefreshBox` | --- ## 6. Anti-Patterns ### Over-nesting Layouts Figma designs often have deep frame hierarchies for organizational reasons. Do not mirror this nesting in Compose -- flatten aggressively. ```kotlin // Anti-pattern: mirroring Figma's 5-level nesting Box { Column { Row { Box { Column { Text("Title") Text("Subtitle") } } } } } // Correct: flatten to what layout actually requires Column { Text("Title", style = MaterialTheme.typography.titleMedium) Text("Subtitle", style = MaterialTheme.typography.bodyMedium) } ``` Deep nesting increases measure/layout passes. Each layout node is a measure cost. Flatten to the minimum tree depth that achieves the visual result. ### Hardcoded Values vs Theme Tokens ```kotlin // Anti-pattern: hardcoded hex colors and font sizes Text( text = "Hello", color = Color(0xFF1A1A1A), fontSize = 16.sp, fontWeight = FontWeight.Medium ) Box(Modifier.background(Color(0xFFF5F5F5))) // Correct: theme tokens Text( text = "Hello", style = MaterialTheme.typography.bodyLarge, color = MaterialTheme.colorScheme.onSurface ) Box(Modifier.background(MaterialTheme.colorScheme.surfaceVariant)) ``` Hardcoded values break dark theme, dynamic color, and design system updates. The only acceptable hardcoded color is inside your theme definition files. ### Ignoring Accessibility ```kotlin // Anti-pattern: no content description, tiny touch target Icon( imageVector = Icons.Default.Favorite, contentDescription = null, // Screen readers skip this modifier = Modifier .size(20.dp) // Below 48dp minimum touch target .clickable { onFavorite() } ) // Correct: accessible IconButton(onClick = onFavorite) { // IconButton enforces 48dp minimum Icon( imageVector = Icons.Default.Favorite, contentDescription = "Add to favorites" ) } ``` **Accessibility checklist for design translation:** - All interactive elements have minimum 48dp touch targets (use `IconButton`, `TextButton`, or `Modifier.sizeIn(minWidth = 48.dp, minHeight = 48.dp)`) - All images and icons have meaningful `contentDescription` (or `null` if purely decorative, paired with `Modifier.semantics { }` as needed) - Color contrast ratios meet WCAG AA (4.5:1 for text, 3:1 for large text) - Interactive states are visually distinguishable (not just color change) ### Designing for One Screen Width Only ```kotlin // Anti-pattern: fixed widths that break on tablets Row(Modifier.width(360.dp)) { Column(Modifier.width(180.dp)) { /* left panel */ } Column(Modifier.width(180.dp)) { /* right panel */ } } // Correct: responsive with WindowSizeClass val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass when (windowSizeClass.windowWidthSizeClass) { WindowWidthSizeClass.COMPACT -> { // Single column layout (phones) Column { /* all content stacked */ } } WindowWidthSizeClass.MEDIUM -> { // Two-pane list-detail (small tablets, foldables) ListDetailPaneScaffold(/* ... */) } WindowWidthSizeClass.EXPANDED -> { // Navigation rail + content (large tablets, desktop) Row { NavigationRail { /* ... */ } Content(Modifier.weight(1f)) } } } ``` **Do:** Use `fillMaxWidth()`, `weight()`, and `WindowSizeClass` for responsive layouts. **Don't:** Use fixed pixel/dp widths for containers that should adapt. ### Forgetting Scroll Behavior ```kotlin // Anti-pattern: content overflows without scrolling Column(Modifier.fillMaxSize()) { // 20 items that exceed screen height -- bottom items invisible repeat(20) { Text("Item $it") } } // Correct: add scrolling Column( Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) ) { repeat(20) { Text("Item $it") } } // Or for dynamic lists: LazyColumn(Modifier.fillMaxSize()) { items(20) { Text("Item $it") } } ``` ### Ignoring Content Padding from Scaffold ```kotlin // Anti-pattern: ignoring innerPadding from Scaffold Scaffold(topBar = { TopAppBar(title = { Text("App") }) }) { innerPadding -> // Content renders BEHIND the top bar LazyColumn { items(data) { Text(it) } } } // Correct: apply innerPadding Scaffold(topBar = { TopAppBar(title = { Text("App") }) }) { innerPadding -> LazyColumn( modifier = Modifier.padding(innerPadding), // or contentPadding = innerPadding ) { items(data) { Text(it) } } } ``` ================================================ FILE: .claude/skills/compose-expert/references/lists-scrolling.md ================================================ # Lists and Scrolling in Jetpack Compose Efficient list rendering and scrolling are core to responsive mobile UIs. Jetpack Compose provides lazy layouts that compose items on-demand, not all at once. ## LazyColumn and LazyRow These composables only compose visible items, making them efficient for large lists unlike `Column`/`Row` which compose all children upfront. ### LazyColumn (Vertical Scrolling) ```kotlin LazyColumn(modifier = Modifier.fillMaxSize()) { item { HeaderComposable() } items(itemList.size) { index -> ListItemComposable(itemList[index]) } item { FooterComposable() } } ``` ### LazyRow (Horizontal Scrolling) ```kotlin LazyRow(modifier = Modifier.fillMaxWidth()) { items(imageList.size) { index -> Image( painter = painterResource(imageList[index]), contentDescription = null, modifier = Modifier.width(200.dp) ) } } ``` **Key difference from Column/Row:** Items are composed lazily as they enter the viewport, reducing memory and CPU usage. **Source:** `androidx/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/` ## DSL Patterns: item, items, itemsIndexed ### `item` — Single Composable ```kotlin LazyColumn { item { HeaderComposable() } } ``` ### `items` — From a List or Count ```kotlin // From a List val users = listOf(User("Alice"), User("Bob")) LazyColumn { items(users) { user -> UserCard(user) } } // From a count LazyColumn { items(100) { index -> Text("Item $index") } } ``` ### `itemsIndexed` — With Index ```kotlin LazyColumn { itemsIndexed(users) { index, user -> Text("${index + 1}. ${user.name}") } } ``` ## Keys: Critical for Correctness and Performance The `key` parameter ensures Compose can correctly identify and reuse items even if the list is reordered. ### ✓ Good: Stable Keys ```kotlin data class User(val id: Long, val name: String) LazyColumn { items(users, key = { it.id }) { user -> UserCard(user) } } ``` When `users` list is reordered, Compose knows which item moved because the key (id) is stable. ### ✗ Bad: Index as Key ```kotlin // AVOID: If list is reordered, state gets mixed up LazyColumn { items(users, key = { index }) { user -> // Wrong! var selected by remember { mutableStateOf(false) } UserCard(user, selected) } } ``` If you remove item at index 0, the item that was at index 1 moves to index 0 and incorrectly inherits the state. ### ✗ Bad: No Key ```kotlin // If list changes, item state/animations may misbehave LazyColumn { items(users) { user -> UserCard(user) } } ``` Without a key, Compose can't distinguish items reliably if the list changes. **Rule:** Always provide a stable, unique key when the list can change. Use IDs, not indices. ## Content Types for Recycling Optimization Use `contentType` to enable layout reuse when rendering different item types: ```kotlin sealed class ListItem data class HeaderItem(val title: String) : ListItem() data class UserItem(val user: User) : ListItem() LazyColumn { items( items = listItems, key = { it.id }, contentType = { when (it) { is HeaderItem -> "header" is UserItem -> "user" }} ) { item -> when (item) { is HeaderItem -> HeaderComposable(item) is UserItem -> UserCard(item) } } } ``` Items with the same `contentType` can reuse layout state, improving performance when types repeat. ## LazyListState: Scroll Position and Animations Manage scroll position programmatically: ```kotlin val listState = rememberLazyListState() LazyColumn(state = listState) { items(100) { index -> Text("Item $index") } } // Scroll to item 50 LaunchedEffect(Unit) { listState.scrollToItem(50) } // Animate scroll LaunchedEffect(Unit) { listState.animateScrollToItem(50) } // Read current scroll position val firstVisibleIndex = listState.firstVisibleItemIndex val firstVisibleOffset = listState.firstVisibleItemScrollOffset ``` **Use case:** Scroll to a newly added item, or scroll on user action. ## LazyVerticalGrid and LazyHorizontalGrid ### Fixed Columns ```kotlin LazyVerticalGrid(columns = GridCells.Fixed(3)) { items(itemList.size) { index -> GridItemComposable(itemList[index]) } } ``` ### Adaptive Columns (Responsive) ```kotlin // Column width ~100dp, fills available space with as many columns as fit LazyVerticalGrid(columns = GridCells.Adaptive(100.dp)) { items(itemList.size) { index -> GridItemComposable(itemList[index]) } } ``` Adaptive is preferable for responsive layouts. ## LazyVerticalStaggeredGrid For Pinterest-style layouts with variable heights: ```kotlin LazyVerticalStaggeredGrid( columns = StaggeredGridCells.Fixed(2), modifier = Modifier.fillMaxSize() ) { items(images.size) { index -> AsyncImage( model = images[index].url, contentDescription = null, modifier = Modifier.fillMaxWidth() ) } } ``` Items flow into the column with the shortest current height, creating a natural staggered appearance. ## HorizontalPager and VerticalPager Page-by-page horizontal or vertical swiping: ```kotlin val pagerState = rememberPagerState(pageCount = { pages.size }) HorizontalPager(state = pagerState) { page -> PageComposable(pages[page]) } // Programmatic scroll to page LaunchedEffect(Unit) { pagerState.scrollToPage(2) } // Animate to page LaunchedEffect(Unit) { pagerState.animateScrollToPage(2) } ``` ## Sticky Headers in Lazy Lists Headers that remain visible at the top while scrolling: ```kotlin LazyColumn { stickyHeader { SectionHeaderComposable("Section A") } items(itemsA) { item -> ItemComposable(item) } stickyHeader { SectionHeaderComposable("Section B") } items(itemsB) { item -> ItemComposable(item) } } ``` ## Nested Scrolling: Pitfalls ### ✗ Avoid Scrollable Inside LazyColumn ```kotlin // Bad: nested scroll behavior is unpredictable LazyColumn { item { LazyRow { // Nested lazy is OK, but... items(innerList) { item -> InnerItem(item) } } } } ``` Nested lazy composables are acceptable but require careful thought about scroll precedence. ### ✗ Avoid verticalScroll Modifier Inside LazyColumn ```kotlin // Bad: two scroll containers fight for input LazyColumn { item { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { Text("This is scrollable twice!") } } } ``` Don't wrap lazy children in scrollable modifiers; use nested lazy composables if you need multiple scroll axes. ### ✓ Use nestedScroll for Complex Scenarios ```kotlin val scrollState = rememberScrollState() val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { // Custom scroll handling return Offset.Zero } } } LazyColumn(modifier = Modifier.nestedScroll(nestedScrollConnection)) { items(100) { index -> Text("Item $index") } } ``` ## Performance: Scroll-Dependent UI ### ✗ Bad: Heavy Computation in Item Lambda ```kotlin LazyColumn { items(users) { user -> val processedData = expensiveComputation(user) // Runs every recomposition! UserCard(user, processedData) } } ``` ### ✓ Good: Use derivedStateOf for Scroll-Dependent Logic ```kotlin val listState = rememberLazyListState() val showScrollToTop = remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } LazyColumn(state = listState) { items(100) { index -> Text("Item $index") } } if (showScrollToTop.value) { Button(onClick = { /* scroll up */ }) { Text("Top") } } ``` `derivedStateOf` derives a new value when scroll state changes without recomposing the entire list. ## Anti-Patterns ### ✗ Using LazyColumn for Small Fixed Lists ```kotlin // Bad: overkill for 5 items LazyColumn { items(5) { index -> Text("Item $index") } } ``` **Better:** Use `Column` for small fixed lists. ### ✗ No Keys + List Mutations ```kotlin var items by remember { mutableStateOf(initialList) } LazyColumn { items(items) { item -> // No key! ItemComposable(item, onDelete = { items = items.filter { it.id != item.id } }) } } ``` Without keys, removing an item corrupts the state of remaining items. ### ✗ Creating New Objects in Keys ```kotlin // Bad: key creates new object each recomposition LazyColumn { items(users, key = { User(it.id, it.name) }) { user -> UserCard(user) } } ``` **Better:** Use primitive stable identifiers. ## Key Takeaways 1. Always provide stable, unique keys when using `items` on mutable lists 2. Use `contentType` for multi-type lists to enable layout reuse 3. Prefer `GridCells.Adaptive` for responsive grid layouts 4. Avoid nested scrollables; use `nestedScroll` for complex scroll behavior 5. Use `derivedStateOf` to avoid recomposing the entire list for scroll-dependent logic 6. `LazyColumn`/`LazyRow` are for large or unbounded lists; use `Column`/`Row` for small fixed lists 7. Never use indices as keys; list mutations will corrupt item state ### Production Crash Patterns #### indexOf() Inside items {} — O(n^2) and Crashes ```kotlin // BAD: O(n^2) total, returns -1 on recreated objects → IndexOutOfBoundsException items(list.size) { index -> val item = list[index] val position = list.indexOf(item) // -1 if object was recreated! } // GOOD: use items() with key for stable identity items(list, key = { it.id }) { item -> ItemRow(item) } ``` Root cause: `indexOf()` uses `equals()`. If list items are recreated (new object instances without proper `equals()` implementation), `indexOf()` returns -1. #### Duplicate LazyColumn Keys Backend sends items without unique IDs, or WebSocket reconnects deliver duplicates → `IllegalArgumentException: Key X was already used`. ```kotlin // BAD: backend IDs may not be unique items(messages, key = { it.id }) { msg -> ... } // GOOD: add dedup index for safety items(messages, key = { "${it.id}_${it.dedupIndex}" }) { msg -> ... } ``` The `dedupIndex` pattern: Add a field to the data class that is excluded from `equals()`/`hashCode()` but included in the LazyColumn key: ```kotlin data class ChatMessage( val id: String, val text: String, val timestamp: Long ) { // NOT in data class constructor — excluded from equals/hashCode var dedupIndex: Int = 0 } // When processing messages from backend: fun deduplicateKeys(messages: List): List { val seen = mutableMapOf() return messages.map { msg -> val count = seen.getOrPut(msg.id) { 0 } seen[msg.id] = count + 1 msg.also { it.dedupIndex = count } } } ``` #### derivedStateOf Driving Collection Size → IOOB ```kotlin // BAD: derived count can be stale when items{} reads val itemCount by remember { derivedStateOf { filterItems(allItems).size } } LazyColumn { items(itemCount) { index -> val item = filterItems(allItems)[index] // IOOB if allItems changed! } } // GOOD: derive the full filtered list, not just the count val filteredItems by remember { derivedStateOf { filterItems(allItems) } } LazyColumn { items(filteredItems, key = { it.id }) { item -> ItemRow(item) } } ``` Rule: `derivedStateOf` for scroll direction, visibility, form validation — **never for item counts that drive LazyList rendering**. ### LazyList Hardening #### Multi-Field Keys with Collision Prefixes When a LazyList mixes items from different data sources, IDs can collide: ```kotlin // BAD: id=42 in both liveItems and archivedItems → crash LazyColumn { items(liveItems, key = { it.id }) { ... } items(archivedItems, key = { it.id }) { ... } } // GOOD: prefix keys by source LazyColumn { items(liveItems, key = { "live_${it.id}" }) { ... } items(archivedItems, key = { "archived_${it.id}" }) { ... } items(pinnedItems, key = { "pinned_${it.id}" }) { ... } } ``` #### items() with key Preferred Over itemsIndexed() `items(list, key = { it.id })` gives each item a stable identity across recompositions. This enables: - Correct `animateItem()` animations - Efficient diffing (only changed items recompose) - Proper state preservation per item Use `itemsIndexed()` only when you genuinely need the index for display (e.g., numbered list). #### animateItem() Parameters ```kotlin items(items, key = { it.id }) { item -> ItemRow( item = item, modifier = Modifier.animateItem( fadeInSpec = tween(durationMillis = 250), placementSpec = spring( dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessMediumLow ), fadeOutSpec = tween(durationMillis = 150) ) ) } ``` #### ReportDrawnWhen for Startup Performance Signal to the system that the first meaningful content is visible: ```kotlin @Composable fun ConversationListScreen(items: List) { ReportDrawnWhen { items.isNotEmpty() } LazyColumn { items(items, key = { it.id }) { item -> ConversationRow(item) } } } ``` This improves Time To Initial Display (TTID) and Time To Full Display (TTFD) metrics in Android vitals. #### Device-Specific Pagination Some devices (notably Samsung) handle scroll events differently, requiring fewer scroll confirmations for lazy load triggers. When implementing infinite scroll, test on multiple OEMs and consider a configurable scroll threshold. ```kotlin val shouldLoadMore by remember { derivedStateOf { val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 val totalItems = listState.layoutInfo.totalItemsCount lastVisibleItem >= totalItems - PREFETCH_THRESHOLD // e.g., 5 } } LaunchedEffect(shouldLoadMore) { if (shouldLoadMore) { viewModel.loadNextPage() } } ``` ================================================ FILE: .claude/skills/compose-expert/references/material3-motion.md ================================================ # Material 3 Motion Source: `compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/tokens/MotionTokens.kt` and `compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MotionScheme.kt` in `androidx/androidx` (branch: `androidx-main`) CMP compatibility: `MotionTokens`, `MotionScheme`, and all easing constants are in `androidx.compose.material3` — available on all CMP targets (Android, Desktop, iOS, Web) since M3 1.2.0. No platform guards needed. --- ## 1. Two APIs, One System M3 provides two ways to apply motion: | API | When to Use | |-----|------------| | **`MotionScheme`** (preferred) | Inside components that should adapt to the app's motion scheme — the theme controls whether spring-based or tween-based specs are used | | **`MotionTokens` + `tween()`** | When you need explicit `tween()` / `keyframes {}` control and the component is not theme-motion-aware | Use `MotionScheme` for new components. Use `MotionTokens` when the caller explicitly provides `AnimationSpec` parameters or when working with `AnimatedVisibility`, `Crossfade`, or shared elements. --- ## 2. MotionScheme API (Preferred for Components) `MotionScheme` is part of `MaterialTheme` alongside `colorScheme`, `typography`, and `shapes`. ```kotlin // Access via MaterialTheme val motionScheme = MaterialTheme.motionScheme // Two built-in schemes MaterialTheme(motionScheme = MotionScheme.standard()) // utilitarian UI MaterialTheme(motionScheme = MotionScheme.expressive()) // prominent UI (M3 recommended default) ``` ### Spec Functions | Function | Use Case | |----------|---------| | `defaultSpatialSpec()` | Layout changes, position/size transitions (spatial) | | `fastSpatialSpec()` | Quick spatial transitions | | `slowSpatialSpec()` | Deliberate spatial transitions | | `defaultEffectsSpec()` | Opacity, color, non-spatial changes | | `fastEffectsSpec()` | Quick opacity/color transitions | | `slowEffectsSpec()` | Deliberate opacity/color transitions | ```kotlin @Composable fun AnimatedCard(expanded: Boolean) { val motionScheme = MaterialTheme.motionScheme // Size change = spatial val size by animateDpAsState( targetValue = if (expanded) 200.dp else 100.dp, animationSpec = motionScheme.defaultSpatialSpec(), label = "card-size" ) // Color change = effects val color by animateColorAsState( targetValue = if (expanded) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surface, animationSpec = motionScheme.defaultEffectsSpec(), label = "card-color" ) } ``` > **Key difference from `tween()`**: `MotionScheme` specs are spring-based by default in > `expressive()`. The actual spec type (spring vs. tween) is controlled by the theme, not > the component. This means motion adapts to the app's motion scheme without code changes. --- ## 3. Duration Tokens Use when explicit `tween()` control is needed. All values sourced from `MotionTokens.kt` (generated from Material Design spec v0_103). Durations are `Float` — use `.toInt()` for `tween(durationMillis = ...)`. | Token | Value (ms) | Use Case | |-------|-----------|---------| | `MotionTokens.DurationShort1` | 50ms | Micro interactions — ripple spread, checkbox tick | | `MotionTokens.DurationShort2` | 100ms | Small element appear/disappear | | `MotionTokens.DurationShort3` | 150ms | Icon transitions, selection indicators | | `MotionTokens.DurationShort4` | 200ms | Tooltip appear, chip selection | | `MotionTokens.DurationMedium1` | 250ms | FAB expand, card state change | | `MotionTokens.DurationMedium2` | 300ms | **Most common** — dialog, bottom sheet, nav drawer | | `MotionTokens.DurationMedium3` | 350ms | Expanded component transitions | | `MotionTokens.DurationMedium4` | 400ms | Page-level panel transitions | | `MotionTokens.DurationLong1` | 450ms | Complex layout changes | | `MotionTokens.DurationLong2` | 500ms | Shared element enter | | `MotionTokens.DurationLong3` | 550ms | Shared element — large content | | `MotionTokens.DurationLong4` | 600ms | Full container morphs | | `MotionTokens.DurationExtraLong1` | 700ms | Full-screen transitions only | | `MotionTokens.DurationExtraLong2` | 800ms | Full-screen transitions only | | `MotionTokens.DurationExtraLong3` | 900ms | Full-screen transitions only | | `MotionTokens.DurationExtraLong4` | 1000ms | Full-screen transitions only | --- ## 4. Easing Tokens All values sourced from `MotionTokens.kt`. Access via `MotionTokens.Easing*CubicBezier`. | Token | CubicBezierEasing(x1, y1, x2, y2) | Direction | Use Case | |-------|------------------------------------|-----------|---------| | `MotionTokens.EasingEmphasizedDecelerateCubicBezier` | `(0.05f, 0.7f, 0.1f, 1.0f)` | Entering | Element arriving on screen — fast start, gentle settle | | `MotionTokens.EasingEmphasizedAccelerateCubicBezier` | `(0.3f, 0.0f, 0.8f, 0.15f)` | Exiting | Element leaving screen — slow start, fast exit | | `MotionTokens.EasingEmphasizedCubicBezier` | `(0.2f, 0.0f, 0.0f, 1.0f)` | Both | Default for most M3 component transitions | | `MotionTokens.EasingStandardDecelerateCubicBezier` | `(0.0f, 0.0f, 0.0f, 1.0f)` | Entering | Simple enter — less expressive than Emphasized | | `MotionTokens.EasingStandardAccelerateCubicBezier` | `(0.3f, 0.0f, 1.0f, 1.0f)` | Exiting | Simple exit | | `MotionTokens.EasingStandardCubicBezier` | `(0.2f, 0.0f, 0.0f, 1.0f)` | Both | Simple state changes | | `MotionTokens.EasingLinearCubicBezier` | `(0.0f, 0.0f, 1.0f, 1.0f)` | — | Looping / repeating animations only | | `MotionTokens.EasingLegacyCubicBezier` | `(0.4f, 0.0f, 0.2f, 1.0f)` | — | `FastOutSlowInEasing` equivalent — do not use in new code | | `MotionTokens.EasingLegacyAccelerateCubicBezier` | `(0.4f, 0.0f, 1.0f, 1.0f)` | — | `FastOutLinearInEasing` equivalent — do not use in new code | | `MotionTokens.EasingLegacyDecelerateCubicBezier` | `(0.0f, 0.0f, 0.2f, 1.0f)` | — | `LinearOutSlowInEasing` equivalent — do not use in new code | > **Enter/exit rule (always):** Enter = Decelerate easing (fast start, gentle settle). > Exit = Accelerate easing (slow start, quick departure). Never use the same easing for both. > The `Legacy*` tokens are equivalent to the pre-M3 named constants — do not use them in new code. --- ## 5. Using Tokens in Compose Animation APIs ### animate*AsState (prefer MotionScheme) ```kotlin // Color — effects spec val color by animateColorAsState( targetValue = if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface, animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), label = "selection-color" ) // If explicit tween is required: val color by animateColorAsState( targetValue = targetColor, animationSpec = tween( durationMillis = MotionTokens.DurationShort4.toInt(), // 200ms easing = MotionTokens.EasingStandardCubicBezier // state change, not enter/exit ), label = "color" ) ``` ### AnimatedVisibility (asymmetric enter/exit) Enter and exit must use different durations and easing — exit is always faster. ```kotlin AnimatedVisibility( visible = visible, enter = fadeIn( animationSpec = tween( durationMillis = MotionTokens.DurationMedium2.toInt(), // 300ms easing = MotionTokens.EasingEmphasizedDecelerateCubicBezier // entering ) ) + slideInVertically( animationSpec = tween( durationMillis = MotionTokens.DurationMedium2.toInt(), easing = MotionTokens.EasingEmphasizedDecelerateCubicBezier ), initialOffsetY = { it / 4 } ), exit = fadeOut( animationSpec = tween( durationMillis = MotionTokens.DurationShort4.toInt(), // 200ms — exit is faster easing = MotionTokens.EasingEmphasizedAccelerateCubicBezier // exiting ) ) + slideOutVertically( animationSpec = tween( durationMillis = MotionTokens.DurationShort4.toInt(), easing = MotionTokens.EasingEmphasizedAccelerateCubicBezier ), targetOffsetY = { it / 4 } ) ) { content() } ``` ### updateTransition (multi-property, shared spec) ```kotlin val transition = updateTransition(targetState = expanded, label = "card") val elevation by transition.animateDp( transitionSpec = { tween( durationMillis = MotionTokens.DurationMedium1.toInt(), // 250ms easing = MotionTokens.EasingEmphasizedCubicBezier ) }, label = "elevation" ) { isExpanded -> if (isExpanded) 8.dp else 0.dp } val cornerRadius by transition.animateDp( transitionSpec = { tween( durationMillis = MotionTokens.DurationMedium1.toInt(), easing = MotionTokens.EasingEmphasizedCubicBezier ) }, label = "corner-radius" ) { isExpanded -> if (isExpanded) 0.dp else 12.dp } ``` ### Shared element transitions Shared elements cross screen boundaries — use Long range. ```kotlin Modifier.sharedElement( state = rememberSharedContentState(key = "hero-${item.id}"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> tween( durationMillis = MotionTokens.DurationLong2.toInt(), // 500ms easing = MotionTokens.EasingEmphasizedCubicBezier ) } ) ``` --- ## 6. Decision Tree Pick the right duration by working through these questions in order: 1. **Micro interaction?** (ripple, checkbox tick, toggle thumb snap) → `DurationShort1` (50ms) or `DurationShort2` (100ms) 2. **Component state change?** (button press feedback, chip select, icon swap, tab indicator) → `DurationShort3` (150ms) or `DurationShort4` (200ms) 3. **Container change?** (card expand, FAB extend/shrink, menu open, tooltip) → `DurationMedium1` (250ms) or `DurationMedium2` (300ms) ← most common 4. **Screen-level element?** (dialog enter, bottom sheet slide, search bar expand, nav drawer) → `DurationMedium3` (350ms) or `DurationMedium4` (400ms) 5. **Shared element / hero transition?** (image or card expands from list to detail screen) → `DurationLong1` (450ms) or `DurationLong2` (500ms) 6. **Full-screen complex morph?** (entire screen layout changes) → `DurationLong3`–`DurationExtraLong1` (550ms–700ms) **Easing rule (always apply):** - Element arriving → `EasingEmphasizedDecelerateCubicBezier` - Element departing → `EasingEmphasizedAccelerateCubicBezier` - Element changing state (stays on screen) → `EasingEmphasizedCubicBezier` - Looping/infinite → `EasingLinearCubicBezier` - Prefer `MotionScheme` specs over manual easing for theme-aware components --- ## 7. Review Flags Patterns to catch in code review. See also `references/pr-review.md` Category 3. | Pattern in Code | Flag | Fix | |----------------|------|-----| | `tween(50)` | Hardcoded duration | `MotionTokens.DurationShort1.toInt()` | | `tween(100)` | Hardcoded duration | `MotionTokens.DurationShort2.toInt()` | | `tween(150)` | Hardcoded duration | `MotionTokens.DurationShort3.toInt()` | | `tween(200)` | Hardcoded duration | `MotionTokens.DurationShort4.toInt()` | | `tween(250)` | Hardcoded duration | `MotionTokens.DurationMedium1.toInt()` | | `tween(300)` | Hardcoded duration | `MotionTokens.DurationMedium2.toInt()` | | `tween(350)` | Hardcoded duration | `MotionTokens.DurationMedium3.toInt()` | | `tween(400)` | Hardcoded duration | `MotionTokens.DurationMedium4.toInt()` | | `tween(N)` with any integer literal | Hardcoded duration | Nearest `MotionTokens.Duration*` token | | `FastOutSlowInEasing` | Pre-M3 easing | `MotionTokens.EasingEmphasizedCubicBezier` | | `LinearOutSlowInEasing` | Pre-M3 easing | `MotionTokens.EasingEmphasizedDecelerateCubicBezier` | | `FastOutLinearInEasing` | Pre-M3 easing | `MotionTokens.EasingEmphasizedAccelerateCubicBezier` | | `animateColorAsState(target)` no `animationSpec` | Missing spec | `MaterialTheme.motionScheme.defaultEffectsSpec()` | | Same easing on both `enter` and `exit` | Wrong pairing | Decelerate for enter, Accelerate for exit | | Duration > 600ms on non-shared-element | Too slow | Reduce to `DurationLong1`–`DurationLong2` | | New component uses explicit `tween()` instead of `MotionScheme` | Not theme-aware | Use `MaterialTheme.motionScheme.defaultSpatialSpec()` / `defaultEffectsSpec()` | ================================================ FILE: .claude/skills/compose-expert/references/modifiers.md ================================================ # Jetpack Compose Modifiers Reference Modifiers are the primary way to decorate or augment a composable. They apply layout, drawing, gesture, and accessibility behavior. Understanding modifier ordering and the available APIs is critical for correctness and performance. ## Modifier Chain Ordering Order matters. Modifiers are applied left-to-right in the DSL, but conceptually they wrap bottom-to-top. Each modifier receives a lambda that draws/measures the content below it. ```kotlin // Example: different results depending on order Box( Modifier .background(Color.Red) .padding(16.dp) .size(100.dp) ) // Red background wraps the padded content, which wraps the 100x100 box Box( Modifier .size(100.dp) .padding(16.dp) .background(Color.Red) ) // 100x100 box is padded, then the whole thing (132x132) gets red background ``` **Do:** Order modifiers from outer (layout/sizing) to inner (styling/interaction). **Don't:** Put `size` after `padding` if you want the padding included in the final size. Source: `compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt` ## Common Modifier Patterns ### Padding and Sizing ```kotlin // Padding: external spacing around content Box(Modifier.padding(16.dp)) { } // Size: exact dimensions (overrides requested size from parent) Box(Modifier.size(100.dp)) { } Box(Modifier.size(width = 200.dp, height = 100.dp)) { } // FillMaxWidth/FillMaxHeight: expand to available space Box(Modifier.fillMaxWidth(0.8f)) { } // 80% of parent width Box(Modifier.fillMaxSize()) { } // 100% of parent // Do: use fillMaxWidth before adding padding for alignment clarity Column(Modifier.fillMaxWidth()) { Box(Modifier.padding(16.dp).fillMaxWidth()) { } } // Don't: apply fillMaxWidth after background if you want background to expand // Instead: Box(Modifier.fillMaxWidth().background(Color.Blue)) { } ``` ### Background and Border ```kotlin // Background applies a color to the surface Box(Modifier.background(Color.Blue)) { } Box(Modifier.background(Color.Blue, shape = RoundedCornerShape(8.dp))) { } // Border draws a stroke (order matters!) Box( Modifier .size(100.dp) .border(2.dp, Color.Black, RoundedCornerShape(8.dp)) .background(Color.White) ) // The border is drawn AFTER background in visual order (because modifiers below it are drawn first) // Combine background + border: apply border first in chain Box( Modifier .border(2.dp, Color.Black, RoundedCornerShape(8.dp)) .background(Color.White) ) ``` ### Clipping ```kotlin // Clip content to a shape Box(Modifier.clip(RoundedCornerShape(8.dp))) { Image(painter = painterResource(id = R.drawable.my_image), contentDescription = "") } // Do: apply clip before background if you want background inside the shape Box( Modifier .clip(RoundedCornerShape(8.dp)) .background(Color.Blue) ) { } // Don't: apply background then clip (works but semantically wrong) Box( Modifier .background(Color.Blue) .clip(RoundedCornerShape(8.dp)) ) { } ``` ## Clickable and Combined Clickable ```kotlin // Basic click handling with ripple effect (Material 3 default) Button(onClick = { }) { Text("Click me") } // Manual clickable with ripple Box( Modifier .size(100.dp) .clickable( indication = ripple(), // Material ripple feedback interactionSource = remember { MutableInteractionSource() } ) { /* handle click */ } ) // Combined clickable: long press + double click + click Box( Modifier .combinedClickable( onClick = { }, onLongClick = { }, onDoubleClick = { }, indication = ripple() ) ) { } // Do: provide explicit interactionSource for testing/state observation val interactionSource = remember { MutableInteractionSource() } Box( Modifier.clickable( interactionSource = interactionSource, indication = ripple() ) { } ) // Don't: forget indication parameter (will have no visual feedback) Box(Modifier.clickable { }) { } // No ripple ``` ## Modifier.composed vs Modifier.Node The old API (`composed`) is being phased out in favor of the new `ModifierNodeElement` API. Both work, but new code should use the latter. ### Old API: Modifier.composed ```kotlin fun Modifier.myCustomModifier(value: String) = composed { val state = remember { mutableStateOf(value) } this.then( Modifier .background(Color.Blue) .clickable { state.value = "updated" } ) } ``` - Creates a new composable scope - Captures composition locals - Causes recomposition when remember state changes - Deprecated but still supported ### New API: Modifier.Node ```kotlin class MyCustomNode(val value: String) : Modifier.Node { override fun onDetach() { // Cleanup when removed } } data class MyCustomElement(val value: String) : ModifierNodeElement() { override fun create() = MyCustomNode(value) override fun update(node: MyCustomNode) { node.value = value } } fun Modifier.myCustomModifier(value: String) = this.then(MyCustomElement(value)) ``` **Do:** Use `Modifier.Node` for new custom modifiers. It's more efficient and doesn't create composition scopes. **Don't:** Create new `composed` modifiers; migrate existing ones to `Modifier.Node`. Source: `compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/modifier/ModifierNodeElement.kt` ## Layout vs Drawing vs Pointer Input Modifiers Modifiers fall into categories that affect when they execute: ```kotlin // Layout modifier: affects measurement and layout pass fun Modifier.customSize(width: Dp, height: Dp) = this.then(object : LayoutModifier { override fun MeasureScope.measure(measurable: Measurable, constraints: Constraints) = measurable.measure(Constraints.fixed(width.roundToPx(), height.roundToPx())) .run { layout(width = size.width, height = size.height) { place(0, 0) } } }) // Drawing modifier: doesn't affect layout, just draws after content fun Modifier.customDraw() = drawBehind { drawCircle(Color.Red) } // Pointer input modifier: handles gestures/events fun Modifier.detectCustomGesture() = pointerInput(Unit) { detectTapGestures { offset -> /* handle */ } } ``` **Do:** Use layout modifiers for sizing/positioning, drawing modifiers for visual effects, pointer modifiers for input. **Don't:** Use layout modifiers to create visual effects; use drawing modifiers instead. ## Modifier.graphicsLayer — Performance Implications `graphicsLayer` applies transformations at the graphics rendering level. It's more efficient than recomposing for animations. ```kotlin // Efficient: transforms applied on the graphics layer, no recomposition Box( Modifier.graphicsLayer( scaleX = 1.2f, scaleY = 1.2f, translationX = 10f, rotationZ = 45f, alpha = 0.8f ) ) { } // Less efficient: recomposes every frame var scaleX by remember { mutableStateOf(1f) } LaunchedEffect(Unit) { while (true) { scaleX = 1.2f delay(16) } } Box(Modifier.scale(scaleX)) { } ``` **Do:** Use `graphicsLayer` for animations and frequent property changes. **Don't:** Animate state values that trigger recomposition when `graphicsLayer` would suffice. Source: `compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/graphics/GraphicsLayerModifier.kt` ## Modifier.semantics — Accessibility Semantics describe the meaning of UI elements for screen readers and accessibility tests. ```kotlin // Add semantic label Button(onClick = { }) { Icon(Icons.Default.Add, contentDescription = null) Text("Add item") } // Custom semantic properties Box( Modifier .size(100.dp) .semantics { contentDescription = "Custom box" onClick(label = "Activate") { true } } ) { } // Do: always provide contentDescription for images Image( painter = painterResource(id = R.drawable.icon), contentDescription = "User avatar" ) // Don't: forget contentDescription (screen readers won't announce it) Image(painter = painterResource(id = R.drawable.icon), contentDescription = null) // Wrong ``` Source: `compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/Semantics.kt` ## Modifier.testTag — UI Testing ```kotlin // Add a test tag for finding composables in tests Box(Modifier.testTag("my_box")) { } // In tests: composeTestRule.onNodeWithTag("my_box").performClick() composeTestRule.onNodeWithTag("my_box").assertIsDisplayed() ``` **Do:** Use unique, descriptive test tags. **Don't:** Use test tags in production code for business logic. ## Review Checklist: Modifier Ordering Bugs ### Hardcoded Size After Caller's `modifier` Parameter When a composable accepts `modifier: Modifier = Modifier` and chains fixed `.height()` / `.width()` / `.size()` after it, caller size constraints are silently ignored or clamped. ```kotlin // BAD: caller's height is outer constraint, component's 172.dp is inner — component always renders at 172dp @Composable fun BannerCard( modifier: Modifier = Modifier, ) { Box( modifier = modifier // caller constraints applied first (outer) .fillMaxWidth() .height(172.dp) // inner — wins when smaller, clamped when larger .clip(RoundedCornerShape(18.dp)) .background(Color.Green.copy(alpha = 0.08f)), ) } // Caller expects 200dp but gets 172dp: BannerCard(modifier = Modifier.height(200.dp)) // Caller expects 100dp — component gets clamped/squished: BannerCard(modifier = Modifier.height(100.dp)) ``` **Why it happens:** Modifier chain resolves outer-to-inner (left-to-right). Outer constraint sets max bounds, inner constraint requests within those bounds. First size constraint wins as the ceiling. **Fix option 1:** Component defaults first, caller can override via `.then(modifier)`: ```kotlin // GOOD: component sets defaults, caller's modifier applied last and can override @Composable fun BannerCard( modifier: Modifier = Modifier, ) { Box( modifier = Modifier .fillMaxWidth() .height(172.dp) .clip(RoundedCornerShape(18.dp)) .background(Color.Green.copy(alpha = 0.08f)) .then(modifier), // caller can override size ) } ``` **Fix option 2:** Use `defaultMinSize` for flexible sizing: ```kotlin // GOOD: minimum guaranteed, caller can make it larger @Composable fun BannerCard( modifier: Modifier = Modifier, ) { Box( modifier = modifier .fillMaxWidth() .defaultMinSize(minHeight = 172.dp) // floor, not ceiling .clip(RoundedCornerShape(18.dp)) .background(Color.Green.copy(alpha = 0.08f)), ) } ``` **Rule:** When a composable accepts `modifier: Modifier = Modifier`, never chain fixed `.height()` / `.width()` / `.size()` after the caller's modifier — caller constraints become outer bounds and the component's fixed size either ignores or gets clamped by them. Use `.then(modifier)` at the end or `defaultMinSize` for flexible sizing. Flag in every PR review. --- ## Anti-patterns ### Creating Modifiers in Composition ```kotlin // Don't: creates a new Modifier every recomposition @Composable fun BadModifier() { Box(Modifier.padding(16.dp).background(Color.Blue)) { } } // Do: extract to a variable or parameter @Composable fun GoodModifier(modifier: Modifier = Modifier) { Box(modifier.padding(16.dp).background(Color.Blue)) { } } ``` ### Conditional Modifier Chains Done Wrong ```kotlin // Don't: breaks type checking and readability val mod = if (isSelected) Modifier.background(Color.Blue) else Modifier Box(mod.padding(16.dp)) { } // Do: use then() for conditional chaining Box( Modifier .padding(16.dp) .then(if (isSelected) Modifier.background(Color.Blue) else Modifier) ) { } ``` --- **Summary:** Master modifier ordering, prefer `Modifier.Node` over `composed`, use `graphicsLayer` for animations, and always consider the semantic layer for accessibility. ================================================ FILE: .claude/skills/compose-expert/references/multiplatform.md ================================================ # Compose Multiplatform (CMP) Reference Reference: `compose-multiplatform` (JetBrains), `androidx.compose` (Google) ## CMP Architecture Overview Compose Multiplatform uses a three-layer architecture: ### Layer 1: commonMain (Shared UI Runtime) All Compose runtime, foundation, material3, and navigation APIs live here. Rendering uses Skia (via Skiko) on non-Android platforms, and the native Android Compose renderer on Android. ```kotlin // commonMain/kotlin/App.kt @Composable fun App() { MaterialTheme { var count by remember { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Clicked $count times") } } } ``` This single composable renders natively on Android, Desktop (JVM), iOS, and WebAssembly. ### Layer 2: Platform Source Sets Platform-specific code lives in `androidMain`, `desktopMain`, `iosMain`, `wasmJsMain`. Use `expect`/`actual` to bridge. ``` src/ commonMain/kotlin/ # Shared UI + logic androidMain/kotlin/ # Android-specific (AndroidView, Context) desktopMain/kotlin/ # Desktop-specific (Window, MenuBar) iosMain/kotlin/ # iOS-specific (UIKitView, NSBundle) wasmJsMain/kotlin/ # Web-specific (ComposeViewport) ``` ### Layer 3: Platform Entry Points Each platform has a different entry point to host the shared `App()` composable. ```kotlin // androidMain — Standard Android Activity class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { App() } } } // desktopMain — JVM window fun main() = application { Window( onCloseRequest = ::exitApplication, title = "My App" ) { App() } } // iosMain — UIKit integration fun MainViewController(): UIViewController = ComposeUIViewController { App() } // wasmJsMain — Browser canvas @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(document.body!!) { App() } } ``` --- ## API Availability Matrix | API | commonMain | Android | Desktop | iOS | Web | |-----|:----------:|:-------:|:-------:|:---:|:---:| | **Runtime** (`@Composable`, `remember`, `mutableStateOf`, `LaunchedEffect`, `derivedStateOf`) | Yes | Yes | Yes | Yes | Yes | | **Foundation** (`Box`, `Column`, `Row`, `LazyColumn`, `TextField`, `Canvas`) | Yes | Yes | Yes | Yes | Yes | | **Material3** (`Button`, `Card`, `Scaffold`, `TopAppBar`, `NavigationBar`) | Yes | Yes | Yes | Yes | Yes | | **Navigation Compose** (type-safe `@Serializable` routes, `NavHost`) | Yes | Yes | Yes | Yes | Yes | | **ViewModel** (`lifecycle-viewmodel-compose:2.10.0+`) | Yes | Yes | Yes | Yes | Yes | | **`collectAsState()`** | Yes | Yes | Yes | Yes | Yes | | **Compose Resources** (`Res.drawable.*`, `Res.string.*`, `Res.font.*`) | Yes | Yes | Yes | Yes | Yes | | `AndroidView` | -- | Yes | -- | -- | -- | | `BackHandler` | -- | Yes | -- | -- | -- | | `dynamicColorScheme()` | -- | Yes | -- | -- | -- | | `LocalContext` | -- | Yes | -- | -- | -- | | `collectAsStateWithLifecycle()` | -- | Yes | -- | -- | -- | | `hiltViewModel()` | -- | Yes | -- | -- | -- | | Baseline Profiles / Macrobenchmark | -- | Yes | -- | -- | -- | | `Window`, `MenuBar`, `Tray` | -- | -- | Yes | -- | -- | | `DialogWindow` | -- | -- | Yes | -- | -- | | `ComposePanel`, `SwingPanel` | -- | -- | Yes | -- | -- | | Scrollbar composables | -- | -- | Yes | -- | -- | | Keyboard shortcuts (`KeyShortcut`) | -- | -- | Yes | -- | -- | | Desktop notifications | -- | -- | Yes | -- | -- | | `UIKitView`, `UIKitViewController` | -- | -- | -- | Yes | -- | | `ComposeUIViewController`, `ComposeUIView` | -- | -- | -- | Yes | -- | | `PlatformImeOptions` | -- | -- | -- | Yes | -- | | `LocalUIViewController`, `LocalUIView` | -- | -- | -- | Yes | -- | | `ComposeViewport` | -- | -- | -- | -- | Yes | | Browser history integration | -- | -- | -- | -- | Yes | **Key insight:** The vast majority of the Compose API surface is available in `commonMain`. Platform-specific APIs exist for interop (embedding native views) and OS-level features (window management, system themes). --- ## expect/actual Patterns ### Pattern 1: isSystemInDarkTheme() The theme detection API is `expect` in commonMain with platform-specific implementations. ```kotlin // commonMain @Composable expect fun isSystemInDarkTheme(): Boolean ``` ```kotlin // skikoMain (Desktop, iOS, Web shared actual) @Composable actual fun isSystemInDarkTheme(): Boolean { val systemTheme = LocalSystemTheme.current return systemTheme == SystemTheme.Dark } ``` ```kotlin // androidMain @Composable actual fun isSystemInDarkTheme(): Boolean { val uiMode = LocalContext.current.resources.configuration.uiMode return (uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES } ``` ### Pattern 2: ResourceReader The resource reading interface has four platform actuals because each platform loads files differently. ```kotlin // commonMain internal expect fun getPlatformResourceReader(): ResourceReader interface ResourceReader { suspend fun read(path: String): ByteArray suspend fun readPart(path: String, offset: Long, size: Long): ByteArray fun getUri(path: String): String } ``` ```kotlin // androidMain — uses AssetManager internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { override suspend fun read(path: String): ByteArray = context.assets.open(path).use { it.readBytes() } override fun getUri(path: String): String = "file:///android_asset/$path" // ... } // desktopMain — uses ClassLoader internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { override suspend fun read(path: String): ByteArray = this::class.java.classLoader!!.getResourceAsStream(path)!!.readBytes() override fun getUri(path: String): String = this::class.java.classLoader!!.getResource(path)!!.toURI().toString() // ... } // iosMain — uses NSBundle + NSFileManager internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { override suspend fun read(path: String): ByteArray { val nsPath = NSBundle.mainBundle.resourcePath + "/" + path return NSFileManager.defaultManager.contentsAtPath(nsPath)!!.toByteArray() } // ... } // wasmJsMain — uses window.fetch() internal actual fun getPlatformResourceReader(): ResourceReader = object : ResourceReader { override suspend fun read(path: String): ByteArray { val response = window.fetch(path).await() return response.arrayBuffer().await().toByteArray() } // ... } ``` ### Pattern 3: rememberResourceState (Sync vs Async) JVM and iOS can load resources synchronously. JS/WASM must load asynchronously (returns default value first, then updates). ```kotlin // JVM/iOS (skikoMain without web) @Composable internal actual fun rememberResourceState( key: Any, getDefault: () -> T, block: suspend () -> T ): State { // Can block briefly to load synchronously on first composition return remember(key) { mutableStateOf(runBlocking { block() }) } } // wasmJsMain @Composable internal actual fun rememberResourceState( key: Any, getDefault: () -> T, block: suspend () -> T ): State { val state = remember(key) { mutableStateOf(getDefault()) } LaunchedEffect(key) { state.value = block() // Async update after initial render } return state } ``` **Pitfall:** On WASM, resources loaded with `Res.*` may flash a default value before the actual resource loads. Design UIs to handle this gracefully (use placeholders or loading states). ### Pattern 4: Font Loading Each platform uses its native font system, so font instantiation is platform-specific. ```kotlin // commonMain @Composable expect fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font // androidMain — Typeface from assets @Composable actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { val typeface = remember(resource) { Typeface.createFromAsset(context.assets, resource.path) } return AndroidFont(typeface, weight, style) } // desktopMain — java.awt.Font from classpath // iosMain — CTFontCreateWithFontDescriptor from bundle // wasmJsMain — FontFace API loaded via fetch ``` ### Pattern 5: getSystemEnvironment Returns locale, theme, and density information per platform. ```kotlin // commonMain internal expect fun getSystemEnvironment(): ResourceEnvironment data class ResourceEnvironment( val language: LanguageQualifier, val region: RegionQualifier, val theme: ThemeQualifier, val density: DensityQualifier ) // androidMain — reads from Configuration + DisplayMetrics // desktopMain — reads from Locale.getDefault() + system theme detection // iosMain — reads from NSLocale.currentLocale + UITraitCollection // wasmJsMain — reads from navigator.language + matchMedia("(prefers-color-scheme: dark)") ``` --- ## Resource System (Res.*) ### Directory Structure Place resources under `commonMain/composeResources/`: ``` commonMain/ composeResources/ drawable/ icon.xml # Vector drawable (works on ALL platforms) logo.png drawable-dark/ icon.xml # Dark theme variant (auto-selected) font/ roboto_regular.ttf roboto_bold.ttf values/ strings.xml # Default strings values-fr/ strings.xml # French localization files/ data.json # Raw files ``` ### Usage ```kotlin // Images @Composable fun AppLogo() { Image( painter = painterResource(Res.drawable.logo), contentDescription = "App logo" ) } // Strings (with arguments) @Composable fun Greeting(name: String) { Text(stringResource(Res.string.greeting, name)) } // Fonts @Composable fun StyledText() { val fontFamily = FontFamily(Font(Res.font.roboto_regular)) Text("Hello", fontFamily = fontFamily) } // Raw files val bytes: ByteArray = Res.readBytes("files/data.json") ``` ### Gotchas **Lottie is NOT KMP-compatible.** The standard `com.airbnb.lottie:lottie-compose` only works on Android. For multiplatform Lottie animations, use: - **Kottie** (`io.github.ismai117:kottie`) -- all CMP targets - **Compottie** (`io.github.alexzhirkevich:compottie`) -- all CMP targets **Multi-module font loading:** Fonts must be declared in the module that owns the `composeResources/` directory. In multi-module projects, place shared fonts in the top-level (app) module or create a shared resources module. Child modules cannot reference parent module resources directly. **Android XML vectors work everywhere.** Android-format `VectorDrawable` XML files placed in `drawable/` render correctly on all platforms via the CMP Skia-based renderer. No conversion needed. **Replace R.* with Res.*:** ```kotlin // Android-only (will not compile in commonMain) painterResource(R.drawable.icon) stringResource(R.string.greeting) // CMP (works in commonMain) painterResource(Res.drawable.icon) stringResource(Res.string.greeting) ``` --- ## Migration from Android-Only to CMP ### Dependency Replacement Table | Android-Only | CMP Replacement | Notes | |-------------|-----------------|-------| | Hilt (`hiltViewModel()`) | Koin (`koinViewModel()`) | Koin has first-class KMP support. Koin 4.0+ has Compose annotations. | | Retrofit | Ktor Client | Ktor has multiplatform HTTP engines per platform. | | Room | SQLDelight | SQLDelight generates Kotlin from SQL. Room KMP is experimental (2.7.0-alpha). | | Coil 2.x | Coil 3.x KMP | Coil 3.0+ is fully multiplatform. Same API. | | Lottie | Kottie / Compottie | See Lottie gotcha above. | | `R.drawable.*`, `R.string.*` | `Res.drawable.*`, `Res.string.*` | Compose Resources replaces Android resources. | | `collectAsStateWithLifecycle()` | `collectAsState()` | `collectAsState()` is available in commonMain. Lifecycle-awareness is Android-specific. | | `BackHandler` | `expect`/`actual` | Implement back handling per platform. Desktop/iOS have different back concepts. | | `LocalContext.current` | `expect`/`actual` | No universal replacement. Abstract platform needs behind an interface. | ### Top 5 Migration Pitfalls **1. `LocalContext.current` sprinkled everywhere** There is no KMP replacement for Android `Context`. Every usage must be audited and abstracted. ```kotlin // Bad: Context usage scattered in composables @Composable fun ShareButton(text: String) { val context = LocalContext.current // Android-only! Button(onClick = { val intent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, text) } context.startActivity(intent) }) { Text("Share") } } // Good: Abstract behind expect/actual // commonMain expect fun shareText(text: String) // androidMain actual fun shareText(text: String) { val intent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, text) } applicationContext.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) } // iosMain actual fun shareText(text: String) { val controller = UIActivityViewController(listOf(text), null) UIApplication.sharedApplication.keyWindow?.rootViewController ?.presentViewController(controller, true, null) } ``` **2. Compose Compiler 2.0.0 incorrect stability inference on non-JVM targets** The Compose compiler may incorrectly mark classes as unstable on iOS/WASM targets, causing excessive recomposition. If you see performance issues on non-Android targets: ```kotlin // Explicitly annotate shared data classes @Immutable data class UiState( val items: List, val isLoading: Boolean ) ``` Always check compiler stability reports for all targets, not just Android. **3. Don't migrate bottom-up -- start from the app module** Migrating leaf modules first creates a broken build that stays broken for weeks. Instead: 1. Add KMP plugin to the app module 2. Move composables to `commonMain` one screen at a time 3. Create `expect`/`actual` stubs for platform dependencies 4. Migrate feature modules once the app module compiles **4. `rememberSaveable` + `Bundle` + `@Parcelize` is Android-only** `Bundle` and `@Parcelize` do not exist on non-Android targets. Use `@Serializable` with a custom `Saver` instead. ```kotlin // Android-only (will not compile in commonMain) @Parcelize data class FormState(val name: String, val email: String) : Parcelable var state by rememberSaveable { mutableStateOf(FormState("", "")) } // CMP-compatible @Serializable data class FormState(val name: String, val email: String) val formStateSaver = Saver( save = { Json.encodeToString(it) }, restore = { Json.decodeFromString(it) } ) var state by rememberSaveable(stateSaver = formStateSaver) { mutableStateOf(FormState("", "")) } ``` **5. Version lockstep: Compose, Kotlin, Gradle, AGP** CMP has strict version compatibility requirements. Mismatched versions produce cryptic compiler errors. ```kotlin // build.gradle.kts -- versions must be compatible plugins { kotlin("multiplatform") version "2.1.20" // Kotlin version id("org.jetbrains.compose") version "1.8.0" // CMP plugin version id("org.jetbrains.kotlin.plugin.compose") version "2.1.20" // Must match Kotlin } // Check compatibility at: // https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html ``` --- ## Navigation in CMP ### Navigation Compose (Official -- Recommended) The official `androidx.navigation:navigation-compose` is fully multiplatform as of Navigation 2.8.0+. Use `@Serializable` type-safe routes. ```kotlin // commonMain -- works on all platforms @Serializable data object Home @Serializable data class Details(val id: Int) @Composable fun AppNavigation() { val navController = rememberNavController() NavHost(navController, startDestination = Home) { composable { HomeScreen(onItemClick = { id -> navController.navigate(Details(id)) }) } composable
{ backStackEntry -> val route = backStackEntry.toRoute
() DetailsScreen(itemId = route.id) } } } ``` **Deep link handling differs per platform.** On Android, deep links integrate with `Intent` and `AndroidManifest.xml`. On other platforms, deep links must be wired manually (e.g., custom URL scheme on iOS via `application(_:open:options:)`, browser URL on Web). ### Navigation 3 (Experimental -- CMP 1.10+) Navigation 3 is a ground-up redesign. `navigation3-common` is multiplatform, but `navigation3-ui` is not yet fully KMP. ```kotlin // Available in CMP 1.10+ (experimental) // navigation3-common: multiplatform // navigation3-ui: limited platform support (check release notes) ``` Wait for stable releases before using in production multiplatform projects. ### Voyager (Third-Party) Simple screen-based navigation. Good for small to medium apps. ```kotlin // commonMain class HomeScreen : Screen { @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow Button(onClick = { navigator.push(DetailsScreen(42)) }) { Text("Go to Details") } } } class DetailsScreen(private val id: Int) : Screen { @Composable override fun Content() { val screenModel = rememberScreenModel { DetailsScreenModel(id) } // ScreenModel is Voyager's equivalent of ViewModel Text("Details for $id") } } // Entry point Navigator(HomeScreen()) ``` ### Decompose (Third-Party) Separates navigation logic from UI. Steeper learning curve but maximum testability and control. Navigation state is held in platform-agnostic `ComponentContext` objects. ```kotlin // Navigation logic (pure Kotlin, no Compose dependency) interface RootComponent { val childStack: Value> sealed class Child { data class Home(val component: HomeComponent) : Child() data class Details(val component: DetailsComponent) : Child() } } // UI layer (Compose) @Composable fun RootContent(component: RootComponent) { val childStack by component.childStack.subscribeAsState() Children(childStack) { child -> when (val instance = child.instance) { is RootComponent.Child.Home -> HomeContent(instance.component) is RootComponent.Child.Details -> DetailsContent(instance.component) } } } ``` ### Navigation Decision Guide | Criteria | Navigation Compose | Voyager | Decompose | |----------|:-----------------:|:-------:|:---------:| | Official support | Yes (Google + JetBrains) | Community | Community | | Learning curve | Low | Low | High | | Type-safe routes | Yes (`@Serializable`) | Manual | Yes | | Testability | Moderate | Moderate | High | | All CMP targets | Yes | Yes | Yes | | ViewModel integration | Yes (`lifecycle-viewmodel`) | ScreenModel | ComponentContext | --- ## Anti-Patterns ### Don't: Use hiltViewModel() in shared code ```kotlin // Will not compile in commonMain -- Hilt is Android-only @Composable fun ProfileScreen() { val viewModel: ProfileViewModel = hiltViewModel() // Android-only! } // Use lifecycle-viewmodel-compose (KMP) or Koin @Composable fun ProfileScreen() { val viewModel = viewModel { ProfileViewModel() } // KMP ViewModel // or val viewModel = koinViewModel() // Koin KMP } ``` ### Don't: Use @Preview from the wrong package in commonMain ```kotlin // Will not compile in commonMain import androidx.compose.ui.tooling.preview.Preview // Android-only package! @Preview @Composable fun MyPreview() { /* ... */ } // CMP preview support varies by IDE and target // Use Android Studio previews in androidMain only // For Desktop, run the app directly (hot reload is fast) ``` ### Don't: Use R.* in shared code ```kotlin // Will not compile in commonMain Image(painter = painterResource(R.drawable.icon), contentDescription = null) Text(stringResource(R.string.title)) // Use Compose Resources Image(painter = painterResource(Res.drawable.icon), contentDescription = null) Text(stringResource(Res.string.title)) ``` ### Don't: Assume collectAsStateWithLifecycle exists in commonMain ```kotlin // Will not compile in commonMain val state by viewModel.uiState.collectAsStateWithLifecycle() // Android-only! // Use collectAsState() instead -- available everywhere val state by viewModel.uiState.collectAsState() // collectAsState() is sufficient for CMP. The lifecycle awareness of // collectAsStateWithLifecycle() is an Android optimization that stops // collection when the app is backgrounded. On Desktop/Web this is // unnecessary; on iOS, CMP handles lifecycle automatically. ``` ================================================ FILE: .claude/skills/compose-expert/references/navigation.md ================================================ # Navigation in Jetpack Compose Reference: `androidx/navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/` ## Setup ### Basic NavHost and NavController ```kotlin val navController = rememberNavController() NavHost( navController = navController, startDestination = "home" // Use Route::class with type-safe navigation ) { composable { HomeScreen(onNavigate = { navController.navigate(Details()) }) } } ``` `rememberNavController()` creates a `NavController` that survives recomposition. Always use it in `NavHost`—never create `NavController` in a ViewModel. ## Type-Safe Navigation (Navigation 2.8+) Use `@Serializable` route classes instead of string routes. This is the recommended pattern. ```kotlin @Serializable data class Home(val userId: String? = null) @Serializable data class Details(val itemId: Int) NavHost(navController, startDestination = Home()) { composable { backStackEntry -> val args = backStackEntry.toRoute() HomeScreen(userId = args.userId) } composable
{ backStackEntry -> val args = backStackEntry.toRoute
() DetailsScreen(itemId = args.itemId) } } ``` Serialize complex types using `@Serializable` on nested data classes: ```kotlin @Serializable data class User(val id: Int, val name: String) @Serializable data class UserProfile(val user: User) // Navigate: navController.navigate(UserProfile(user = User(1, "Alice"))) ``` ## Declaring Destinations ### composable — Screen destinations ```kotlin composable { backStackEntry -> ScreenContent() } ``` ### dialog — Dialog destinations ```kotlin dialog { backStackEntry -> AlertDialog(...) } ``` ### navigation — Nested graphs (feature modules) ```kotlin navigation(startDestination = Home()) { composable { HomeScreen() } composable
{ DetailsScreen() } } ``` ## Navigating ### Navigate to a destination ```kotlin // Type-safe navController.navigate(Details(itemId = 42)) // Avoid: string-based navigation navController.navigate("details/42") // Anti-pattern ``` ### Pop back stack ```kotlin navController.popBackStack() // Pop with return value (save state before popping) navController.previousBackStackEntry?.savedStateHandle?.set("key", value) navController.popBackStack() // In destination, retrieve: val result = navController.currentBackStackEntry?.savedStateHandle?.get("key") ``` ### popUpTo — Clear back stack ```kotlin // Navigate to Details, clearing Home from stack navController.navigate( Details(itemId = 42), navOptions = navOptions { popUpTo(Home::class) { inclusive = false } } ) // inclusive = true: Remove the target route too navController.navigate( Login(), navOptions = navOptions { popUpTo(Home::class) { inclusive = true } } ) // launchSingleTop: Reuse existing instance if already on stack navController.navigate( Details(itemId = 42), navOptions = navOptions { launchSingleTop = true } ) ``` ## Arguments and Back Stack Data Compose Navigation handles serialization automatically with `@Serializable` routes. ### Passing complex data ```kotlin @Serializable data class Message(val id: Int, val text: String, val metadata: Metadata) @Serializable data class Metadata(val timestamp: Long, val priority: Int) navController.navigate(Message(1, "Hello", Metadata(System.currentTimeMillis(), 1))) ``` ### Result passing via SavedStateHandle ```kotlin // Send result back navController.previousBackStackEntry?.savedStateHandle?.set("result", "success") navController.popBackStack() // Receive in previous screen val result = navController.currentBackStackEntry?.savedStateHandle?.get("result") ``` ## Nested Navigation Graphs Organize related destinations into feature graphs. ```kotlin navigation(startDestination = FeatureHome()) { composable { FeatureHomeScreen(onNext = { navController.navigate(FeatureDetail()) }) } composable { FeatureDetailScreen() } } ``` Benefits: scoped ViewModels, separate back stack behavior, feature isolation. ## Deep Links Declare deep links to open your app from URLs or notifications. ```kotlin composable
( deepLinks = listOf( navDeepLink
( uriPattern = "https://example.com/details/{itemId}" ) ) ) { backStackEntry -> val args = backStackEntry.toRoute
() DetailsScreen(itemId = args.itemId) } // Navigate via deep link navController.navigate("https://example.com/details/42") ``` Handle in `AndroidManifest.xml`: ```xml ``` ## Back Stack Management ### saveState / restoreState Preserve screen state during navigation: ```kotlin navController.navigate( Details(itemId = 42), navOptions = navOptions { saveState = true restoreState = true } ) ``` ### Check current route ```kotlin val currentRoute = navController.currentBackStackEntry?.destination?.route ``` ### Observe back stack ```kotlin val backStackEntry by navController.currentBackStackEntryAsState() val route = backStackEntry?.destination?.route ``` ## Bottom Navigation Integration ```kotlin var selectedItem by remember { mutableStateOf("home") } val navController = rememberNavController() Scaffold( bottomBar = { NavigationBar { NavigationBarItem( selected = selectedItem == "home", onClick = { selectedItem = "home" navController.navigate(Home()) { popUpTo(Home::class) { inclusive = true } launchSingleTop = true } }, icon = { Icon(Icons.Default.Home, null) }, label = { Text("Home") } ) NavigationBarItem( selected = selectedItem == "profile", onClick = { selectedItem = "profile" navController.navigate(Profile()) { popUpTo(Home::class) { inclusive = false } launchSingleTop = true } }, icon = { Icon(Icons.Default.Person, null) }, label = { Text("Profile") } ) } } ) { NavHost(navController, startDestination = Home()) { composable { HomeScreen() } composable { ProfileScreen() } } } ``` ## Shared Element Transitions ```kotlin NavHost(navController, startDestination = List()) { composable( sharedTransitionSpec = { SharedTransitionLayout() } ) { ListScreen() } composable( sharedTransitionSpec = { SharedTransitionLayout() } ) { DetailScreen() } } ``` Use in screens: ```kotlin Image( painter = painterResource(id = R.drawable.image), contentDescription = null, modifier = Modifier.sharedBounds( sharedContentState = rememberSharedContentState(key = "image"), animatedVisibilityScope = this ) ) ``` ## ViewModel Scoping with Navigation Use `hiltViewModel()` to scope ViewModels to a back stack entry. ```kotlin composable
{ backStackEntry -> val viewModel: DetailsViewModel = hiltViewModel() DetailsScreen(viewModel = viewModel) } ``` ViewModels scoped this way survive configuration changes but are cleared when the back stack entry is removed. ## Testing Navigation Use `TestNavHostController` to test navigation behavior. ```kotlin @get:Rule val composeTestRule = createComposeRule() @Test fun navigateToDetails() { val navController = TestNavHostController(ApplicationProvider.getApplicationContext()) navController.navigatorProvider.addNavigator(ComposeNavigator()) composeTestRule.setContent { NavHost(navController, startDestination = Home()) { composable { HomeScreen(onNavigate = { navController.navigate(Details()) }) } composable
{ DetailsScreen() } } } composeTestRule.onNodeWithTag("detail_button").performClick() assertEquals(Details::class.serializer().descriptor.serialName, navController.currentBackStackEntry?.destination?.route) } ``` ## Anti-Patterns ### Don't: Use string-based routes ```kotlin // ❌ Anti-pattern navController.navigate("details/42") // ✅ Correct navController.navigate(Details(itemId = 42)) ``` ### Don't: Create NavController in ViewModel ```kotlin // ❌ Anti-pattern class MyViewModel : ViewModel() { val navController = NavController(context) // Wrong! } // ✅ Correct // NavController lives in NavHost, injected into composables ``` ### Don't: Navigate in composition ```kotlin // ❌ Anti-pattern @Composable fun MyScreen() { if (condition) { navController.navigate(Details()) // Navigates on every recomposition! } } // ✅ Correct @Composable fun MyScreen() { LaunchedEffect(condition) { if (condition) { navController.navigate(Details()) } } } ``` ### Don't: Mix navigation approaches ```kotlin // ❌ Anti-pattern navigation(startDestination = "home") { composable("home") { } // String-based composable
{ } // Type-safe mixed with strings } // ✅ Correct @Serializable object FeatureHome navigation(startDestination = FeatureHome()) { composable { } composable { } } ``` ================================================ FILE: .claude/skills/compose-expert/references/performance.md ================================================ # Performance Optimization Reference ## Three Phases: Composition, Layout, Drawing Every frame consists of three phases. Understanding state reads in each phase prevents unnecessary recompositions. ### Composition Phase - Executes composable functions, evaluates state reads - Generates lambda and instance allocations - **State reads here trigger recomposition** of the entire scope ### Layout Phase - Calculates size and position, runs `measure` and `layout` blocks - Can read state without triggering composition recomposition - Mutable state reads OK; prefer `Modifier.offset { }` over `Modifier.offset()` ### Drawing Phase - Emits draw operations, runs `Canvas` and custom `DrawScope` - Cannot read mutable state without stability warnings **Source**: `androidx/compose/runtime/Composer.kt` --- ## Recomposition Skipping with Compiler Reports The Compose compiler generates `$changed` bitmasks to detect state changes. Enable compiler reports to inspect stability and skippability: ```kotlin // build.gradle.kts composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_reports") metricsDestination = layout.buildDirectory.dir("compose_metrics") } ``` After building (`./gradlew assembleRelease`), check the generated files in `build/compose_reports/`: - **`*_composables.txt`** — shows each composable's restartability and skippability: ``` restartable skippable fun MyComponent(name: String, onClick: Function0) restartable fun UnstableComponent(items: List) // NOT skippable — unstable param ``` - **`*_classes.txt`** — shows stability inference for each class: ``` stable class User { stable val name: String } unstable class ScreenState { unstable val items: List } ``` A composable missing `skippable` means the compiler cannot skip it during recomposition, even when inputs haven't changed. Fix by stabilizing its parameters (see Stability section below). ### Stability — @Stable and @Immutable A type is **stable** if: - Its public properties are stable - Overrides to `equals()` and `hashCode()` are based on stable properties - Recomposition is skipped when the same instance is passed Mark stable types explicitly: ```kotlin @Immutable data class Person(val name: String, val age: Int) @Stable class UserViewModel : ViewModel { private val _state = MutableState(UserState()) val state: State = _state } // Composable receiving stable types can skip recomposition @Composable fun PersonCard(person: Person) { Text(person.name) // Skips if person unchanged } ``` **Avoid**: `@Stable` on data classes with mutable fields or non-final properties. --- ## Strong Skipping Mode (Default) Android Gradle Plugin 8.0+ and Compose compiler 1.5.0+ enable **strong skipping mode**. This changes how lambdas are treated: Without strong skipping, every lambda is unstable. With it enabled: - Lambdas become stable if all captured variables are stable - Fewer unnecessary recompositions ```kotlin // With strong skipping: lambda is stable if count is stable @Composable fun Counter(count: Int) { Button(onClick = { println(count) }) { // Stable lambda Text("Count: $count") } } ``` Check `build.gradle.kts`: ```kotlin android { composeOptions { kotlinCompilerExtensionVersion = "1.5.0" } } ``` --- ## Defer State Reads to Layout/Draw Phase Reading state in composition triggers recomposition. Push reads to later phases: ### Bad: Recomposition on Every Offset Change ```kotlin @Composable fun Box(offsetX: State) { val x = offsetX.value // Reads in composition, triggers recomposition Box(modifier = Modifier.offset(x.dp, 0.dp)) } ``` ### Good: Deferred Read in Layout Phase ```kotlin @Composable fun Box(offsetX: State) { Box( modifier = Modifier.offset { IntOffset(offsetX.value.toInt(), 0) // Read in layout phase } ) } ``` Use `Modifier.offset { }` (lambda) instead of `Modifier.offset()` (parameter) for state-dependent positioning. --- ## derivedStateOf — Reducing Recomposition Frequency When deriving expensive computations from state, wrap in `derivedStateOf` to dedup recompositions: ```kotlin // Bad: recomposes on every items change @Composable fun SearchResults(items: List, query: String) { val filtered = items.filter { query in it.title } // Composition phase LazyColumn { items(filtered) { /* ... */ } } } // Good: only recomposes if filtered result actually changes @Composable fun SearchResults(items: List, query: String) { val filtered = remember(items, query) { derivedStateOf { items.filter { query in it.title } } } LazyColumn { items(filtered.value) { /* ... */ } } } ``` `derivedStateOf` deduplicates downstream recompositions — two different filters yielding the same list trigger only one downstream recomposition. --- ## remember with Keys Avoid unnecessary recalculation: ```kotlin // Recalculates on every recomposition @Composable fun ExpensiveItem(id: Int) { val metadata = computeMetadata(id) // Called every time Text(metadata) } // Recalculates only when id changes @Composable fun ExpensiveItem(id: Int) { val metadata = remember(id) { computeMetadata(id) } Text(metadata) } // Multiple keys @Composable fun Item(id: Int, userId: Int) { val data = remember(id, userId) { fetchData(id, userId) } Text(data.toString()) } ``` Omit `remember` if computation is cheap (string formatting, simple objects). Over-wrapping causes memory leaks. --- ## LazyList Performance — Keys and ContentType ### Always Provide Keys Keys enable item reuse and animations: ```kotlin // Bad: no keys, items recreated on every list change LazyColumn { items(users) { user -> UserRow(user) } } // Good: keys enable reuse LazyColumn { items(users, key = { it.id }) { user -> UserRow(user) } } ``` ### ContentType for Efficient Reuse ```kotlin sealed class ListItem { data class Header(val title: String) : ListItem() data class User(val user: User) : ListItem() } LazyColumn { items( items = items, key = { it.hashCode() }, contentType = { item -> when (item) { is ListItem.Header -> "header" is ListItem.User -> "user" } } ) { item -> when (item) { is ListItem.Header -> HeaderRow(item.title) is ListItem.User -> UserRow(item.user) } } } ``` Without `contentType`, all items compete for one ViewHolder pool. With it, items reuse efficiently. ### Avoid Allocations in Item Scope ```kotlin // Bad: allocates on every recomposition LazyColumn { items(users) { user -> val userState = remember { mutableStateOf(user) } UserRow(userState.value) } } // Good: allocates once LazyColumn { items( items = users, key = { it.id } ) { user -> UserRow(user) } } ``` --- ## Baseline Profiles Baseline profiles instruct R8 to pre-compile hot code paths, reducing startup time and jank. ### Generating Profiles Use Jetpack Macrobenchmark to record profiles: ```kotlin @RunWith(AndroidBenchmarkRunner::class) class StartupBenchmark { @get:Rule val benchmarkRule = MacrobenchmarkRule() @Test fun startupCompilation() = benchmarkRule.measureRepeated( packageName = "com.example.app", metrics = listOf(StartupTimings.FIRST_FRAME), iterations = 10, setupBlock = { pressHome() startActivityAndWait() } ) { // Interact with app } } ``` Profiles are generated in `baseline-prof.txt`: ``` androidx/compose/runtime/Recomposer;startRecomposition()V com/example/MyScreen;ComposableFunctionName(ILandroidx/compose/runtime/Composer;I)V ``` --- ## R8/ProGuard Compose Rules Compose includes default ProGuard rules. Ensure `shrinkResources true` and `minifyEnabled true`: ```gradle android { buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } } ``` Custom rules to preserve stability: ```proguard -keep @androidx.compose.runtime.Stable class ** -keep @androidx.compose.runtime.Immutable class ** -keepclassmembers class * { @androidx.compose.runtime.Stable ; } ``` --- ## Measuring Performance ### Layout Inspector — Recomposition Counts In Android Studio: 1. Run app on device 2. Open **Layout Inspector** (Tools > Layout Inspector) 3. Select target process 4. Check **Show Composition Counts** (toggle in inspector) Recomposition counts display how many times each composable was recomposed since inspection started. ### Macrobenchmark — Frame Timing ```kotlin benchmarkRule.measureRepeated( packageName = "com.example.app", metrics = listOf(FrameTimingMetric()), iterations = 10 ) { // Interact: scroll, click, etc. } ``` Reports frame times (ms), jank, jitter. Target <16.67ms for 60 fps. --- ## Common Hot Paths ### String Formatting in Composition ```kotlin // Bad: allocates string every recomposition @Composable fun Counter(count: Int) { Text("Count: ${count}") // String.format called } // Still composed, but optimized @Composable fun Counter(count: Int) { Text(buildString { append("Count: "); append(count) }) } ``` ### List Filtering Without derivedStateOf ```kotlin // Bad: filters every recomposition @Composable fun FilteredList(items: List, predicate: (Item) -> Boolean) { LazyColumn { items(items.filter(predicate)) { /* ... */ } } } // Good @Composable fun FilteredList(items: List, predicate: (Item) -> Boolean) { val filtered = remember(items, predicate) { derivedStateOf { items.filter(predicate) } } LazyColumn { items(filtered.value) { /* ... */ } } } ``` ### Creating Objects in Lambdas ```kotlin // Bad Button( colors = ButtonDefaults.buttonColors( containerColor = if (isPressed) Color.Red else Color.Blue ) ) { } // Good: compute once val buttonColors = remember(isPressed) { ButtonDefaults.buttonColors( containerColor = if (isPressed) Color.Red else Color.Blue ) } Button(colors = buttonColors) { } ``` --- ## Anti-Patterns ### Wrapping Everything in remember ```kotlin // Unnecessary val text = remember { "Hello" } val size = remember { 12.sp } val color = remember { Color.Black } ``` `remember` only for mutable state or expensive calculations. ### Premature Optimization Profile first. Don't add `derivedStateOf` or `remember` without Layout Inspector data. ### @Stable on Mutable Data Classes ```kotlin // DON'T @Stable data class MutableUser(val name: String, val age: MutableState) // DO @Immutable data class User(val name: String, val age: Int) ``` --- ## Resources - **Compose Compiler Reports**: https://developer.android.com/develop/ui/compose/performance/stability-report - **Macrobenchmark**: https://developer.android.com/develop/ui/compose/performance/measurement - **Baseline Profiles**: https://developer.android.com/develop/ui/compose/performance/baseline-profiles --- ## Zero-Size DrawScope Guard During initial composition, a composable's size can be zero. This causes crashes in calculations like `size.minDimension / 2`. ```kotlin // BAD: crashes when size is zero Canvas(modifier = Modifier.fillMaxSize()) { val radius = size.minDimension / 2 // NaN or divide-by-zero drawCircle(color = Color.Blue, radius = radius) } // GOOD: always guard Canvas(modifier = Modifier.fillMaxSize()) { if (size.minDimension <= 0f) return@Canvas val radius = size.minDimension / 2 drawCircle(color = Color.Blue, radius = radius) } ``` Rule: Never use `fillMaxSize()` on Canvas without an explicit height constraint. Always guard DrawScope operations. --- ## Composition Tracing Use `trace()` for Perfetto/systrace integration: ```kotlin @Composable fun ExpensiveScreen() { trace("ExpensiveScreen") { // composable body — visible in system traces } } ``` Enables identifying slow composables in production profiling without adding logging. --- ## movableContentOf Avoid recomposition when moving content between layout positions: ```kotlin val movableContent = remember { movableContentOf { ExpensiveChild() // Only composed once, moved without recomposition } } if (isExpanded) { ExpandedLayout { movableContent() } } else { CollapsedLayout { movableContent() } } ``` Without `movableContentOf`, switching between layouts would dispose and recompose `ExpensiveChild`. With it, the content is moved, preserving state and avoiding recomposition. --- ## Compiler Reports (Expanded) Enable compiler reports to see which composables are skippable and which types are stable: ```kotlin // build.gradle.kts composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_reports") metricsDestination = layout.buildDirectory.dir("compose_metrics") } ``` After building, check the output files: - `*_composables.txt` — shows each composable's status: ``` restartable skippable fun MyComponent(name: String, onClick: Function0) restartable fun UnstableComponent(items: List) // NOT skippable! ``` - `*_classes.txt` — shows type stability: ``` stable class User { stable val name: String } unstable class ScreenState { unstable val items: List } ``` Fix unstable types by: 1. Using `@Stable` annotation on the class 2. Using `kotlinx.collections.immutable.ImmutableList` instead of `List` 3. Adding the class to `compose-stability-config.txt` for multi-module projects --- ## Production Performance Rules 1. **R8: strip previews + semantics in release** — add to `proguard-rules.pro`: ``` -assumenosideeffects class androidx.compose.ui.tooling.preview.** { *; } ``` 2. **`@Suppress("ComposeUnstableCollections")`** — pragmatic skipping when stability isn't worth the complexity: ```kotlin @Suppress("ComposeUnstableCollections") @Composable fun ItemList(items: List) { // List is unstable but acceptable here // ... } ``` 3. **ImmutableList for stability** — Guava `ImmutableList` or `kotlinx.collections.immutable`: ```kotlin // Makes the parameter stable, enabling recomposition skipping @Composable fun StableList(items: ImmutableList) { ... } ``` 4. **ReportDrawnWhen** for startup performance: ```kotlin ReportDrawnWhen { items.isNotEmpty() } ``` 5. **Canvas always explicitly sized** — never `fillMaxSize()` without a height constraint: ```kotlin // BAD Canvas(Modifier.fillMaxSize()) { ... } // GOOD Canvas(Modifier.fillMaxWidth().height(200.dp)) { ... } ``` --- ## Compose Multiplatform Performance Performance tooling differs across platforms: | Tool | Android | Desktop | iOS | Web | |------|---------|---------|-----|-----| | Baseline Profiles | Yes | No | No | No | | Macrobenchmark | Yes | No | No | No | | Layout Inspector | Yes (AS) | No | No | No | | Profiling | Android Studio | JMH | Instruments | Browser DevTools | | R8/ProGuard | Yes | ProGuard separately | N/A (Kotlin/Native) | N/A | iOS-specific: - Kotlin/Native has different GC behavior than Android ART - Enable ProMotion with `CADisableMinimumFrameDurationOnPhone = true` in Info.plist - Use configurable frame rate API: increase for animations, decrease for static content Desktop JVM: - Different GC characteristics (G1GC vs ART) - JIT compilation warms up differently Web/WASM: - Renders entire canvas per frame (unlike DOM partial repaints) - WASM GC behavior differs from JVM - Bundle size impacts initial load time ================================================ FILE: .claude/skills/compose-expert/references/platform-specifics.md ================================================ # Platform-Specific APIs and Gotchas (Compose Multiplatform) Compose Multiplatform shares most UI code across platforms, but entry points, interop APIs, and runtime behavior differ significantly. This reference covers what you need to know per platform. --- ## 1. Desktop (JVM) ### Entry Points Three ways to launch a Compose Desktop application: ```kotlin // Standard — blocks the main thread until all windows close fun main() = application { Window(onCloseRequest = ::exitApplication, title = "My App") { App() } } // Suspending — same behavior, usable from coroutines suspend fun main() = awaitApplication { Window(onCloseRequest = ::exitApplication, title = "My App") { App() } } // Non-blocking — launches in a CoroutineScope, does not block fun main() { val scope = CoroutineScope(Dispatchers.Default) scope.launchApplication { Window(onCloseRequest = ::exitApplication, title = "My App") { App() } } // scope continues running other coroutines } ``` ### Window Composable `Window` is the top-level container. Key parameters: | Parameter | Type | Default | Purpose | |-----------|------|---------|---------| | `onCloseRequest` | `() -> Unit` | required | Called when user clicks close. Use `::exitApplication` or custom logic | | `state` | `WindowState` | auto | Controls size, position, placement (maximized/minimized) | | `title` | `String` | `""` | Window title bar text | | `icon` | `Painter?` | `null` | Window and taskbar icon | | `resizable` | `Boolean` | `true` | Whether user can resize | | `alwaysOnTop` | `Boolean` | `false` | Pin window above all others | | `visible` | `Boolean` | `true` | Show/hide without destroying | | `undecorated` | `Boolean` | `false` | Remove OS title bar and borders | | `transparent` | `Boolean` | `false` | Transparent background (requires `undecorated = true`) | Full example with MenuBar: ```kotlin fun main() = application { val windowState = rememberWindowState( size = DpSize(800.dp, 600.dp), position = WindowPosition(Alignment.Center) ) Window( onCloseRequest = ::exitApplication, state = windowState, title = "My App" ) { MenuBar { Menu("File") { Item("New", shortcut = KeyShortcut(Key.N, meta = true)) { /* handle */ } Item("Open", shortcut = KeyShortcut(Key.O, meta = true)) { /* handle */ } Separator() Item("Exit", onClick = ::exitApplication) } Menu("Edit") { Item("Undo", shortcut = KeyShortcut(Key.Z, meta = true)) { /* handle */ } Item("Redo", shortcut = KeyShortcut(Key.Z, meta = true, shift = true)) { /* handle */ } } } App() } } ``` ### Multi-Window Management ```kotlin fun main() = application { var showSettings by remember { mutableStateOf(false) } Window(onCloseRequest = ::exitApplication, title = "Main") { Button(onClick = { showSettings = true }) { Text("Settings") } App() } if (showSettings) { Window( onCloseRequest = { showSettings = false }, title = "Settings", state = rememberWindowState(size = DpSize(400.dp, 300.dp)) ) { SettingsScreen() } } } ``` ### Tray Icon ```kotlin fun main() = application { var isVisible by remember { mutableStateOf(true) } Tray( icon = painterResource("app_icon.png"), menu = { Item("Show/Hide", onClick = { isVisible = !isVisible }) Item("Exit", onClick = ::exitApplication) } ) if (isVisible) { Window(onCloseRequest = { isVisible = false }, title = "My App") { App() } } } ``` **Gotcha:** On macOS, `Tray` responds to right-click only. Left-click shows nothing by default. This is OS behavior, not a bug. ### DialogWindow ```kotlin var showDialog by remember { mutableStateOf(false) } if (showDialog) { DialogWindow( onCloseRequest = { showDialog = false }, title = "Confirm Action", state = rememberDialogState(size = DpSize(350.dp, 200.dp)) ) { Column(modifier = Modifier.padding(16.dp)) { Text("Are you sure?") Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) { TextButton(onClick = { showDialog = false }) { Text("Cancel") } Button(onClick = { /* confirm */ showDialog = false }) { Text("OK") } } } } } ``` ### Interop: ComposePanel (Compose inside Swing/AWT) Embed Compose content inside an existing Swing application: ```kotlin fun main() { SwingUtilities.invokeLater { val frame = JFrame("Swing + Compose") val composePanel = ComposePanel() composePanel.setContent { MaterialTheme { App() } } // Add to any Swing container — JPanel, JLayeredPane, JSplitPane, etc. frame.contentPane.add(composePanel, BorderLayout.CENTER) frame.setSize(800, 600) frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE frame.isVisible = true } } ``` ### Interop: SwingPanel (Swing inside Compose) Embed a Swing/AWT component inside Compose: ```kotlin @Composable fun LegacyTableView(data: List>) { SwingPanel( modifier = Modifier.fillMaxSize(), factory = { val model = DefaultTableModel( data.map { it.toTypedArray() }.toTypedArray(), arrayOf("Name", "Email", "Role") ) JScrollPane(JTable(model)) }, update = { scrollPane -> // Called on recomposition — update the Swing component here } ) } ``` ### Scrollbar (Desktop-Only) Desktop has explicit scrollbar composables that do not exist on mobile platforms: ```kotlin @Composable fun ScrollableContent() { val scrollState = rememberScrollState() Box(modifier = Modifier.fillMaxSize()) { Column( modifier = Modifier .verticalScroll(scrollState) .padding(end = 12.dp) // leave room for scrollbar ) { repeat(100) { Text("Item $it", modifier = Modifier.padding(8.dp)) } } VerticalScrollbar( adapter = rememberScrollbarAdapter(scrollState), modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), style = ScrollbarStyle( minimalHeight = 16.dp, thickness = 8.dp, shape = RoundedCornerShape(4.dp), hoverDurationMillis = 300, unhoverColor = Color.Black.copy(alpha = 0.12f), hoverColor = Color.Black.copy(alpha = 0.50f) ) ) } } // For LazyColumn: val lazyListState = rememberLazyListState() VerticalScrollbar( adapter = rememberScrollbarAdapter(lazyListState), modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight() ) ``` `HorizontalScrollbar` works identically for horizontal scroll containers. **Key scrolling gotcha:** Desktop scrolling is mouse-wheel only. There are no touch physics, no momentum/fling, and no overscroll bounce effect. Scrolling feels "mechanical" compared to mobile. This is expected desktop behavior, not a bug. --- ## 2. iOS ### Entry Point The iOS entry point creates a `UIViewController` that hosts Compose content: ```kotlin // In iosMain — typically in a file like MainViewController.kt fun MainViewController(): UIViewController = ComposeUIViewController { App() } ``` This is called from Swift in your iOS app target: ```swift // In Swift (e.g., ContentView.swift or AppDelegate) import ComposeApp struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { MainViewControllerKt.MainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} } ``` ### ComposeUIViewController Configuration ```kotlin fun MainViewController(): UIViewController = ComposeUIViewController( configure = { opaque = true // true = opaque background (better perf), false = transparent parallelRendering = false // experimental: parallel rendering pipeline onFocusBehavior = OnFocusBehavior.FocusableAboveKeyboard // auto-scroll focused field above keyboard } ) { App() } ``` ### UIKit Interop — UIKitView Embed native UIKit views inside Compose: ```kotlin @Composable fun NativeMapView(latitude: Double, longitude: Double) { val region = MKCoordinateRegion( CLLocationCoordinate2DMake(latitude, longitude), MKCoordinateSpanMake(0.1, 0.1) ) UIKitView( factory = { MKMapView() }, modifier = Modifier.fillMaxSize(), update = { mapView -> mapView.setRegion(region, animated = true) mapView.mapType = MKMapType.MKMapTypeStandard }, properties = UIKitInteropProperties( interactionMode = UIKitInteropInteractionMode.Cooperative(delayMillis = 150) ) ) } ``` **Interaction modes:** - `Cooperative(delayMillis)` — Compose gets first touch. After `delayMillis`, touch passes to UIKit. Use for maps, web views, scrollable native content. - `NonCooperative` — UIKit gets all touch events. Compose touch handling is blocked in the UIKitView area. Use only when the native view must own all gestures. ### CompositionLocals for iOS ```kotlin // Access the hosting UIViewController val viewController = LocalUIViewController.current viewController.presentViewController(picker, animated = true, completion = null) // Access the underlying UIView val uiView = LocalUIView.current ``` ### Key Gotchas **DisposableEffect cleanup unreliable with UINavigationController:** `DisposableEffect.onDispose` may not fire when a Compose screen is popped from a `UINavigationController`. Do not rely on it for critical cleanup (e.g., releasing resources, stopping location updates). Use ViewModel `onCleared()` or explicit lifecycle observation instead. **Keyboard Done button inserts newline instead of submitting** (JetBrains/compose-multiplatform#3473): On iOS, pressing "Done" on a `TextField` with `ImeAction.Done` may insert `\n` instead of triggering `onImeAction`. Workaround: ```kotlin TextField( value = text, onValueChange = { newText -> // Filter out newlines inserted by Done key if ("\n" in newText && "\n" !in text) { onSubmit() } else { text = newText } }, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done) ) ``` **TextField in scrollable Column pushes TopAppBar off screen** (JetBrains/compose-multiplatform#3621): When a `TextField` inside a scrollable `Column` receives focus and the keyboard appears, the entire content shifts up, pushing the `TopAppBar` out of view. Mitigate by using `LazyColumn` or wrapping the scrollable area below the app bar with `imePadding()`. **Touch delay in Cooperative mode:** With `UIKitInteropInteractionMode.Cooperative(delayMillis = 150)`, there is a noticeable ~150ms delay before UIKit views receive touch. Users may perceive maps/web views as unresponsive. Reduce `delayMillis` if the native view does not conflict with Compose gestures, but be aware that lower values increase accidental UIKit touch interception. **ProMotion 120Hz requires Info.plist entry:** Compose renders at 60Hz by default on ProMotion displays (iPhone 13 Pro+, iPad Pro). Add this to `Info.plist` to enable 120Hz: ```xml CADisableMinimumFrameDurationOnPhone ``` Without this, animations and scrolling will feel noticeably less smooth than native SwiftUI on the same hardware. **App size overhead:** A minimal Compose Multiplatform iOS app is approximately 24.8 MB, compared to ~1.7 MB for an equivalent native SwiftUI app. The Skia rendering engine adds 15-20 MB. This is a fixed cost that does not grow significantly with app complexity, but it matters for markets sensitive to download size. **Flows continue running in background** (JetBrains/compose-multiplatform#3889): Unlike Android (where lifecycle-aware collection pauses in the background), Compose for iOS does not automatically suspend `Flow` collection when the app moves to background. Flows will keep collecting, potentially wasting CPU and battery. You must observe `UIApplication` lifecycle notifications manually: ```kotlin @Composable fun LifecycleAwareCollection(flow: Flow) { val lifecycle = LocalLifecycleOwner.current.lifecycle val data by flow.collectAsStateWithLifecycle( initialValue = Data.Empty, lifecycle = lifecycle ) // Only collects when in foreground } ``` If `collectAsStateWithLifecycle` is not available on your CMP version, use a `DisposableEffect` that observes `NSNotificationCenter` for `UIApplicationDidEnterBackgroundNotification` and `UIApplicationWillEnterForegroundNotification`. --- ## 3. Web / WASM ### Entry Point ```kotlin fun main() { ComposeViewport(viewportContainerId = "root") { App() } } ``` The corresponding HTML needs a container element: ```html My App
``` ### Fundamental Limitation: Canvas-Only Rendering Compose for Web renders EVERYTHING to a single `` element. There are zero DOM elements for your UI content. This has severe consequences: | Feature | Works? | Explanation | |---------|--------|-------------| | Ctrl+F / Cmd+F text search | No | Canvas pixels, not DOM text | | Browser translate (Google Translate) | No | No DOM text to translate | | HTML form autofill | No | No `` elements | | Browser context menus | No | Canvas intercepts right-click | | Password manager autofill | No | No `` | | SEO / search engine indexing | No | Blank `` to crawlers | | Server-side rendering (SSR) | No | Requires JS + WASM runtime | | Screen reader accessibility | Partial | Compose semantics mapped to ARIA, but limited | | Copy/paste text | Partial | Works within Compose, not with browser native | **If you need SEO, text search, or browser-native form behavior, Compose for Web is not the right choice.** Use Kobweb (Compose HTML) or a traditional web framework for content-heavy pages. ### Navigation and Browser History Routes integrate with the browser's History API: ```kotlin // Routes update the URL bar — Back/Forward buttons work // The exact API depends on your navigation library (e.g., Voyager, Decompose) // Key behavior: browser URL updates reflect Compose navigation state ``` ### Browser Compatibility Compose WASM requires WASM GC (Garbage Collection) support: | Browser | Minimum Version | |---------|----------------| | Chrome | 119+ | | Firefox | 120+ | | Safari | 18.2+ | | Edge | 119+ (Chromium-based) | Older browsers will show a blank page. There is no graceful fallback. ### Bundle Size Expect multi-megabyte downloads before first paint: - Skiko WASM module: ~5-8 MB - Application code: varies, but 2-5 MB typical - Total initial load: often 8-15 MB This makes Compose WASM unsuitable for landing pages or content sites where first-paint speed matters. It works best for internal tools, dashboards, or app-like experiences where users accept a loading phase. --- ## 4. Performance Across Platforms ### Rendering Stack Each platform uses a different rendering backend, but all go through Skia: | Platform | Skia Source | Graphics API | Notes | |----------|------------|--------------|-------| | Android | Built into the OS | OpenGL ES / Vulkan | No extra binary size. Vulkan on Android 10+ | | iOS | Bundled with app (Skiko) | Metal | Adds 15-20 MB to app size | | Desktop | Bundled with app (Skiko) | OpenGL / Metal (macOS) / DirectX (Windows) | Auto-selects best backend | | Web | Compiled to WASM (Skiko) | WebGL / WebGPU | Single ``, all rendering via GPU | ### iOS Performance (CMP 1.8+) Performance has improved significantly since early versions: - **Startup time:** Comparable to native SwiftUI. Cold start overhead is minimal once Skia is initialized. - **Scrolling:** On par with SwiftUI, including 120Hz on ProMotion displays (with the Info.plist entry above). - **Complex animations:** Generally smooth, but frame drops can occur with deeply nested animated composables or heavy `Canvas` drawing during transitions. Profile with Instruments if you see jank. ### Configurable Frame Rate For battery optimization, you can configure the target frame rate: ```kotlin // Reduce frame rate when idle or showing static content // Platform-specific API — check your CMP version for exact usage ComposeUIViewController( configure = { // platformLayers configuration varies by version } ) { App() } ``` On iOS, this is particularly useful for apps that show mostly static content but occasionally animate. Reducing from 120Hz to 60Hz (or even 30Hz for static screens) can meaningfully improve battery life. ### Cross-Platform Performance Tips 1. **Avoid `Modifier.graphicsLayer` with `clip = true` on iOS** unless needed. Clipping with Skia on Metal has higher cost than native UIKit clipping. 2. **Image decoding is synchronous on iOS** by default. Use `rememberAsyncImagePainter` (Coil) or similar to avoid blocking the main thread on image-heavy screens. 3. **Desktop: disable vsync for benchmarking** but never in production. Without vsync, frame rates become erratic and tearing is visible. 4. **Web: minimize composable count.** Every composable draws to canvas via WebGL. Complex UIs hit GPU limits faster than on native platforms. 5. **Shared code performance is generally the same across platforms.** The Kotlin compiler generates platform-optimized bytecode. Performance differences come from the rendering backend, not from your composable logic. ================================================ FILE: .claude/skills/compose-expert/references/pr-review.md ================================================ # PR Review Mode Activate when: input contains a GitHub PR URL (`github.com/.+/pull/\d+`) or explicit review phrases: "review this PR", "review this diff", "check this code", "what's wrong with this". When Review Mode activates: 1. Do **not** follow the generation workflow in `SKILL.md` 2. Follow only this document 3. Output a structured local report — do not post to GitHub --- ## Review Workflow ### Step 1 — Fetch the diff ```bash gh pr diff ``` Note all changed `.kt` files. Store the output. ### Step 2 — Fetch full file contents For each changed `.kt` file, fetch the **complete file** — not just the diff lines. ```bash # Get PR metadata gh pr view --json headRefName,headRepository \ --jq '{branch: .headRefName, repo: .headRepository.nameWithOwner}' # Fetch full file (replace {owner}, {repo}, {path}, {branch}) gh api "repos/{owner}/{repo}/contents/{path}?ref={branch}" \ --jq '.content' | base64 -d ``` **Why full files matter:** The diff shows what changed. The full file shows what the composable actually looks like — including whether a `modifier` parameter exists at all, and how modifier chains are structured across multiple lines. Single-line modifier patterns like `Row(modifier = Modifier.fillMaxWidth().padding(16.dp)) {` are invisible in a diff-only view when that line itself was not modified. ### Step 3 — Scan project settings Run in priority order. Always run step d regardless of what lint config is found. **a. `.editorconfig`:** ```bash cat .editorconfig 2>/dev/null || echo "not found" ``` Note: indent size, max line length, trailing comma rules under `[*.kt]`. **b. ktlint config:** ```bash grep -A 20 "\[\*\.kt\]" .editorconfig 2>/dev/null cat .ktlint 2>/dev/null || echo "not found" ``` **c. detekt:** ```bash find . -name "detekt.yml" -o -name "detekt-config.yml" 2>/dev/null | head -3 ``` If found, read it and note complexity, naming, and style rules. **d. Infer codebase conventions (always run):** ```bash # Find 3–5 existing composable files NOT in the diff find . -name "*.kt" -not -path "*/build/*" | xargs grep -l "@Composable" | \ grep -v "Test" | head -5 ``` For each file, note: - Modifier chaining: one per line vs. inline on constructor line - Modifier parameter name: `modifier` vs `Modifier` (both are valid — note which team uses) - Trailing lambda vs. named `content =` on single-slot composables - Named parameter usage on single-arg calls Build a **project profile**. Use it to suppress false positives — flag deviations from the team's own conventions, not deviations from an external style guide. ### Step 4 — Run the checklist Evaluate every changed composable against all 6 categories below. Use the **full file** from Step 2, not the diff from Step 1. ### Step 5 — Output the report Use the format at the end of this document. --- ## Compose Review Checklist ### Category 1: Modifier Hygiene Scan the full file for each changed `@Composable` function. - [ ] **Modifier parameter present.** Every `@Composable` function that renders UI must have a `modifier: Modifier = Modifier` parameter. Flag if absent. Exception: private composables used only as internal implementation detail with no layout impact. - [ ] **Modifier passed to root element.** The `modifier` parameter must be applied to the outermost layout composable in the function body — not ignored, not applied to an inner element, not used on a sibling. - [ ] **Modifier not duplicated.** `modifier` must not be split across two sibling elements. One root element receives it. - [ ] **Modifier ordering follows the paint model.** Work outward-in: `size / fillMaxWidth` → `padding` → `background / border` → `clickable / pointerInput`. Flag these specific reversals: - `background()` before `padding()` when the intent is background-wraps-content (`Modifier.padding(16.dp).background(Color.Red)` = background wraps the padding area; `Modifier.background(Color.Red).padding(16.dp)` = background does NOT include the padding area) - `clickable()` before `padding()` — shrinks the effective touch target - `size()` or `fillMaxWidth()` after `padding()` — the size constraint no longer includes the padding - [ ] **Single-line modifier check.** Read the full constructor line even when unchanged in the diff. Verify ordering is correct even when the entire modifier chain is written inline: `Row(modifier = Modifier.fillMaxWidth().clickable { }.padding(16.dp))` — this has wrong ordering (`clickable` before `padding` shrinks touch target). - [ ] **No `padding` + `offset` for the same adjustment.** `offset` does not affect layout; `padding` does. They are not interchangeable. ### Category 2: Recomposition - [ ] **No unstable parameter types** without stability annotations: - Plain `List` — compiler infers as unstable. Use `@Immutable` data class wrapper, `ImmutableList` (kotlinx-collections-immutable), or annotate the call site - `HashMap`, `MutableMap`, any mutable collection — not stable - Non-`data` class without `@Stable` — compiler cannot infer stability - [ ] **Lambdas not created inline without `remember`.** A new lambda instance on every recomposition of the parent prevents the child from being skipped. ```kotlin // BAD — new lambda every parent recomposition MyComposable(onClick = { doSomething() }) // OK — stable reference val onClick = remember { { doSomething() } } MyComposable(onClick = onClick) ``` Exception: if strong skipping mode is enabled in the Compose compiler (`freeCompilerArgs += "-P", "plugin:androidx.compose.compiler.plugins.kotlin:experimentalStrongSkipping=true"`), lambdas are stable by default. - [ ] **`derivedStateOf {}` used for computed values.** Flag values computed from state reads inside a composable body without `remember { derivedStateOf { ... } }`: ```kotlin // BAD — recomputes and recomposes every time any state changes val isValid = username.isNotEmpty() && password.length > 8 // OK val isValid by remember { derivedStateOf { username.isNotEmpty() && password.length > 8 } } ``` - [ ] **`remember {}` keys are correct.** - Missing keys but referencing an input variable inside — stale value bug - Key that never changes (`remember(Unit)` or `remember(constant)`) — effectively `remember {}` with no recalculation ### Category 3: M3 Motion Cross-reference with `references/material3-motion.md` for token values and easing names. - [ ] **No hardcoded integer durations** in `tween()`, `spring()`, or `keyframes {}`. Flag any `tween(N)` where N is a plain integer literal. Suggest nearest `MotionTokens.Duration*` token. - [ ] **No pre-M3 easing constants:** - `FastOutSlowInEasing` → `MotionTokens.EasingEmphasizedCubicBezier` - `LinearOutSlowInEasing` → `MotionTokens.EasingEmphasizedDecelerateCubicBezier` - `FastOutLinearInEasing` → `MotionTokens.EasingEmphasizedAccelerateCubicBezier` - [ ] **`animateColorAsState` has an `animationSpec`.** No-spec `animateColorAsState(target)` uses the default spring which is inappropriate for color transitions. Suggest: `animationSpec = MaterialTheme.motionScheme.defaultEffectsSpec()` - [ ] **`AnimatedVisibility` enter/exit easing is asymmetric.** Enter must use Decelerate easing; exit must use Accelerate easing. Same easing for both is incorrect. - [ ] **No non-shared-element animation > 600ms.** Flag durations above `DurationLong4` (600ms) unless the animation is a shared element or full-screen transition. - [ ] **New components use `MotionScheme` not raw `tween()`.** A new component accepting no `AnimationSpec` parameter should use `MaterialTheme.motionScheme.defaultSpatialSpec()` / `defaultEffectsSpec()` to be theme-motion-aware. ### Category 4: CMP Compatibility Apply only to files whose path contains `commonMain` or has no platform-specific path segment. - [ ] No `android.*` imports (`android.content.Context`, `android.util.Log`, etc.) - [ ] No `androidx.*` imports that are Android-only. Check `references/multiplatform.md` API availability matrix for what is available in `commonMain`. - [ ] No `LocalContext.current` — not available in CMP `commonMain` - [ ] No `Activity`, `Context`, or `Application` references - [ ] Resources via `Res.drawable.*` / `Res.string.*` — not `R.drawable.*` / `R.string.*` ### Category 5: Lists & Keys - [ ] **Every `items()` call has a `key = {}`** in `LazyColumn`, `LazyRow`, `LazyVerticalGrid`, `LazyHorizontalGrid`. Missing keys cause incorrect animations and item reuse bugs. ```kotlin // BAD items(movies) { movie -> MovieCard(movie) } // OK items(movies, key = { it.id }) { movie -> MovieCard(movie) } ``` - [ ] **`contentType = {}` present for heterogeneous lists.** When a lazy list renders more than one type of composable (e.g., headers + items), `contentType` must be specified so Compose reuses composition nodes correctly. - [ ] **No `LazyColumn` directly nested inside `LazyColumn`** without a fixed height on the inner one. Unbounded nested lazy lists throw `java.lang.IllegalStateException` at runtime. - [ ] **`TvLazyRow` / `TvLazyColumn` / `TvLazyVerticalGrid` / `TvLazyHorizontalGrid` from `tv-foundation` flagged as deprecated.** Replace with standard Foundation equivalents. See migration table in `references/tv-compose.md` Section 5. ### Category 6: Atomic Design Cross-reference with `references/atomic-design.md` for token patterns and naming rules. - [ ] **No hardcoded `Color(0xFF...)` inside composable bodies.** Colors must come from `MaterialTheme.colorScheme.*` or an app-level brand token (`CompositionLocal`). Exception: `Color.Transparent`, `Color.Unspecified`, and `Color.White`/`Color.Black` as explicit design choices are acceptable. - [ ] **No hardcoded `fontSize`, `fontWeight`, or `TextStyle(...)`.** Typography must come from `MaterialTheme.typography.*`. Flag any inline `TextStyle(fontSize = 14.sp)` or `fontWeight = FontWeight.Bold` outside of a theme definition file. - [ ] **No magic number spacing (`16.dp`) without token.** If the project defines a spacing scale (check for `CompositionLocal` with spacing values), flag raw `dp` values that should use the scale. If no spacing scale exists, note it as a suggestion — not a critical issue. - [ ] **Composable names describe function, not context.** Flag composables matching these patterns: `*For*` (e.g., `ButtonForSettings`), `*With*` (e.g., `CardWithRedBorder`), `*In*` (e.g., `HeaderInHome`). Exception: `*WithDefaults` pattern used for providing default parameters is acceptable. - [ ] **Public composables have `modifier: Modifier = Modifier`.** (Overlaps Category 1 — in atomic context, additionally verify the modifier is passed to the root element and not consumed by an inner element.) - [ ] **Composables rendering variable content have slot APIs.** Flag composables that hardcode `Text("Submit")`, `Icon(Icons.Default.Close, ...)`, or similar fixed content that should be a slot parameter. Exception: internal/private composables with fixed content by design. - [ ] **Organisms do not directly reference ViewModel.** Any composable that combines multiple UI components (organism-level) must accept data and callbacks as parameters. Flag direct `viewModel()`, `hiltViewModel()`, or `koinViewModel()` calls inside organisms. The screen-level composable is the correct place for ViewModel access. --- ## Output Report Format ``` ## PR Review: (#NNN) Branch: ### Project Profile - Code style: - Lint config: - Conventions from: --- ### Issues #### Critical Issues that cause bugs, crashes, or correctness problems. - `path/File.kt:42` — `MyCard` is missing `modifier: Modifier = Modifier` parameter. All UI composables must expose a modifier for caller layout control. Fix: add `modifier: Modifier = Modifier` to the signature; pass to the root element. #### Suggestions Style, M3 alignment, and performance improvements. - `path/File.kt:87` — `tween(300)` → `MotionTokens.DurationMedium2.toInt()` (300ms = Medium2) - `path/File.kt:103` — `FastOutSlowInEasing` → `MotionTokens.EasingEmphasizedCubicBezier` - `path/File.kt:115` — `items(movies)` missing `key = { it.id }` — add key to prevent reorder bugs #### Positive Patterns Good Compose usage — always include at least one. - `path/File.kt:55` — Correct `derivedStateOf {}` preventing redundant recompositions - `path/File.kt:71` — `sharedBounds()` used correctly for container-to-page expansion --- ### Summary critical, suggestions across files reviewed. ``` **Sections policy:** - Critical and Suggestions are always present (write "None found" if empty) - Positive Patterns is always present — reviews must not read as a pure hit list ================================================ FILE: .claude/skills/compose-expert/references/production-crash-playbook.md ================================================ # Production Crash Playbook for Jetpack Compose Real-world crash patterns observed in Compose applications at scale. Each section documents the root cause, the failing pattern, the fix, and the rule to prevent recurrence. --- ## 1. remember {} Without Configuration-Derived Key ### Root Cause `remember {}` without keys caches the initial computation and never recalculates. When the remembered value derives from configuration state (screen dimensions, font scale, density), a configuration change (rotation, foldable posture, window resize) leaves the cached value stale. ### Crash Pattern ```kotlin // BAD: shimmerCount is cached from initial screenHeightDp, stale after rotation @Composable fun ShimmerList() { val config = LocalConfiguration.current val screenHeightDp = config.screenHeightDp val itemHeightDp = 80 val shimmerCount = remember { (screenHeightDp / itemHeightDp).toInt() } LazyColumn { items(shimmerCount) { ShimmerItem() } } } ``` After rotation, `screenHeightDp` changes but `shimmerCount` still holds the portrait value. The list renders the wrong number of shimmer placeholders. In extreme cases (foldable unfolding), the stale count causes layout overflow or zero items. ### Fix ```kotlin // GOOD: screenHeightDp is a key, so remember recalculates on config change @Composable fun ShimmerList() { val config = LocalConfiguration.current val screenHeightDp = config.screenHeightDp val itemHeightDp = 80 val shimmerCount = remember(screenHeightDp) { (screenHeightDp / itemHeightDp).toInt().coerceAtLeast(1) } LazyColumn { items(shimmerCount) { ShimmerItem() } } } ``` **Rule:** Any value derived from `LocalConfiguration`, `LocalDensity`, or `LocalLayoutDirection` MUST include that configuration source in `remember`'s key parameters. Audit all `remember {}` calls that reference `screenHeightDp`, `screenWidthDp`, `fontScale`, or `densityDpi`. --- ## 2. indexOf() Inside items {} ### Root Cause Using `list.indexOf(item)` inside a LazyColumn's `items {}` block is O(n) per item, making the overall list O(n^2). Worse, `indexOf` uses structural equality (`equals`). If the list contains recreated objects (new instances from a network response mapped to data classes), `indexOf` may return `-1`, which when passed as an index triggers `IndexOutOfBoundsException`. ### Crash Pattern ```kotlin // BAD: O(n^2) and crashes when indexOf returns -1 @Composable fun MessageList(messages: List) { LazyColumn { items(messages) { message -> val index = messages.indexOf(message) // O(n) per item MessageRow( message = message, isEven = index % 2 == 0 // -1 % 2 == -1, not a crash here ) } } } // Worse: using indexOf result as a direct index items(messages) { message -> val index = messages.indexOf(message) val nextMessage = messages[index + 1] // IndexOutOfBoundsException } ``` ### Fix ```kotlin // GOOD: Use itemsIndexed to get the index directly @Composable fun MessageList(messages: List) { LazyColumn { itemsIndexed( items = messages, key = { _, message -> message.id } ) { index, message -> MessageRow( message = message, isEven = index % 2 == 0 ) } } } ``` If you need `items` with a key but also need the index, use `items` with `key` and derive the index from the item itself if possible, or switch to `itemsIndexed`. **Rule:** Never call `indexOf()`, `lastIndexOf()`, or `indexOfFirst {}` inside a `LazyListScope` item factory. Use `itemsIndexed` for index access. Use `items(key = { ... })` for stable identity. --- ## 3. DrawScope Without Zero-Size Guard ### Root Cause During initial composition, a `Canvas` composable may receive a `Size.Zero` (or `Size.Unspecified`) before layout completes, especially when using `fillMaxSize()` inside a parent that hasn't been measured yet. Dividing by zero-dimension values or computing radii from `size.minDimension` when it's zero produces `NaN` or `Infinity`, which crashes the Skia rendering pipeline. ### Crash Pattern ```kotlin // BAD: size.minDimension is 0 during initial composition Canvas(modifier = Modifier.fillMaxSize()) { val radius = size.minDimension / 2 drawCircle(color = Color.Blue, radius = radius) } ``` On certain devices or inside `SubcomposeLayout`, the first draw call fires with `size = Size(0, 0)`. The `drawCircle` call with `radius = 0f` may not crash, but derived math like `360f / size.width` produces `Infinity` and corrupts the draw path. ### Fix ```kotlin // GOOD: guard against zero size, and give Canvas an explicit size Canvas( modifier = Modifier .fillMaxWidth() .height(200.dp) ) { if (size.minDimension <= 0f) return@Canvas val radius = size.minDimension / 2 drawCircle(color = Color.Blue, radius = radius) } ``` For dynamic sizing, use `Modifier.aspectRatio()` or `BoxWithConstraints` to guarantee non-zero dimensions before drawing: ```kotlin BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { if (maxWidth > 0.dp && maxHeight > 0.dp) { Canvas(modifier = Modifier.size(maxWidth, maxWidth)) { val radius = size.minDimension / 2 drawCircle(color = Color.Blue, radius = radius) } } } ``` **Rule:** Always guard `DrawScope` blocks against zero-size conditions. Never use `fillMaxSize()` on `Canvas` without an explicit height constraint. Prefer `Modifier.size()`, `Modifier.height()`, or `Modifier.aspectRatio()` so Canvas always enters draw with known dimensions. --- ## 4. Duplicate LazyColumn Keys ### Root Cause `LazyColumn` requires unique keys across all items. When the backend sends items without unique IDs, or when a WebSocket reconnection delivers duplicate messages before deduplication, duplicate keys cause `IllegalArgumentException: Key ... was already used`. ### Crash Pattern ```kotlin // BAD: backend sends duplicate IDs after WebSocket reconnect data class Notification(val id: String, val text: String) LazyColumn { items( items = notifications, key = { it.id } // Crashes if two items share the same id ) { notification -> NotificationRow(notification) } } ``` ### Fix: Dedup Index Pattern Add a `dedupIndex` field that is excluded from `equals`/`hashCode` but included in the key. This handles duplicates gracefully without losing data: ```kotlin data class Notification( val id: String, val text: String, val timestamp: Long ) { // dedupIndex is NOT in the primary constructor, excluded from equals/hashCode var dedupIndex: Int = 0 } fun List.withDedupIndex(): List { val seen = mutableMapOf() return map { item -> val count = seen.getOrDefault(item.id, 0) seen[item.id] = count + 1 item.also { it.dedupIndex = count } } } @Composable fun NotificationList(notifications: List) { val deduped = remember(notifications) { notifications.withDedupIndex() } LazyColumn { items( items = deduped, key = { "${it.id}_${it.dedupIndex}" } // Guaranteed unique ) { notification -> NotificationRow(notification) } } } ``` An alternative defensive approach using `distinctBy` when true duplicates should be dropped: ```kotlin val uniqueNotifications = remember(notifications) { notifications.distinctBy { it.id } } ``` **Rule:** Never trust backend data to provide unique keys. Either deduplicate with `distinctBy` or use the dedup-index pattern to make keys unique. Wrap key construction in a function that can be unit-tested. --- ## 5. derivedStateOf Driving Collection Size ### Root Cause `derivedStateOf` recalculates lazily when its inputs change, but the recomposition that reads the derived value and the recomposition that reads the source collection may occur in different frames. When `derivedStateOf` exposes only a count, the `items(count)` call may use a stale count that is out of sync with the actual collection, causing `IndexOutOfBoundsException`. ### Crash Pattern ```kotlin // BAD: derive only the count; items {} reads allItems directly with stale count @Composable fun FilteredList(allItems: List, filter: String) { val itemCount by remember { derivedStateOf { allItems.count { it.name.contains(filter) } } } LazyColumn { items(itemCount) { index -> val item = allItems.filter { it.name.contains(filter) }[index] // itemCount may be stale relative to allItems, IOOB crash ItemRow(item) } } } ``` ### Fix ```kotlin // GOOD: derive the full filtered list; count and access are always consistent @Composable fun FilteredList(allItems: List, filter: String) { val filteredItems by remember { derivedStateOf { allItems.filter { it.name.contains(filter) } } } LazyColumn { items( items = filteredItems, key = { it.id } ) { item -> ItemRow(item) } } } ``` **Rule:** `derivedStateOf` is appropriate for scroll direction, visibility thresholds, and validation states -- values that don't drive collection iteration. Never use `derivedStateOf` to expose a count or index that a `LazyList` will use to access a separate collection. --- ## 6. collectAsState vs collectAsStateWithLifecycle ### Root Cause `collectAsState()` subscribes to a `Flow` and keeps collecting even when the app is in the background. This causes unnecessary work (network calls, database queries, sensor reads), battery drain, and stale state that flashes briefly when the user returns to the app. ### The Difference ```kotlin // BAD: continues collecting in background @Composable fun ProfileScreen(viewModel: ProfileViewModel) { val state by viewModel.uiState.collectAsState() ProfileContent(state) } // GOOD: stops collecting when lifecycle drops below STARTED @Composable fun ProfileScreen(viewModel: ProfileViewModel) { val state by viewModel.uiState.collectAsStateWithLifecycle() ProfileContent(state) } ``` `collectAsStateWithLifecycle` is part of `androidx.lifecycle:lifecycle-runtime-compose`. It automatically cancels collection when the `Lifecycle` drops below a configurable state (default: `Lifecycle.State.STARTED`). ### Custom Lifecycle Minimum ```kotlin // Collect only when RESUMED (stricter, for camera/location flows) val locationState by locationFlow.collectAsStateWithLifecycle( minActiveState = Lifecycle.State.RESUMED ) ``` ### Compose Multiplatform Consideration `collectAsStateWithLifecycle` is Android-only because it depends on `androidx.lifecycle`. In Compose Multiplatform (CMP) projects, use `collectAsState()` on non-Android targets and `collectAsStateWithLifecycle()` on Android via an `expect`/`actual` pattern: ```kotlin // commonMain @Composable expect fun Flow.collectAsStateMultiplatform( initial: T ): State // androidMain @Composable actual fun Flow.collectAsStateMultiplatform( initial: T ): State = collectAsStateWithLifecycle(initialValue = initial) // iosMain / desktopMain / wasmJsMain @Composable actual fun Flow.collectAsStateMultiplatform( initial: T ): State = collectAsState(initial = initial) ``` **Rule:** On Android, always use `collectAsStateWithLifecycle` for `StateFlow` and `SharedFlow`. Reserve `collectAsState` for Compose Multiplatform common code or non-lifecycle-aware contexts only. --- ## 7. SafeShimmerItem Pattern ### Root Cause Shimmer placeholders rendered via `SubcomposeLayout` can crash when the parent layout has zero size (see Section 3) or when the shimmer animation's `DrawScope` receives unexpected dimensions. A defensive wrapper prevents crashes while maintaining the shimmer UX. ### Full Implementation ```kotlin @Composable fun SafeShimmerItem( modifier: Modifier = Modifier, itemHeight: Dp = 80.dp, shimmerColor: Color = Color.LightGray.copy(alpha = 0.6f), highlightColor: Color = Color.LightGray.copy(alpha = 0.2f), ) { val transition = rememberInfiniteTransition(label = "shimmer") val translateAnim by transition.animateFloat( initialValue = 0f, targetValue = 1000f, animationSpec = infiniteRepeatable( animation = tween(durationMillis = 1200, easing = LinearEasing), repeatMode = RepeatMode.Restart ), label = "shimmer_translate" ) val brush = Brush.linearGradient( colors = listOf(shimmerColor, highlightColor, shimmerColor), start = Offset(translateAnim - 500f, 0f), end = Offset(translateAnim, 0f) ) Box( modifier = modifier .fillMaxWidth() .height(itemHeight) .clip(RoundedCornerShape(8.dp)) ) { val result = runCatching { Canvas(modifier = Modifier.matchParentSize()) { if (size.minDimension <= 0f) return@Canvas drawRect(brush = brush) } } if (result.isFailure) { // Fallback: solid color placeholder when Canvas fails Box( modifier = Modifier .matchParentSize() .background(shimmerColor, RoundedCornerShape(8.dp)) ) } } } ``` ### Usage with Configuration-Aware Count ```kotlin @Composable fun ShimmerLoadingList() { val config = LocalConfiguration.current val itemHeight = 80.dp val shimmerCount = remember(config.screenHeightDp) { (config.screenHeightDp / itemHeight.value).toInt().coerceAtLeast(1) } LazyColumn { items(shimmerCount) { SafeShimmerItem(itemHeight = itemHeight) } } } ``` **Rule:** Wrap all shimmer and placeholder drawing in `runCatching` or explicit size guards. Provide a solid-color fallback so the loading state is never invisible and never crashes. --- ## 8. Multi-Field Keys with Collision Prefixes ### Root Cause When a `LazyColumn` displays items from multiple categories (live, archived, pinned), using the raw ID as the key causes collisions when the same underlying entity appears in multiple sections. For example, a message with `id = 42` could appear in both "pinned" and "live" sections. ### Crash Pattern ```kotlin // BAD: same message ID in pinned and live sections causes key collision LazyColumn { items(pinnedMessages, key = { it.id }) { message -> PinnedMessageRow(message) } items(liveMessages, key = { it.id }) { message -> LiveMessageRow(message) } } // IllegalArgumentException: Key 42 was already used ``` ### Fix: Type-Prefixed Keys ```kotlin // GOOD: prefix keys with section type to avoid collisions LazyColumn { items( items = pinnedMessages, key = { "pinned_${it.id}" } ) { message -> PinnedMessageRow(message) } items( items = liveMessages, key = { "live_${it.id}" } ) { message -> LiveMessageRow(message) } items( items = archivedMessages, key = { "archived_${it.id}" } ) { message -> ArchivedMessageRow(message) } } ``` ### Sealed Class Alternative For type safety, use a sealed class for keys: ```kotlin sealed class ListKey { data class Pinned(val id: Long) : ListKey() data class Live(val id: Long) : ListKey() data class Archived(val id: Long) : ListKey() } LazyColumn { items(pinnedMessages, key = { ListKey.Pinned(it.id) }) { ... } items(liveMessages, key = { ListKey.Live(it.id) }) { ... } items(archivedMessages, key = { ListKey.Archived(it.id) }) { ... } } ``` **Rule:** When mixing item types in a single `LazyList`, always prefix or wrap keys with the item type. Use string prefixes for simplicity or sealed classes for compile-time safety. --- ## 9. Production State Rules These rules prevent the most common state-related crashes and architectural mistakes in production Compose applications. ### Rule 1: mutableStateOf ONLY in Composables, Never in ViewModels ```kotlin // BAD: Compose state in ViewModel couples VM to Compose runtime class ProfileViewModel : ViewModel() { var name by mutableStateOf("") // Don't do this } // GOOD: Use coroutine-native state in ViewModel class ProfileViewModel : ViewModel() { private val _name = MutableStateFlow("") val name: StateFlow = _name.asStateFlow() } ``` ViewModels should expose `StateFlow` (via `MutableStateFlow` + `asStateFlow()`). Compose state (`mutableStateOf`) belongs in `@Composable` functions and state holder classes annotated with `@Stable`. This keeps ViewModels testable without the Compose runtime. ### Rule 2: SharedFlow for Events, Not Channel ```kotlin // BAD: Channel drops events when no collector is active class OrderViewModel : ViewModel() { private val _events = Channel() val events = _events.receiveAsFlow() } // GOOD: SharedFlow with buffer handles brief collector gaps class OrderViewModel : ViewModel() { private val _events = MutableSharedFlow( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST ) val events: SharedFlow = _events.asSharedFlow() } ``` `Channel` is a hot stream that requires an active collector. During configuration changes or lifecycle transitions, events are silently dropped. `SharedFlow` with `extraBufferCapacity = 1` buffers one event during brief collector gaps. ### Rule 3: rememberSaveable Only at NavGraph Level ```kotlin // BAD: rememberSaveable deep in a list item (bloats saved state bundle) @Composable fun ChatMessageItem(message: Message) { var expanded by rememberSaveable { mutableStateOf(false) } // Saved for every message in the list -- Bundle size explodes } // GOOD: rememberSaveable at screen level, remember inside items @Composable fun ChatScreen(viewModel: ChatViewModel) { var searchQuery by rememberSaveable { mutableStateOf("") } LazyColumn { items(messages, key = { it.id }) { message -> var expanded by remember { mutableStateOf(false) } ChatMessageItem(message, expanded) } } } ``` `rememberSaveable` serializes to the `Bundle`, which has a ~1MB limit on Android. Using it inside list items for per-item state quickly exceeds this limit and causes `TransactionTooLargeException`. ### Rule 4: snapshotFlow + distinctUntilChanged for Reactive Scroll ```kotlin // GOOD: efficient reactive scroll position monitoring @Composable fun ScrollAwareList(listState: LazyListState) { val isScrolledPast = remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } // For side effects based on scroll position LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .distinctUntilChanged() .collect { index -> // Analytics, FAB visibility, header collapse } } } ``` `snapshotFlow` converts Compose snapshot state to a `Flow`, and `distinctUntilChanged()` prevents redundant emissions. Never poll scroll state in a recomposition-driven loop. ### Rule 5: stateIn() with map() for Derived Flows ```kotlin // GOOD: derive UI state from repository flow class DashboardViewModel(repository: DashboardRepository) : ViewModel() { val uiState: StateFlow = repository.dashboardData .map { data -> DashboardUiState( totalSales = data.sales.sumOf { it.amount }, topProduct = data.products.maxByOrNull { it.revenue }?.name.orEmpty(), isLoading = false ) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = DashboardUiState() ) } ``` `SharingStarted.WhileSubscribed(5_000)` keeps the upstream active for 5 seconds after the last subscriber detaches, surviving configuration changes without restarting the flow. Combine with `.map()` for derived transformations instead of creating separate `derivedStateOf` in the UI. --- ## 10. Production Performance Rules ### Rule 1: @Stable on UI State, @Immutable on Data Models ```kotlin // Data models from network/database: truly immutable @Immutable data class Product( val id: Long, val name: String, val price: Double, val imageUrl: String ) // UI state with observable mutations: stable contract @Stable data class CartUiState( val items: List = emptyList(), val total: Double = 0.0, val isLoading: Boolean = false ) ``` `@Immutable` tells the compiler all properties will never change after construction. `@Stable` promises that changes will be notified to the Compose runtime. Misuse (e.g., `@Immutable` on a class with `var` properties) causes skipped recompositions and stale UI. ### Rule 2: Canvas Always Explicitly Sized ```kotlin // BAD: Canvas with fillMaxSize and no parent constraints Canvas(modifier = Modifier.fillMaxSize()) { /* may get zero size */ } // GOOD: explicit dimensions Canvas(modifier = Modifier.size(200.dp)) { ... } // GOOD: explicit height with flexible width Canvas(modifier = Modifier.fillMaxWidth().height(120.dp)) { ... } // GOOD: aspect ratio Canvas(modifier = Modifier.fillMaxWidth().aspectRatio(16f / 9f)) { ... } ``` See Section 3 for detailed crash scenarios. Explicit sizing prevents zero-size draw calls. ### Rule 3: ReportDrawnWhen for Startup Signals ```kotlin @Composable fun HomeScreen(viewModel: HomeViewModel) { val state by viewModel.uiState.collectAsStateWithLifecycle() ReportDrawnWhen { state.isContentReady } when { state.isLoading -> ShimmerLoadingList() state.isContentReady -> ContentList(state.items) } } ``` `ReportDrawnWhen` signals to the system (and performance monitoring tools) when the screen has meaningful content. This is critical for accurate Time-To-Initial-Display (TTID) and Time-To-Full-Display (TTFD) metrics. ### Rule 4: R8 Strip Previews and Semantics in Release ```kotlin // In build.gradle.kts (app module) android { buildTypes { release { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } } ``` In `proguard-rules.pro`, R8 automatically strips `@Preview` composables from release builds. For semantics stripping in production (accessibility data you don't need in release), use `Modifier.clearAndSetSemantics {}` selectively rather than blanket removal. ### Rule 5: Pragmatic Collection Stability ```kotlin // When a composable receives a List that you know won't mutate: @Suppress("ComposeUnstableCollections") @Composable fun ProductGrid( products: List, // Compiler sees List as unstable onProductClick: (Product) -> Unit ) { LazyVerticalGrid(columns = GridCells.Fixed(2)) { items(products, key = { it.id }) { product -> ProductCard(product, onProductClick) } } } ``` The Compose compiler treats `List`, `Map`, and `Set` as unstable because they are interfaces that could be backed by mutable implementations. Options to handle this: 1. **`@Suppress("ComposeUnstableCollections")`** -- pragmatic, per-function opt-out 2. **Wrap in `@Immutable` holder** -- `@Immutable data class ProductList(val items: List)` 3. **Use `kotlinx.collections.immutable`** -- `ImmutableList` is recognized as stable 4. **Compose compiler stability config file** -- declare stable classes globally Choose based on team convention. Option 1 is fastest for existing codebases. Option 3 is cleanest for new projects. --- ## Quick Reference: Crash Pattern Checklist | Pattern | Symptom | Section | |---------|---------|---------| | `remember {}` without config key | Stale values after rotation | 1 | | `indexOf()` in `items {}` | O(n^2), IndexOutOfBoundsException | 2 | | Canvas with zero size | NaN/Infinity in draw, Skia crash | 3 | | Duplicate LazyColumn keys | IllegalArgumentException | 4 | | `derivedStateOf` for collection count | IndexOutOfBoundsException | 5 | | `collectAsState` in background | Battery drain, stale flash | 6 | | Unguarded shimmer drawing | Crash in SubcomposeLayout | 7 | | Key collision across item types | IllegalArgumentException | 8 | | `mutableStateOf` in ViewModel | Untestable, lifecycle mismatch | 9 | | Canvas with `fillMaxSize()` only | Zero-size draw crash | 10 | ================================================ FILE: .claude/skills/compose-expert/references/side-effects.md ================================================ # Jetpack Compose Side Effects Reference Compose is declarative, but apps must interact with the imperative world: launch coroutines, register listeners, manage resources. Side effects are the bridge. Understanding when and how to use them is essential for correctness. ## The Effect Mental Model Compose recomposes when state changes. Effects are blocks of code that run outside the normal composition and recomposition cycle: - **Composition**: Calculate the UI tree - **Side effects**: Run imperative code (coroutines, callbacks, lifecycle events) - **Layout**: Measure and position elements - **Drawing**: Render to screen Effects run *after* composition succeeds. If composition fails, the effect doesn't run. ```kotlin @Composable fun MyScreen() { // This runs during composition val state = remember { mutableStateOf("initial") } // This runs AFTER composition, and only when 'state.value' changes LaunchedEffect(state.value) { println("State changed to: ${state.value}") } // This runs after every composition (use sparingly) SideEffect { println("Recomposition happened") } // This runs when composable leaves composition DisposableEffect(Unit) { onDispose { println("Composable is leaving composition") } } Button(onClick = { state.value = "updated" }) { Text(state.value) } } ``` ## SideEffect — After Every Successful Composition `SideEffect` runs after *every* successful composition. It has no cleanup, no keys, and always executes. ```kotlin @Composable fun MyComposable() { var clickCount by remember { mutableStateOf(0) } // Runs after every recomposition SideEffect { println("Recomposed! Click count: $clickCount") } Button(onClick = { clickCount++ }) { Text("Clicks: $clickCount") } } ``` ### Use Cases - Synchronizing Compose state with external systems (e.g., Analytics logging) - Updating non-Compose UI elements - One-way synchronization where cleanup isn't needed ```kotlin @Composable fun TrackScreenView(screenName: String) { SideEffect { Analytics.logScreenView(screenName) } } ``` **Do:** Use for simple, stateless synchronization. **Don't:** Use for resource allocation (use `DisposableEffect` instead). Source: `compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt` ## LaunchedEffect(key) — Coroutines Scoped to Composition `LaunchedEffect` launches a coroutine in a scope tied to the composable's lifecycle. The coroutine is cancelled if the key changes or the composable leaves composition. ```kotlin @Composable fun DataLoader(userId: String) { var data by remember { mutableStateOf(null) } // Coroutine runs when userId changes or composable enters composition LaunchedEffect(userId) { data = loadData(userId) // suspend function } Text(data ?: "Loading...") } ``` ### Key Selection ```kotlin // Key = Unit: runs once when composable enters composition, never cancels/restarts LaunchedEffect(Unit) { setupOnce() } // Key = specific value: reruns whenever the value changes var userId by remember { mutableStateOf("user1") } LaunchedEffect(userId) { loadUserData(userId) // reruns when userId changes } // Multiple keys: reruns if ANY key changes LaunchedEffect(userId, postId) { loadUserAndPost(userId, postId) } // No key parameter (not recommended): equivalent to Unit LaunchedEffect { setupOnce() } ``` ### Common Mistake: Wrong Key Selection ```kotlin // Don't: Key changes every recomposition (creates infinite loop) @Composable fun BadKeySelection() { var count by remember { mutableStateOf(0) } val randomKey = Random.nextInt() // Changes every recomposition! LaunchedEffect(randomKey) { count++ // This launches infinitely } Text("Count: $count") } // Do: Use stable keys that represent the data you depend on @Composable fun GoodKeySelection(userId: String) { var userData by remember { mutableStateOf(null) } LaunchedEffect(userId) { userData = fetchUser(userId) } Text(userData?.name ?: "Loading...") } ``` ### Cancellation Behavior ```kotlin @Composable fun ResourceUser(shouldLoad: Boolean) { LaunchedEffect(shouldLoad) { if (shouldLoad) { val resource = acquireResource() try { delay(5000) // Long operation processResource(resource) } finally { resource.close() // Runs even if cancelled } } } } // If shouldLoad becomes false, the LaunchedEffect coroutine is cancelled. // The finally block ensures cleanup. ``` ## DisposableEffect(key) — For Cleanup `DisposableEffect` runs after composition and requires a cleanup function (onDispose). Use for listeners, registrations, and resources. ```kotlin @Composable fun LocationListener(context: Context) { DisposableEffect(context) { val listener = LocationListener { location -> println("Location: $location") } // Register listener locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, 0, 0f, listener ) // Cleanup: unregister listener onDispose { locationManager.removeUpdates(listener) } } } ``` ### Common Pattern: Lifecycle Events ```kotlin @Composable fun ScreenWithLifecycle() { val lifecycle = LocalLifecycleOwner.current.lifecycle DisposableEffect(lifecycle) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> println("Screen resumed") Lifecycle.Event.ON_PAUSE -> println("Screen paused") else -> {} } } lifecycle.addObserver(observer) onDispose { lifecycle.removeObserver(observer) } } } ``` **Do:** Use `DisposableEffect` for every resource you allocate. **Don't:** Forget the `onDispose` block (resource leaks result). Source: `compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt` ## rememberCoroutineScope — Launching from Event Handlers `rememberCoroutineScope` provides a coroutine scope tied to the composable. Use it to launch coroutines from event handlers (clicks, gestures). ```kotlin @Composable fun ButtonWithAsync() { val scope = rememberCoroutineScope() var result by remember { mutableStateOf("") } Button( onClick = { // Launch coroutine from click handler scope.launch { result = fetchData() } } ) { Text("Fetch") } Text(result) } ``` ### Do vs Don't ```kotlin // Don't: regular function scope doesn't work @Composable fun BadAsync() { var result by remember { mutableStateOf("") } Button( onClick = { runBlocking { // Blocks UI thread! result = fetchData() } } ) { Text("Fetch") } } // Do: use rememberCoroutineScope @Composable fun GoodAsync() { val scope = rememberCoroutineScope() var result by remember { mutableStateOf("") } Button( onClick = { scope.launch { result = fetchData() } } ) { Text("Fetch") } } ``` ## rememberUpdatedState — Capturing Latest Values Long-running effects need the latest value of frequently-changing state, but you don't want to restart the effect on every change. ```kotlin // Don't: effect restarts when callback changes @Composable fun BadCallback(onSuccess: (String) -> Unit) { LaunchedEffect(onSuccess) { // Restarts whenever onSuccess changes! val result = expensiveOperation() onSuccess(result) } } // Do: use rememberUpdatedState to capture latest without restarting @Composable fun GoodCallback(onSuccess: (String) -> Unit) { val updatedOnSuccess = rememberUpdatedState(onSuccess) LaunchedEffect(Unit) { val result = expensiveOperation() updatedOnSuccess.value(result) } } ``` ### Another Example: Animations ```kotlin @Composable fun AnimateWithCallback( shouldAnimate: Boolean, onAnimationEnd: () -> Unit ) { val updatedCallback = rememberUpdatedState(onAnimationEnd) var progress by remember { mutableStateOf(0f) } LaunchedEffect(shouldAnimate) { if (shouldAnimate) { while (progress < 1f) { progress += 0.1f delay(16) } updatedCallback.value() // Call latest callback without restarting } } } ``` ## produceState — Converting Non-Compose State to Compose State `produceState` converts imperative state sources (callbacks, flows, coroutines) into Compose state. ```kotlin @Composable fun UserData(userId: String): State = produceState(initialValue = null) { value = fetchUser(userId) // Optional: for lifecycle cleanup snapshotFlow { userId }.collect { newUserId -> value = fetchUser(newUserId) } } // Usage @Composable fun UserScreen(userId: String) { val user by UserData(userId) Text(user?.name ?: "Loading...") } ``` ### Integration with Flows ```kotlin @Composable fun Flow.collectAsState(initial: T): State = produceState(initial) { collect { value = it } } // Usage @Composable fun ObserveFlow(dataFlow: Flow) { val data by dataFlow.collectAsState(initial = "") Text(data) } ``` ## Effect Ordering and Lifecycle Effects execute in declaration order after composition: ```kotlin @Composable fun EffectOrder() { println("1. Composition") SideEffect { println("4. Side effect (after every composition)") } LaunchedEffect(Unit) { println("3. Launched effect (async, but scheduled)") delay(100) println("5. After delay in launched effect") } DisposableEffect(Unit) { println("2. Disposable effect setup (after composition)") onDispose { println("6. Cleanup when leaving composition") } } println("End of composition body") } // Output order (approximate): // 1. Composition // End of composition body // 2. Disposable effect setup (after composition) // 3. Launched effect (async, but scheduled) // 4. Side effect (after every composition) // 5. After delay in launched effect // [... later when composable leaves ...] // 6. Cleanup when leaving composition ``` ## Common Mistakes ### Using LaunchedEffect(Unit) When Key Should Change ```kotlin // Don't: effect runs once, never updates @Composable fun BadSearch(query: String) { var results by remember { mutableStateOf>(emptyList()) } LaunchedEffect(Unit) { results = search(query) // Only runs once! } Text("Results: ${results.size}") } // Do: use query as key @Composable fun GoodSearch(query: String) { var results by remember { mutableStateOf>(emptyList()) } LaunchedEffect(query) { results = search(query) // Reruns when query changes } Text("Results: ${results.size}") } ``` ### Forgetting Cleanup in DisposableEffect ```kotlin // Don't: memory leak @Composable fun BadListener(context: Context) { DisposableEffect(Unit) { val listener = MyListener() context.registerListener(listener) // Missing: onDispose { context.unregisterListener(listener) } } } // Do: always clean up @Composable fun GoodListener(context: Context) { DisposableEffect(Unit) { val listener = MyListener() context.registerListener(listener) onDispose { context.unregisterListener(listener) } } } ``` ### Capturing Mutable State Directly ```kotlin // Don't: stale state in effect @Composable fun BadCapture() { var count by remember { mutableStateOf(0) } LaunchedEffect(Unit) { delay(1000) println(count) // May be stale! } Button(onClick = { count++ }) { Text("Click") } } // Do: use rememberUpdatedState or include in key @Composable fun GoodCapture() { var count by remember { mutableStateOf(0) } val updatedCount = rememberUpdatedState(count) LaunchedEffect(Unit) { delay(1000) println(updatedCount.value) // Always current } Button(onClick = { count++ }) { Text("Click") } } ``` --- **Summary:** Effects bridge declarative Compose with imperative systems. Master key selection in `LaunchedEffect`, always cleanup in `DisposableEffect`, use `rememberUpdatedState` for long-running effects that need fresh values, and prefer effect-based patterns over manual lifecycle management. ================================================ FILE: .claude/skills/compose-expert/references/source-code/cmp-source.md ================================================ # Compose Multiplatform Source Reference > API signatures from [JetBrains/compose-multiplatform-core](https://github.com/JetBrains/compose-multiplatform-core) (branch: jb-main). > Each signature includes its source file path as a comment. --- ## Desktop APIs ### Application Lifecycle ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Application.desktop.kt fun application( exitProcessOnExit: Boolean = true, content: @Composable ApplicationScope.() -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Application.desktop.kt suspend fun awaitApplication( content: @Composable ApplicationScope.() -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Application.desktop.kt @Stable interface ApplicationScope { fun exitApplication() } ``` ### Window ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Window.desktop.kt @Composable fun Window( onCloseRequest: () -> Unit, state: WindowState = rememberWindowState(), visible: Boolean = true, title: String = "Untitled", icon: Painter? = null, undecorated: Boolean = false, transparent: Boolean = false, resizable: Boolean = true, enabled: Boolean = true, focusable: Boolean = true, alwaysOnTop: Boolean = false, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, content: @Composable FrameWindowScope.() -> Unit ) ``` ### WindowState ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/WindowState.desktop.kt @Stable interface WindowState { var placement: WindowPlacement var isMinimized: Boolean var position: WindowPosition var size: DpSize } // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/WindowState.desktop.kt @Composable fun rememberWindowState( placement: WindowPlacement = WindowPlacement.Floating, isMinimized: Boolean = false, position: WindowPosition = WindowPosition.PlatformDefault, size: DpSize = DpSize(800.dp, 600.dp) ): WindowState // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/WindowPlacement.desktop.kt enum class WindowPlacement { Floating, Maximized, Fullscreen } // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/WindowPosition.desktop.kt @Immutable sealed class WindowPosition { object PlatformDefault : WindowPosition() class Aligned(val alignment: Alignment) : WindowPosition() class Absolute(val x: Dp, val y: Dp) : WindowPosition() } ``` ### Tray ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Tray.desktop.kt @Composable fun ApplicationScope.Tray( icon: Painter, state: TrayState = rememberTrayState(), tooltip: String? = null, onAction: () -> Unit = {}, menu: @Composable MenuScope.() -> Unit = {} ) ``` ### MenuBar and Menu Items ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/MenuBar.desktop.kt @Composable fun FrameWindowScope.MenuBar( content: @Composable MenuBarScope.() -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Menu.desktop.kt @Composable fun MenuScope.Menu( text: String, enabled: Boolean = true, mnemonic: Char? = null, content: @Composable MenuScope.() -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Menu.desktop.kt @Composable fun MenuScope.Item( text: String, icon: Painter? = null, enabled: Boolean = true, mnemonic: Char? = null, shortcut: KeyShortcut? = null, onClick: () -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Menu.desktop.kt @Composable fun MenuScope.CheckboxItem( text: String, checked: Boolean, icon: Painter? = null, enabled: Boolean = true, mnemonic: Char? = null, shortcut: KeyShortcut? = null, onCheckedChange: (Boolean) -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Menu.desktop.kt @Composable fun MenuScope.RadioButtonItem( text: String, selected: Boolean, icon: Painter? = null, enabled: Boolean = true, mnemonic: Char? = null, shortcut: KeyShortcut? = null, onClick: () -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Menu.desktop.kt @Composable fun MenuScope.Separator() ``` ### DialogWindow ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Dialog.desktop.kt @Composable fun DialogWindow( onCloseRequest: () -> Unit, state: DialogState = rememberDialogState(), visible: Boolean = true, title: String = "Untitled", icon: Painter? = null, undecorated: Boolean = false, transparent: Boolean = false, resizable: Boolean = true, enabled: Boolean = true, focusable: Boolean = true, onPreviewKeyEvent: (KeyEvent) -> Boolean = { false }, onKeyEvent: (KeyEvent) -> Boolean = { false }, content: @Composable DialogWindowScope.() -> Unit ) // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DialogState.desktop.kt @Stable interface DialogState { var position: WindowPosition var size: DpSize } // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/DialogState.desktop.kt @Composable fun rememberDialogState( position: WindowPosition = WindowPosition.PlatformDefault, size: DpSize = DpSize(400.dp, 300.dp) ): DialogState ``` ### Notification ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/window/Notification.desktop.kt class Notification( val title: String, val message: String, val type: Type = Type.None ) { enum class Type { None, Info, Warning, Error } } ``` ### Swing / AWT Interop ```kotlin // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/ComposePanel.desktop.kt class ComposePanel : JLayeredPane() { fun setContent(content: @Composable () -> Unit) fun dispose() } // Source: compose/ui/ui/src/desktopMain/kotlin/androidx/compose/ui/awt/SwingPanel.desktop.kt @Composable fun SwingPanel( factory: () -> T, modifier: Modifier = Modifier, update: (T) -> Unit = {}, background: Color = Color.White ) ``` ### Scrollbar ```kotlin // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt @Composable fun VerticalScrollbar( adapter: ScrollbarAdapter, modifier: Modifier = Modifier, reverseLayout: Boolean = false, style: ScrollbarStyle = LocalScrollbarStyle.current, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt @Composable fun HorizontalScrollbar( adapter: ScrollbarAdapter, modifier: Modifier = Modifier, reverseLayout: Boolean = false, style: ScrollbarStyle = LocalScrollbarStyle.current, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } ) // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ScrollbarStyling.desktop.kt @Immutable data class ScrollbarStyle( val minimalHeight: Dp, val thickness: Dp, val shape: Shape, val hoverDurationMillis: Int, val unhoverColor: Color, val hoverColor: Color ) // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ScrollbarStyling.desktop.kt @Composable fun defaultScrollbarStyle(): ScrollbarStyle // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/ScrollbarStyling.desktop.kt val LocalScrollbarStyle = staticCompositionLocalOf { defaultScrollbarStyle() } // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/Scrollbar.desktop.kt @Composable fun rememberScrollbarAdapter( scrollState: ScrollState ): ScrollbarAdapter @Composable fun rememberScrollbarAdapter( scrollState: LazyListState ): ScrollbarAdapter @Composable fun rememberScrollbarAdapter( scrollState: LazyGridState ): ScrollbarAdapter ``` --- ## iOS APIs ### ComposeUIViewController ```kotlin // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeUIViewController.uikit.kt fun ComposeUIViewController( content: @Composable () -> Unit ): UIViewController // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeUIViewController.uikit.kt fun ComposeUIViewController( configure: ComposeUIViewControllerConfiguration.() -> Unit = {}, content: @Composable () -> Unit ): UIViewController ``` ### ComposeUIView (Experimental) ```kotlin // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeUIView.uikit.kt @ExperimentalComposeApi fun ComposeUIView( content: @Composable () -> Unit ): UIView // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeUIView.uikit.kt @ExperimentalComposeApi fun ComposeUIView( configure: ComposeUIViewControllerConfiguration.() -> Unit = {}, content: @Composable () -> Unit ): UIView ``` ### ComposeContainerConfiguration ```kotlin // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/ComposeContainerConfiguration.uikit.kt class ComposeUIViewControllerConfiguration { var onFocusBehavior: OnFocusBehavior = OnFocusBehavior.FocusableAboveKeyboard var opaque: Boolean = true var parallelRendering: Boolean = false var endEdgePanGestureBehavior: EndEdgePanGestureBehavior = EndEdgePanGestureBehavior.Disabled } // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/OnFocusBehavior.uikit.kt sealed interface OnFocusBehavior { object DoNothing : OnFocusBehavior object FocusableAboveKeyboard : OnFocusBehavior } // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/window/EndEdgePanGestureBehavior.uikit.kt sealed interface EndEdgePanGestureBehavior { object Disabled : EndEdgePanGestureBehavior object Back : EndEdgePanGestureBehavior object Forward : EndEdgePanGestureBehavior } ``` ### UIKit Interop ```kotlin // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @Composable fun UIKitView( factory: () -> T, modifier: Modifier = Modifier, update: (T) -> Unit = {}, onRelease: (T) -> Unit = {}, onReset: ((T) -> Unit)? = null, properties: UIKitInteropProperties = UIKitInteropProperties() ) // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitView.uikit.kt @Composable fun UIKitViewController( factory: () -> T, modifier: Modifier = Modifier, update: (T) -> Unit = {}, onRelease: (T) -> Unit = {}, onReset: ((T) -> Unit)? = null, properties: UIKitInteropProperties = UIKitInteropProperties() ) // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropProperties.uikit.kt @Immutable class UIKitInteropProperties( val interactionMode: UIKitInteropInteractionMode = UIKitInteropInteractionMode.Cooperative(), val isNativeAccessibilityEnabled: Boolean = true, val placedAsOverlay: Boolean = false ) // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/UIKitInteropInteractionMode.uikit.kt sealed interface UIKitInteropInteractionMode { object NonCooperative : UIKitInteropInteractionMode class Cooperative(val delayMillis: Long = 150) : UIKitInteropInteractionMode } ``` ### iOS CompositionLocals ```kotlin // Source: compose/ui/ui/src/uikitMain/kotlin/androidx/compose/ui/interop/LocalUIKitInterop.uikit.kt val LocalUIViewController: ProvidableCompositionLocal = staticCompositionLocalOf { error("CompositionLocal LocalUIViewController not provided") } val LocalUIView: ProvidableCompositionLocal = staticCompositionLocalOf { error("CompositionLocal LocalUIView not provided") } ``` --- ## Web / WASM APIs ### ComposeViewport ```kotlin // Source: compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/window/ComposeViewport.wasmJs.kt fun ComposeViewport( viewportContainerId: String, configure: ComposeViewportConfiguration.() -> Unit = {}, content: @Composable () -> Unit ) // Source: compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/window/ComposeViewport.wasmJs.kt fun ComposeViewport( viewportContainer: Element, configure: ComposeViewportConfiguration.() -> Unit = {}, content: @Composable () -> Unit ) // Source: compose/ui/ui/src/wasmJsMain/kotlin/androidx/compose/ui/window/ComposeViewportConfiguration.wasmJs.kt class ComposeViewportConfiguration { var isA11YEnabled: Boolean = true } ``` --- ## Shared / Skiko APIs ### System Theme ```kotlin // Source: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.skiko.kt enum class SystemTheme { Light, Dark, Unknown } // Source: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/SystemTheme.skiko.kt val LocalSystemTheme: ProvidableCompositionLocal = staticCompositionLocalOf { SystemTheme.Unknown } ``` ### ComposeScene ```kotlin // Source: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ComposeScene.skiko.kt sealed interface ComposeScene : AutoCloseable { val focusManager: FocusManager var density: Density var layoutDirection: LayoutDirection var size: IntSize? fun setContent(content: @Composable () -> Unit) fun render(canvas: Canvas, nanoTime: Long) fun sendPointerEvent(/* ... */) fun sendKeyEvent(keyEvent: KeyEvent): Boolean override fun close() } ``` ### ImageComposeScene (Headless Rendering) ```kotlin // Source: compose/ui/ui/src/skikoMain/kotlin/androidx/compose/ui/scene/ImageComposeScene.skiko.kt fun ImageComposeScene( width: Int, height: Int, density: Density = Density(1f), coroutineContext: CoroutineContext = Dispatchers.Unconfined, content: @Composable () -> Unit = {} ): ComposeScene ``` ### isSystemInDarkTheme (expect/actual) ```kotlin // Source: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/DarkTheme.kt @Composable expect fun isSystemInDarkTheme(): Boolean // Source: compose/foundation/foundation/src/desktopMain/kotlin/androidx/compose/foundation/DarkTheme.desktop.kt @Composable actual fun isSystemInDarkTheme(): Boolean = LocalSystemTheme.current == SystemTheme.Dark // Source: compose/foundation/foundation/src/uikitMain/kotlin/androidx/compose/foundation/DarkTheme.uikit.kt @Composable actual fun isSystemInDarkTheme(): Boolean { /* UIScreen.mainScreen traitCollection check */ } // Source: compose/foundation/foundation/src/wasmJsMain/kotlin/androidx/compose/foundation/DarkTheme.wasmJs.kt @Composable actual fun isSystemInDarkTheme(): Boolean { /* window.matchMedia("(prefers-color-scheme: dark)") */ } ``` --- ## Resource System ### Resource Types ```kotlin // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/Resource.kt @Immutable sealed class Resource( internal val id: String, internal val items: Set ) // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceItem.kt @Immutable class ResourceItem( internal val qualifiers: Set, internal val path: String, internal val offset: Long, internal val size: Long ) ``` ### ResourceReader (Platform Actuals) ```kotlin // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceReader.kt interface ResourceReader { suspend fun read(path: String): ByteArray suspend fun readPart(path: String, offset: Long, size: Long): ByteArray fun getUri(path: String): String } // Platform actuals: // Source: components/resources/library/src/jvmMain/kotlin/org/jetbrains/compose/resources/ResourceReader.jvm.kt // Source: components/resources/library/src/iosMain/kotlin/org/jetbrains/compose/resources/ResourceReader.ios.kt // Source: components/resources/library/src/wasmJsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.wasmJs.kt // Source: components/resources/library/src/jsMain/kotlin/org/jetbrains/compose/resources/ResourceReader.js.kt ``` ### rememberResourceState (expect/actual) ```kotlin // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt @Composable expect fun rememberResourceState( key1: Any, getDefault: () -> T, block: suspend (ResourceEnvironment) -> T ): State // JVM/iOS actual: synchronous (blocking read, returns value immediately) // Source: components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt // JS/WASM actual: asynchronous (suspending read, returns default then updates) // Source: components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt ``` ### Resource Composables ```kotlin // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/StringResources.kt @Composable fun stringResource(resource: StringResource): String @Composable fun stringResource(resource: StringResource, vararg formatArgs: Any): String // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ImageResources.kt @Composable fun painterResource(resource: DrawableResource): Painter @Composable fun imageResource(resource: DrawableResource): ImageBitmap // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/VectorResources.kt @Composable fun vectorResource(resource: DrawableResource): ImageVector // Source: components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt @Composable expect fun Font( resource: FontResource, weight: FontWeight = FontWeight.Normal, style: FontStyle = FontStyle.Normal ): Font ``` ================================================ FILE: .claude/skills/compose-expert/references/source-code/foundation-source.md ================================================ # Compose Foundation Source Reference ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Clickable.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.foundation import androidx.collection.mutableLongObjectMapOf import androidx.compose.foundation.ComposeFoundationFlags.isDelayPressesUsingGestureConsumptionEnabled import androidx.compose.foundation.gestures.PressGestureScope import androidx.compose.foundation.gestures.ScrollableContainerNode import androidx.compose.foundation.gestures.changedToDownIgnoreConsumed import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.isChangedToDown import androidx.compose.foundation.gestures.isDeepPress import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.internal.requirePrecondition import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.focus.Focusability import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.indirect.IndirectPointerEvent import androidx.compose.ui.input.indirect.IndirectPointerInputChange import androidx.compose.ui.input.indirect.IndirectPointerInputModifierNode import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType.Companion.KeyDown import androidx.compose.ui.input.key.KeyEventType.Companion.KeyUp import androidx.compose.ui.input.key.KeyInputModifierNode import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode import androidx.compose.ui.input.pointer.changedToUp import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed import androidx.compose.ui.input.pointer.isOutOfBounds import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.node.SemanticsModifierNode import androidx.compose.ui.node.TraversableNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.node.invalidateSemantics import androidx.compose.ui.node.observeReads import androidx.compose.ui.node.requireDensity import androidx.compose.ui.node.traverseAncestors import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.disabled import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.onLongClick import androidx.compose.ui.semantics.role import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.center import androidx.compose.ui.unit.toOffset import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastForEach import kotlin.math.abs import kotlin.math.max import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch /** * Configure component to receive clicks via input or accessibility "click" event. * * Add this modifier to the element to make it clickable within its bounds and show a default * indication when it's pressed. * * This version has no [MutableInteractionSource] or [Indication] parameters, the default indication * from [LocalIndication] will be used. To specify [MutableInteractionSource] or [Indication], use * the other overload. * * If you are only creating this clickable modifier inside composition, consider using the other * overload and explicitly passing `LocalIndication.current` for improved performance. For more * information see the documentation on the other overload. * * If you need to support double click or long click alongside the single click, consider using * [combinedClickable]. * * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do * not need to do this when removing a composable because Compose guarantees it completes via the * snapshot state system.) * * @sample androidx.compose.foundation.samples.ClickableSample * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will appear * disabled for accessibility services * @param onClickLabel semantic / accessibility label for the [onClick] action * @param role the type of user interface element. Accessibility services might use this to describe * the element or do customizations * @param onClick will be called when user clicks on the element */ @Deprecated( message = "Replaced with new overload that only supports IndicationNodeFactory instances inside LocalIndication, and does not use composed", level = DeprecationLevel.HIDDEN, ) fun Modifier.clickable( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onClick: () -> Unit, ) = composed( inspectorInfo = debugInspectorInfo { name = "clickable" properties["enabled"] = enabled properties["onClickLabel"] = onClickLabel properties["role"] = role properties["onClick"] = onClick } ) { val localIndication = LocalIndication.current val interactionSource = if (localIndication is IndicationNodeFactory) { // We can fast path here as it will be created inside clickable lazily null } else { // We need an interaction source to pass between the indication modifier and // clickable, so // by creating here we avoid another composed down the line remember { MutableInteractionSource() } } Modifier.clickable( enabled = enabled, onClickLabel = onClickLabel, onClick = onClick, role = role, indication = localIndication, interactionSource = interactionSource, ) } /** * Configure component to receive clicks via input or accessibility "click" event. * * Add this modifier to the element to make it clickable within its bounds and show a default * indication when it's pressed. * * This overload will use the [Indication] from [LocalIndication]. Use the other overload to * explicitly provide an [Indication] instance. Note that this overload only supports * [IndicationNodeFactory] instances provided through [LocalIndication] - it is strongly recommended * to migrate to [IndicationNodeFactory], but you can use the other overload if you still need to * support [Indication] instances that are not [IndicationNodeFactory]. * * If [interactionSource] is `null`, an internal [MutableInteractionSource] will be lazily created * only when needed. This reduces the performance cost of clickable during composition, as creating * the [indication] can be delayed until there is an incoming * [androidx.compose.foundation.interaction.Interaction]. If you are only passing a remembered * [MutableInteractionSource] and you are never using it outside of clickable, it is recommended to * instead provide `null` to enable lazy creation. If you need the [Indication] to be created * eagerly, provide a remembered [MutableInteractionSource]. * * If you need to support double click or long click alongside the single click, consider using * [combinedClickable]. * * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do * not need to do this when removing a composable because Compose guarantees it completes via the * snapshot state system.) * * @sample androidx.compose.foundation.samples.ClickableSample * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will appear * disabled for accessibility services * @param onClickLabel semantic / accessibility label for the [onClick] action * @param role the type of user interface element. Accessibility services might use this to describe * the element or do customizations * @param interactionSource [MutableInteractionSource] that will be used to dispatch * [PressInteraction.Press] when this clickable is pressed. If `null`, an internal * [MutableInteractionSource] will be created if needed. * @param onClick will be called when user clicks on the element */ fun Modifier.clickable( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, interactionSource: MutableInteractionSource? = null, onClick: () -> Unit, ): Modifier { return this.then( ClickableElement( interactionSource = interactionSource, indicationNodeFactory = null, useLocalIndication = true, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) ) } /** * Configure component to receive clicks via input or accessibility "click" event. * * Add this modifier to the element to make it clickable within its bounds and show an indication as * specified in [indication] parameter. * * If [interactionSource] is `null`, and [indication] is an [IndicationNodeFactory], an internal * [MutableInteractionSource] will be lazily created along with the [indication] only when needed. * This reduces the performance cost of clickable during composition, as creating the [indication] * can be delayed until there is an incoming [androidx.compose.foundation.interaction.Interaction]. * If you are only passing a remembered [MutableInteractionSource] and you are never using it * outside of clickable, it is recommended to instead provide `null` to enable lazy creation. If you * need [indication] to be created eagerly, provide a remembered [MutableInteractionSource]. * * If [indication] is _not_ an [IndicationNodeFactory], and instead implements the deprecated * [Indication.rememberUpdatedInstance] method, you should explicitly pass a remembered * [MutableInteractionSource] as a parameter for [interactionSource] instead of `null`, as this * cannot be lazily created inside clickable. * * If you need to support double click or long click alongside the single click, consider using * [combinedClickable]. * * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do * not need to do this when removing a composable because Compose guarantees it completes via the * snapshot state system.) * * @sample androidx.compose.foundation.samples.ClickableSample * @param interactionSource [MutableInteractionSource] that will be used to dispatch * [PressInteraction.Press] when this clickable is pressed. If `null`, an internal * [MutableInteractionSource] will be created if needed. * @param indication indication to be shown when modified element is pressed. By default, indication * from [LocalIndication] will be used. Pass `null` to show no indication, or current value from * [LocalIndication] to show theme default * @param enabled Controls the enabled state. When `false`, [onClick], and this modifier will appear * disabled for accessibility services * @param onClickLabel semantic / accessibility label for the [onClick] action * @param role the type of user interface element. Accessibility services might use this to describe * the element or do customizations * @param onClick will be called when user clicks on the element */ fun Modifier.clickable( interactionSource: MutableInteractionSource?, indication: Indication?, enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onClick: () -> Unit, ) = clickableWithIndicationIfNeeded( interactionSource = interactionSource, indication = indication, ) { intSource, indicationNodeFactory -> ClickableElement( interactionSource = intSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = false, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) } /** * Configure component to receive clicks, double clicks and long clicks via input or accessibility * "click" event. * * Add this modifier to the element to make it clickable within its bounds. * * If you need only click handling, and no double or long clicks, consider using [clickable] * * This version has no [MutableInteractionSource] or [Indication] parameters, the default indication * from [LocalIndication] will be used. To specify [MutableInteractionSource] or [Indication], use * the other overload. * * If you are only creating this combinedClickable modifier inside composition, consider using the * other overload and explicitly passing `LocalIndication.current` for improved performance. For * more information see the documentation on the other overload. * * Note, if the modifier instance gets re-used between a key down and key up events, the ongoing * input will be aborted. * * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do * not need to do this when removing a composable because Compose guarantees it completes via the * snapshot state system.) * * @sample androidx.compose.foundation.samples.ClickableSample * @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or * [onDoubleClick] won't be invoked * @param onClickLabel semantic / accessibility label for the [onClick] action * @param role the type of user interface element. Accessibility services might use this to describe * the element or do customizations * @param onLongClickLabel semantic / accessibility label for the [onLongClick] action * @param onLongClick will be called when user long presses on the element * @param onDoubleClick will be called when user double clicks on the element * @param hapticFeedbackEnabled whether to use the default [HapticFeedback] behavior * @param onClick will be called when user clicks on the element */ @Deprecated( message = "Replaced with new overload that only supports IndicationNodeFactory instances inside LocalIndication, and does not use composed", level = DeprecationLevel.HIDDEN, ) fun Modifier.combinedClickable( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: (() -> Unit)? = null, onDoubleClick: (() -> Unit)? = null, hapticFeedbackEnabled: Boolean = true, onClick: () -> Unit, ) = composed( inspectorInfo = debugInspectorInfo { name = "combinedClickable" properties["enabled"] = enabled properties["onClickLabel"] = onClickLabel properties["role"] = role properties["onClick"] = onClick properties["onDoubleClick"] = onDoubleClick properties["onLongClick"] = onLongClick properties["onLongClickLabel"] = onLongClickLabel properties["hapticFeedbackEnabled"] = hapticFeedbackEnabled } ) { val localIndication = LocalIndication.current val interactionSource = if (localIndication is IndicationNodeFactory) { // We can fast path here as it will be created inside clickable lazily null } else { // We need an interaction source to pass between the indication modifier and // clickable, so // by creating here we avoid another composed down the line remember { MutableInteractionSource() } } Modifier.combinedClickable( enabled = enabled, onClickLabel = onClickLabel, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick, onDoubleClick = onDoubleClick, onClick = onClick, role = role, indication = localIndication, interactionSource = interactionSource, hapticFeedbackEnabled = hapticFeedbackEnabled, ) } /** * Configure component to receive clicks, double clicks and long clicks via input or accessibility * "click" event. * * Add this modifier to the element to make it clickable within its bounds. * * If you need only click handling, and no double or long clicks, consider using [clickable] * * This overload will use the [Indication] from [LocalIndication]. Use the other overload to * explicitly provide an [Indication] instance. Note that this overload only supports * [IndicationNodeFactory] instances provided through [LocalIndication] - it is strongly recommended * to migrate to [IndicationNodeFactory], but you can use the other overload if you still need to * support [Indication] instances that are not [IndicationNodeFactory]. * * If [interactionSource] is `null`, an internal [MutableInteractionSource] will be lazily created * only when needed. This reduces the performance cost of combinedClickable during composition, as * creating the [indication] can be delayed until there is an incoming * [androidx.compose.foundation.interaction.Interaction]. If you are only passing a remembered * [MutableInteractionSource] and you are never using it outside of combinedClickable, it is * recommended to instead provide `null` to enable lazy creation. If you need the [Indication] to be * created eagerly, provide a remembered [MutableInteractionSource]. * * Note, if the modifier instance gets re-used between a key down and key up events, the ongoing * input will be aborted. * * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do * not need to do this when removing a composable because Compose guarantees it completes via the * snapshot state system.) * * @sample androidx.compose.foundation.samples.ClickableSample * @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or * [onDoubleClick] won't be invoked * @param onClickLabel semantic / accessibility label for the [onClick] action * @param role the type of user interface element. Accessibility services might use this to describe * the element or do customizations * @param onLongClickLabel semantic / accessibility label for the [onLongClick] action * @param onLongClick will be called when user long presses on the element * @param onDoubleClick will be called when user double clicks on the element * @param hapticFeedbackEnabled whether to use the default [HapticFeedback] behavior * @param interactionSource [MutableInteractionSource] that will be used to dispatch * [PressInteraction.Press] when this clickable is pressed. If `null`, an internal * [MutableInteractionSource] will be created if needed. * @param onClick will be called when user clicks on the element */ fun Modifier.combinedClickable( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: (() -> Unit)? = null, onDoubleClick: (() -> Unit)? = null, hapticFeedbackEnabled: Boolean = true, interactionSource: MutableInteractionSource? = null, onClick: () -> Unit, ): Modifier { return this.then( CombinedClickableElement( enabled = enabled, onClickLabel = onClickLabel, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick, onDoubleClick = onDoubleClick, onClick = onClick, role = role, interactionSource = interactionSource, indicationNodeFactory = null, useLocalIndication = true, hapticFeedbackEnabled = hapticFeedbackEnabled, ) ) } @Deprecated(message = "Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) fun Modifier.combinedClickable( enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: (() -> Unit)? = null, onDoubleClick: (() -> Unit)? = null, onClick: () -> Unit, ) = composed( inspectorInfo = debugInspectorInfo { name = "combinedClickable" properties["enabled"] = enabled properties["onClickLabel"] = onClickLabel properties["role"] = role properties["onClick"] = onClick properties["onDoubleClick"] = onDoubleClick properties["onLongClick"] = onLongClick properties["onLongClickLabel"] = onLongClickLabel } ) { val localIndication = LocalIndication.current val interactionSource = if (localIndication is IndicationNodeFactory) { // We can fast path here as it will be created inside clickable lazily null } else { // We need an interaction source to pass between the indication modifier and // clickable, so // by creating here we avoid another composed down the line remember { MutableInteractionSource() } } Modifier.combinedClickable( enabled = enabled, onClickLabel = onClickLabel, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick, onDoubleClick = onDoubleClick, onClick = onClick, role = role, indication = localIndication, interactionSource = interactionSource, hapticFeedbackEnabled = true, ) } /** * Configure component to receive clicks, double clicks and long clicks via input or accessibility * "click" event. * * Add this modifier to the element to make it clickable within its bounds. * * If you need only click handling, and no double or long clicks, consider using [clickable]. * * Add this modifier to the element to make it clickable within its bounds. * * If [interactionSource] is `null`, and [indication] is an [IndicationNodeFactory], an internal * [MutableInteractionSource] will be lazily created along with the [indication] only when needed. * This reduces the performance cost of clickable during composition, as creating the [indication] * can be delayed until there is an incoming [androidx.compose.foundation.interaction.Interaction]. * If you are only passing a remembered [MutableInteractionSource] and you are never using it * outside of clickable, it is recommended to instead provide `null` to enable lazy creation. If you * need [indication] to be created eagerly, provide a remembered [MutableInteractionSource]. * * If [indication] is _not_ an [IndicationNodeFactory], and instead implements the deprecated * [Indication.rememberUpdatedInstance] method, you should explicitly pass a remembered * [MutableInteractionSource] as a parameter for [interactionSource] instead of `null`, as this * cannot be lazily created inside clickable. * * Note, if the modifier instance gets re-used between a key down and key up events, the ongoing * input will be aborted. * * ***Note*** Any removal operations on Android Views from `clickable` should wrap `onClick` in a * `post { }` block to guarantee the event dispatch completes before executing the removal. (You do * not need to do this when removing a composable because Compose guarantees it completes via the * snapshot state system.) * * @sample androidx.compose.foundation.samples.ClickableSample * @param interactionSource [MutableInteractionSource] that will be used to emit * [PressInteraction.Press] when this clickable is pressed. If `null`, an internal * [MutableInteractionSource] will be created if needed. * @param indication indication to be shown when modified element is pressed. By default, indication * from [LocalIndication] will be used. Pass `null` to show no indication, or current value from * [LocalIndication] to show theme default * @param enabled Controls the enabled state. When `false`, [onClick], [onLongClick] or * [onDoubleClick] won't be invoked * @param onClickLabel semantic / accessibility label for the [onClick] action * @param role the type of user interface element. Accessibility services might use this to describe * the element or do customizations * @param onLongClickLabel semantic / accessibility label for the [onLongClick] action * @param onLongClick will be called when user long presses on the element * @param onDoubleClick will be called when user double clicks on the element * @param hapticFeedbackEnabled whether to use the default [HapticFeedback] behavior * @param onClick will be called when user clicks on the element */ fun Modifier.combinedClickable( interactionSource: MutableInteractionSource?, indication: Indication?, enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: (() -> Unit)? = null, onDoubleClick: (() -> Unit)? = null, hapticFeedbackEnabled: Boolean = true, onClick: () -> Unit, ) = clickableWithIndicationIfNeeded( interactionSource = interactionSource, indication = indication, ) { intSource, indicationNodeFactory -> CombinedClickableElement( interactionSource = intSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = false, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick, onDoubleClick = onDoubleClick, hapticFeedbackEnabled = hapticFeedbackEnabled, ) } @Deprecated(message = "Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) fun Modifier.combinedClickable( interactionSource: MutableInteractionSource?, indication: Indication?, enabled: Boolean = true, onClickLabel: String? = null, role: Role? = null, onLongClickLabel: String? = null, onLongClick: (() -> Unit)? = null, onDoubleClick: (() -> Unit)? = null, onClick: () -> Unit, ) = clickableWithIndicationIfNeeded( interactionSource = interactionSource, indication = indication, ) { intSource, indicationNodeFactory -> CombinedClickableElement( interactionSource = intSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = false, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick, onDoubleClick = onDoubleClick, hapticFeedbackEnabled = true, ) } /** * Utility Modifier factory that handles edge cases for [interactionSource], and [indication]. * [createClickable] is the lambda that creates the actual clickable element, which will be chained * with [Modifier.indication] if needed. */ internal inline fun Modifier.clickableWithIndicationIfNeeded( interactionSource: MutableInteractionSource?, indication: Indication?, crossinline createClickable: (MutableInteractionSource?, IndicationNodeFactory?) -> Modifier, ): Modifier { return this.then( when { // Fast path - indication is managed internally indication is IndicationNodeFactory -> createClickable(interactionSource, indication) // Fast path - no need for indication indication == null -> createClickable(interactionSource, null) // Non-null Indication (not IndicationNodeFactory) with a non-null InteractionSource interactionSource != null -> Modifier.indication(interactionSource, indication) .then(createClickable(interactionSource, null)) // Non-null Indication (not IndicationNodeFactory) with a null InteractionSource, so we // need // to use composed to create an InteractionSource that can be shared. This should be a // rare // code path and can only be hit from new callers. else -> Modifier.composed { val newInteractionSource = remember { MutableInteractionSource() } Modifier.indication(newInteractionSource, indication) .then(createClickable(newInteractionSource, null)) } } ) } /** * How long to wait before appearing 'pressed' (emitting [PressInteraction.Press]) - if a touch down * will quickly become a drag / scroll, this timeout means that we don't show a press effect. */ internal expect val TapIndicationDelay: Long /** * Returns whether the root Compose layout node is hosted in a scrollable container outside of * Compose. On Android this will be whether the root View is in a scrollable ViewGroup, as even if * nothing in the Compose part of the hierarchy is scrollable, if the View itself is in a scrollable * container, we still want to delay presses in case presses in Compose convert to a scroll outside * of Compose. * * Combine this with [hasScrollableContainer], which returns whether a [Modifier] is within a * scrollable Compose layout, to calculate whether this modifier is within some form of scrollable * container, and hence should delay presses. */ internal expect fun DelegatableNode.isComposeRootInScrollableContainer(): Boolean /** * Whether the specified [KeyEvent] should trigger a press for a clickable component, i.e. whether * it is associated with a press of an enter key or dpad centre. */ private val KeyEvent.isPress: Boolean get() = type == KeyDown && isEnter /** * Whether the specified [KeyEvent] should trigger a click for a clickable component, i.e. whether * it is associated with a release of an enter key or dpad centre. */ private val KeyEvent.isClick: Boolean get() = type == KeyUp && isEnter private val KeyEvent.isEnter: Boolean get() = when (key) { Key.DirectionCenter, Key.Enter, Key.NumPadEnter, Key.Spacebar -> true else -> false } private class ClickableElement( private val interactionSource: MutableInteractionSource?, private val indicationNodeFactory: IndicationNodeFactory?, private val useLocalIndication: Boolean, private val enabled: Boolean, private val onClickLabel: String?, private val role: Role?, private val onClick: () -> Unit, ) : ModifierNodeElement() { override fun create() = ClickableNode( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) override fun update(node: ClickableNode) { node.update( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) } override fun InspectorInfo.inspectableProperties() { name = "clickable" properties["enabled"] = enabled properties["onClick"] = onClick properties["onClickLabel"] = onClickLabel properties["role"] = role properties["interactionSource"] = interactionSource properties["indicationNodeFactory"] = indicationNodeFactory } override fun equals(other: Any?): Boolean { if (this === other) return true if (other === null) return false if (this::class != other::class) return false other as ClickableElement if (interactionSource != other.interactionSource) return false if (indicationNodeFactory != other.indicationNodeFactory) return false if (useLocalIndication != other.useLocalIndication) return false if (enabled != other.enabled) return false if (onClickLabel != other.onClickLabel) return false if (role != other.role) return false if (onClick !== other.onClick) return false return true } override fun hashCode(): Int { var result = (interactionSource?.hashCode() ?: 0) result = 31 * result + (indicationNodeFactory?.hashCode() ?: 0) result = 31 * result + useLocalIndication.hashCode() result = 31 * result + enabled.hashCode() result = 31 * result + (onClickLabel?.hashCode() ?: 0) result = 31 * result + (role?.hashCode() ?: 0) result = 31 * result + onClick.hashCode() return result } } private class CombinedClickableElement( private val interactionSource: MutableInteractionSource?, private val indicationNodeFactory: IndicationNodeFactory?, private val useLocalIndication: Boolean, private val enabled: Boolean, private val onClickLabel: String?, private val role: Role?, private val onClick: () -> Unit, private val onLongClickLabel: String?, private val onLongClick: (() -> Unit)?, private val onDoubleClick: (() -> Unit)?, private val hapticFeedbackEnabled: Boolean, ) : ModifierNodeElement() { override fun create() = CombinedClickableNode( onClick = onClick, onLongClickLabel = onLongClickLabel, onLongClick = onLongClick, onDoubleClick = onDoubleClick, hapticFeedbackEnabled = hapticFeedbackEnabled, interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, ) override fun update(node: CombinedClickableNode) { node.hapticFeedbackEnabled = hapticFeedbackEnabled node.update( onClick, onLongClickLabel, onLongClick, onDoubleClick, interactionSource, indicationNodeFactory, useLocalIndication, enabled, onClickLabel, role, ) } override fun InspectorInfo.inspectableProperties() { name = "combinedClickable" properties["indicationNodeFactory"] = indicationNodeFactory properties["interactionSource"] = interactionSource properties["enabled"] = enabled properties["onClickLabel"] = onClickLabel properties["role"] = role properties["onClick"] = onClick properties["onDoubleClick"] = onDoubleClick properties["onLongClick"] = onLongClick properties["onLongClickLabel"] = onLongClickLabel properties["hapticFeedbackEnabled"] = hapticFeedbackEnabled } override fun equals(other: Any?): Boolean { if (this === other) return true if (other === null) return false if (this::class != other::class) return false other as CombinedClickableElement if (interactionSource != other.interactionSource) return false if (indicationNodeFactory != other.indicationNodeFactory) return false if (useLocalIndication != other.useLocalIndication) return false if (enabled != other.enabled) return false if (onClickLabel != other.onClickLabel) return false if (role != other.role) return false if (onClick !== other.onClick) return false if (onLongClickLabel != other.onLongClickLabel) return false if (onLongClick !== other.onLongClick) return false if (onDoubleClick !== other.onDoubleClick) return false if (hapticFeedbackEnabled != other.hapticFeedbackEnabled) return false return true } override fun hashCode(): Int { var result = (interactionSource?.hashCode() ?: 0) result = 31 * result + (indicationNodeFactory?.hashCode() ?: 0) result = 31 * result + useLocalIndication.hashCode() result = 31 * result + enabled.hashCode() result = 31 * result + (onClickLabel?.hashCode() ?: 0) result = 31 * result + (role?.hashCode() ?: 0) result = 31 * result + onClick.hashCode() result = 31 * result + (onLongClickLabel?.hashCode() ?: 0) result = 31 * result + (onLongClick?.hashCode() ?: 0) result = 31 * result + (onDoubleClick?.hashCode() ?: 0) result = 31 * result + hapticFeedbackEnabled.hashCode() return result } } internal open class ClickableNode( interactionSource: MutableInteractionSource?, indicationNodeFactory: IndicationNodeFactory?, useLocalIndication: Boolean, enabled: Boolean, onClickLabel: String?, role: Role?, onClick: () -> Unit, ) : AbstractClickableNode( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) { private var downEvent: PointerInputChange? = null private var indirectDownEvent: IndirectPointerInputChange? = null @OptIn(ExperimentalFoundationApi::class) override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { super.onPointerEvent(pointerEvent, pass, bounds) if (pass == PointerEventPass.Main) { if (downEvent == null) { if (pointerEvent.isChangedToDown(requireUnconsumed = true)) { handleDownEvent(pointerEvent.changes[0]) } } else { if (pointerEvent.changes.fastAll { it.changedToUp() }) { // All pointers are up handleUpEvent(pointerEvent.changes[0]) } else { // Other events need to be checked for consumption / bounds related // cancellation. handleNonUpEventIfNeeded(pointerEvent, bounds) } } } else if (pass == PointerEventPass.Final) { checkForCancellation(pointerEvent) } } override fun onIndirectPointerEvent(event: IndirectPointerEvent, pass: PointerEventPass) { super.onIndirectPointerEvent(event, pass) if (pass == PointerEventPass.Main) { if (indirectDownEvent == null) { if (event.changes.fastAny { it.changedToDownIgnoreConsumed() }) { handleDownEvent(event.changes[0]) } } else { if (event.changes.fastAll { it.changedToUp() }) { // All pointers are up handleUpEvent(event.changes[0]) } else { // Other events need to be checked for consumption / exceeding touch slop handleNonUpEventIfNeeded(event) } } } else if (pass == PointerEventPass.Final) { checkForCancellation(event) } } @OptIn(ExperimentalFoundationApi::class) private fun handleDownEvent(down: PointerInputChange) { down.consume() this.downEvent = down if (enabled) { if (isDelayPressesUsingGestureConsumptionEnabled) { handlePressInteractionStart(down) } else { handlePressInteractionStart(down.position, false) } } } @OptIn(ExperimentalFoundationApi::class) private fun handleDownEvent(down: IndirectPointerInputChange) { down.consume() this.indirectDownEvent = down if (enabled) { if (isDelayPressesUsingGestureConsumptionEnabled) { handlePressInteractionStart(down) } else { handlePressInteractionStart(down.position, true) } } } private fun handleUpEvent(up: PointerInputChange) { up.consume() if (enabled) { handlePressInteractionRelease(downEvent!!.position, indirectPointer = false) onClick() } this.downEvent = null } private fun handleUpEvent(up: IndirectPointerInputChange) { up.consume() if (enabled) { handlePressInteractionRelease(indirectDownEvent!!.position, indirectPointer = true) onClick() } this.indirectDownEvent = null } private fun handleNonUpEventIfNeeded(pointerEvent: PointerEvent, bounds: IntSize) { val touchPadding = getExtendedTouchPadding(bounds) if ( pointerEvent.changes.fastAny { it.isConsumed || it.isOutOfBounds(bounds, touchPadding) } ) { cancelInput(indirectPointer = false) } } private fun handleNonUpEventIfNeeded(indirectPointerEvent: IndirectPointerEvent) { val touchSlop = currentValueOf(LocalViewConfiguration).touchSlop if ( indirectPointerEvent.changes.fastAny { val distanceFromPress = it.position - indirectDownEvent!!.position val isOutOfBounds = abs(distanceFromPress.getDistance()) > touchSlop it.isConsumed || isOutOfBounds } ) { cancelInput(indirectPointer = true) } } private fun checkForCancellation(pointerEvent: PointerEvent) { if (downEvent != null) { // Check for cancel by position consumption. We can look on the Final pass of the // existing pointer event because it comes after the pass we checked above. if (pointerEvent.changes.fastAny { it.isConsumed && it != downEvent }) { cancelInput(indirectPointer = false) } } } private fun checkForCancellation(indirectPointerEvent: IndirectPointerEvent) { if (indirectDownEvent != null) { // Check for cancel by position consumption. We can look on the Final pass of the // existing pointer event because it comes after the pass we checked above. if (indirectPointerEvent.changes.fastAny { it.isConsumed && it != indirectDownEvent }) { cancelInput(indirectPointer = true) } } } override fun onCancelPointerInput() { super.onCancelPointerInput() cancelInput(indirectPointer = false) } override fun onCancelIndirectPointerInput() { cancelInput(indirectPointer = true) } private fun cancelInput(indirectPointer: Boolean) { // Don't cancel pointer events when cancelling indirect events (because of losing focus for // example), and vice versa. if (indirectPointer) { indirectDownEvent = null } else { downEvent = null } handlePressInteractionCancel(indirectPointer = indirectPointer) } fun update( interactionSource: MutableInteractionSource?, indicationNodeFactory: IndicationNodeFactory?, useLocalIndication: Boolean, enabled: Boolean, onClickLabel: String?, role: Role?, onClick: () -> Unit, ) { // enabled and onClick are captured inside callbacks, not as an input to detectTapGestures, // so no need to reset pointer input handling when they change updateCommon( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) } final override fun onClickKeyDownEvent(event: KeyEvent) = false final override fun onClickKeyUpEvent(event: KeyEvent): Boolean { onClick() return true } } private class CombinedClickableNode( onClick: () -> Unit, private var onLongClickLabel: String?, private var onLongClick: (() -> Unit)?, private var onDoubleClick: (() -> Unit)?, var hapticFeedbackEnabled: Boolean, interactionSource: MutableInteractionSource?, indicationNodeFactory: IndicationNodeFactory?, useLocalIndication: Boolean, enabled: Boolean, onClickLabel: String?, role: Role?, ) : CompositionLocalConsumerModifierNode, AbstractClickableNode( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) { class DoubleKeyClickState(val job: Job) { var doubleTapMinTimeMillisElapsed: Boolean = false } private val longKeyPressJobs = mutableLongObjectMapOf() private val doubleKeyClickStates = mutableLongObjectMapOf() @OptIn(ExperimentalFoundationApi::class) private val isSuspendingPointerInputEnabled = !ComposeFoundationFlags.isNonSuspendingPointerInputInCombinedClickableEnabled private var downEvent: PointerInputChange? = null private var longPressJob: Job? = null private var tapJob: Job? = null private var isSecondTap = false private var longPressTriggered = false private var firstTapUpTime = -1L private var ignoreNextUp = false private var indirectDownEvent: IndirectPointerInputChange? = null private var indirectLongPressJob: Job? = null private var indirectTapJob: Job? = null private var indirectIsSecondTap = false private var indirectLongPressTriggered = false private var indirectFirstTapUpTime = -1L private var indirectIgnoreNextUp = false override fun createPointerInputNodeIfNeeded(): SuspendingPointerInputModifierNode? { if (isSuspendingPointerInputEnabled) { return SuspendingPointerInputModifierNode { detectTapGestures( onDoubleTap = if (enabled && onDoubleClick != null) { { onDoubleClick?.invoke() } } else null, onLongPress = if (enabled && onLongClick != null) { { onLongClick?.invoke() if (hapticFeedbackEnabled) { currentValueOf(LocalHapticFeedback) .performHapticFeedback(HapticFeedbackType.LongPress) } } } else null, onPress = { offset -> if (enabled) { handlePressInteraction(offset) } }, onTap = { if (enabled) { onClick() } }, ) } } return null } @OptIn(ExperimentalFoundationApi::class) override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { super.onPointerEvent(pointerEvent, pass, bounds) if (isSuspendingPointerInputEnabled) return if (pass == PointerEventPass.Main) { if (downEvent == null) { if (pointerEvent.isChangedToDown(requireUnconsumed = true)) { handleDownEvent(pointerEvent.changes[0]) } } else { if (pointerEvent.isDeepPress) { handleDeepPress() } if (longPressTriggered) { // This branch specifically handles the case where the long press callback has // already been invoked. if (pointerEvent.changes.fastAll { it.changedToUpIgnoreConsumed() }) { // A long press already fired its callback and all the pointers are up. We // must reset our state even if the up event was already consumed by a // child. val up = pointerEvent.changes[0] up.consume() handleUpEvent(uptimeMillis = up.uptimeMillis, downChange = downEvent!!) } else { // Once a long press has triggered, consume every event until pointers are // up. pointerEvent.changes.fastForEach { it.consume() } } return } if (pointerEvent.changes.fastAll { it.changedToUp() }) { // All pointers are up val up = pointerEvent.changes[0] up.consume() handleUpEvent(uptimeMillis = up.uptimeMillis, downChange = downEvent!!) } else { // Other events need to be checked for consumption / bounds related // cancellation. handleNonUpEventIfNeeded(pointerEvent, bounds) } } } else if (pass == PointerEventPass.Final) { checkForCancellation(pointerEvent) } } override fun onIndirectPointerEvent(event: IndirectPointerEvent, pass: PointerEventPass) { super.onIndirectPointerEvent(event, pass) if (pass == PointerEventPass.Main) { if (indirectDownEvent == null) { if (event.changes.fastAny { it.changedToDownIgnoreConsumed() }) { handleDownEvent(event.changes[0]) } } else { if (indirectLongPressTriggered) { // This branch specifically handles the case where the long press callback has // already been invoked. if (event.changes.fastAll { it.changedToUpIgnoreConsumed() }) { // A long press already fired its callback and all the pointers are up. We // must reset our state even if the up event was already consumed by a // child. val up = event.changes[0] up.consume() handleUpEvent( uptimeMillis = up.uptimeMillis, downChange = indirectDownEvent!!, ) } else { // Once a long press has triggered, consume every event until pointers are // up event.changes.fastForEach { it.consume() } } return } if (event.changes.fastAll { it.changedToUp() }) { // All pointers are up val up = event.changes[0] up.consume() handleUpEvent(uptimeMillis = up.uptimeMillis, downChange = indirectDownEvent!!) } else { // Other events need to be checked for consumption / exceeding touch slop handleNonUpEventIfNeeded(event) } } } else if (pass == PointerEventPass.Final) { checkForCancellation(event) } } @OptIn(ExperimentalFoundationApi::class) private fun handleDownEvent(down: PointerInputChange) { down.consume() this.downEvent = down if (enabled) { if (tapJob?.isActive == true) { val minTime = currentValueOf(LocalViewConfiguration).doubleTapMinTimeMillis if (down.uptimeMillis - firstTapUpTime < minTime) { ignoreNextUp = true // Ignore this down event, don't check for long press / emit press // interactions return } else { isSecondTap = true tapJob?.cancel() tapJob = null } } longPressTriggered = false if (isDelayPressesUsingGestureConsumptionEnabled) { handlePressInteractionStart(down) } else { handlePressInteractionStart(down.position, false) } if (onLongClick != null) { longPressJob = coroutineScope.launch { delay(currentValueOf(LocalViewConfiguration).longPressTimeoutMillis) onLongClick?.invoke() if (hapticFeedbackEnabled) { currentValueOf(LocalHapticFeedback) .performHapticFeedback(HapticFeedbackType.LongPress) } longPressTriggered = true tapJob?.cancel() tapJob = null longPressJob = null } } } } @OptIn(ExperimentalFoundationApi::class) private fun handleDownEvent(down: IndirectPointerInputChange) { down.consume() this.indirectDownEvent = down if (enabled) { if (indirectTapJob?.isActive == true) { val minTime = currentValueOf(LocalViewConfiguration).doubleTapMinTimeMillis if (down.uptimeMillis - indirectFirstTapUpTime < minTime) { indirectIgnoreNextUp = true // Ignore this down event, don't check for long press / emit press // interactions return } else { indirectIsSecondTap = true indirectTapJob?.cancel() indirectTapJob = null } } indirectLongPressTriggered = false if (isDelayPressesUsingGestureConsumptionEnabled) { handlePressInteractionStart(down) } else { handlePressInteractionStart(down.position, true) } if (onLongClick != null) { indirectLongPressJob = coroutineScope.launch { delay(currentValueOf(LocalViewConfiguration).longPressTimeoutMillis) onLongClick?.invoke() if (hapticFeedbackEnabled) { currentValueOf(LocalHapticFeedback) .performHapticFeedback(HapticFeedbackType.LongPress) } indirectLongPressTriggered = true indirectTapJob?.cancel() indirectTapJob = null indirectLongPressJob = null } } } } private fun handleUpEvent(uptimeMillis: Long, downChange: PointerInputChange) { if (enabled && !ignoreNextUp) { handlePressInteractionRelease(downChange.position, indirectPointer = false) firstTapUpTime = uptimeMillis // store uptime for double tap check if (!longPressTriggered) { if (isSecondTap) { onDoubleClick?.invoke() } else { if (onDoubleClick != null) { tapJob = coroutineScope.launch { delay(currentValueOf(LocalViewConfiguration).doubleTapTimeoutMillis) onClick() tapJob = null } } else { onClick() } } } } this.downEvent = null ignoreNextUp = false isSecondTap = false longPressJob?.cancel() longPressJob = null longPressTriggered = false } private fun handleUpEvent(uptimeMillis: Long, downChange: IndirectPointerInputChange) { if (enabled && !indirectIgnoreNextUp) { handlePressInteractionRelease(downChange.position, indirectPointer = true) indirectFirstTapUpTime = uptimeMillis // store uptime for double tap check if (!indirectLongPressTriggered) { if (indirectIsSecondTap) { onDoubleClick?.invoke() } else { if (onDoubleClick != null) { indirectTapJob = coroutineScope.launch { delay(currentValueOf(LocalViewConfiguration).doubleTapTimeoutMillis) onClick() indirectTapJob = null } } else { onClick() } } } } this.indirectDownEvent = null indirectIgnoreNextUp = false indirectIsSecondTap = false indirectLongPressJob?.cancel() indirectLongPressJob = null indirectLongPressTriggered = false } private fun handleNonUpEventIfNeeded(pointerEvent: PointerEvent, bounds: IntSize) { val touchPadding = getExtendedTouchPadding(bounds) if ( pointerEvent.changes.fastAny { change -> change.isConsumed || change.isOutOfBounds(bounds, touchPadding) } ) { cancelInput(indirectPointer = false) } } private fun handleNonUpEventIfNeeded(indirectPointerEvent: IndirectPointerEvent) { val touchSlop = currentValueOf(LocalViewConfiguration).touchSlop if ( indirectPointerEvent.changes.fastAny { change -> val distanceFromPress = change.position - indirectDownEvent!!.position val isOutOfBounds = abs(distanceFromPress.getDistance()) > touchSlop change.isConsumed || isOutOfBounds } ) { cancelInput(indirectPointer = true) } } private fun handleDeepPress() { if (!longPressTriggered && enabled && onLongClick != null) { longPressJob?.cancel() longPressJob = null onLongClick?.invoke() if (hapticFeedbackEnabled) { currentValueOf(LocalHapticFeedback) .performHapticFeedback(HapticFeedbackType.LongPress) } longPressTriggered = true } } private fun checkForCancellation(pointerEvent: PointerEvent) { if (downEvent != null && !longPressTriggered) { // Check for cancel by position consumption. We can look on the Final pass of the // existing pointer event because it comes after the pass we checked above. We ignore // cases where the long press has already triggered, as in this case we will consume // events ourselves until the pointer is released. if (pointerEvent.changes.fastAny { it.isConsumed && it != downEvent }) { // Canceled cancelInput(indirectPointer = false) } } } private fun checkForCancellation(indirectPointerEvent: IndirectPointerEvent) { if (indirectDownEvent != null && !indirectLongPressTriggered) { // Check for cancel by position consumption. We can look on the Final pass of the // existing pointer event because it comes after the pass we checked above. We ignore // cases where the long press has already triggered, as in this case we will consume // events ourselves until the pointer is released. if (indirectPointerEvent.changes.fastAny { it.isConsumed && it != indirectDownEvent }) { // Canceled cancelInput(indirectPointer = true) } } } override fun onCancelPointerInput() { super.onCancelPointerInput() cancelInput(indirectPointer = false) } override fun onCancelIndirectPointerInput() { cancelInput(indirectPointer = true) } private fun cancelInput(indirectPointer: Boolean) { if (indirectPointer) { indirectDownEvent = null indirectLongPressJob?.cancel() indirectLongPressJob = null indirectTapJob?.cancel() indirectTapJob = null indirectIsSecondTap = false indirectLongPressTriggered = false indirectFirstTapUpTime = -1L indirectIgnoreNextUp = false } else { downEvent = null longPressJob?.cancel() longPressJob = null tapJob?.cancel() tapJob = null isSecondTap = false longPressTriggered = false firstTapUpTime = -1L ignoreNextUp = false } handlePressInteractionCancel(indirectPointer) } fun update( onClick: () -> Unit, onLongClickLabel: String?, onLongClick: (() -> Unit)?, onDoubleClick: (() -> Unit)?, interactionSource: MutableInteractionSource?, indicationNodeFactory: IndicationNodeFactory?, useLocalIndication: Boolean, enabled: Boolean, onClickLabel: String?, role: Role?, ) { var resetPointerInputHandling = false // onClick is captured inside a callback, not as an input to detectTapGestures, // so no need to reset pointer input handling if (this.onLongClickLabel != onLongClickLabel) { this.onLongClickLabel = onLongClickLabel invalidateSemantics() } // We capture onLongClick and onDoubleClick inside the callback, so if the lambda changes // value we don't want to reset input handling - only reset if they go from not-defined to // defined, and vice versa, as that is what is captured in the parameter to // detectTapGestures. if ((this.onLongClick == null) != (onLongClick == null)) { // Adding or removing longClick should cancel any existing press interactions disposeInteractions() // Adding or removing longClick should add / remove the corresponding property invalidateSemantics() resetPointerInputHandling = true } this.onLongClick = onLongClick if ((this.onDoubleClick == null) != (onDoubleClick == null)) { resetPointerInputHandling = true } this.onDoubleClick = onDoubleClick // enabled is captured as a parameter to detectTapGestures, so we need to restart detecting // gestures if it changes. if (this.enabled != enabled) { resetPointerInputHandling = true // Updating is handled inside updateCommon } updateCommon( interactionSource = interactionSource, indicationNodeFactory = indicationNodeFactory, useLocalIndication = useLocalIndication, enabled = enabled, onClickLabel = onClickLabel, role = role, onClick = onClick, ) if (resetPointerInputHandling) { resetPointerInputHandler() cancelInput(indirectPointer = false) cancelInput(indirectPointer = true) } } override fun SemanticsPropertyReceiver.applyAdditionalSemantics() { if (onLongClick != null) { onLongClick( action = { onLongClick?.invoke() true }, label = onLongClickLabel, ) } } override fun onClickKeyDownEvent(event: KeyEvent): Boolean { val keyCode = event.key.keyCode var handledByLongClick = false if (onLongClick != null) { if (longKeyPressJobs[keyCode] == null) { longKeyPressJobs[keyCode] = coroutineScope.launch { delay(currentValueOf(LocalViewConfiguration).longPressTimeoutMillis) onLongClick?.invoke() } handledByLongClick = true } } val doubleClickState = doubleKeyClickStates[keyCode] // This is the second down event, so it might be a double click if (doubleClickState != null) { // Within the allowed timeout, so check if this is above the minimum time needed for // a double click if (doubleClickState.job.isActive) { doubleClickState.job.cancel() // If the second down was before the minimum double tap time, don't track this as // a double click. Instead, we need to invoke onClick for the previous click, since // that is now counted as a standalone click instead of the first of a double click. if (!doubleClickState.doubleTapMinTimeMillisElapsed) { onClick() doubleKeyClickStates.remove(keyCode) } } else { // We already invoked onClick because we passed the timeout, so stop tracking this // as a double click doubleKeyClickStates.remove(keyCode) } } return handledByLongClick } override fun onClickKeyUpEvent(event: KeyEvent): Boolean { val keyCode = event.key.keyCode var longClickInvoked = false if (longKeyPressJobs[keyCode] != null) { longKeyPressJobs[keyCode]?.let { if (it.isActive) { it.cancel() } else { // If we already passed the timeout, we invoked long click already, and so // we shouldn't invoke onClick in this case longClickInvoked = true } } longKeyPressJobs.remove(keyCode) } if (onDoubleClick != null) { when { // First click doubleKeyClickStates[keyCode] == null -> { // We only track the second click if the first click was not a long click if (!longClickInvoked) { doubleKeyClickStates[keyCode] = DoubleKeyClickState( coroutineScope.launch { val configuration = currentValueOf(LocalViewConfiguration) val minTime = configuration.doubleTapMinTimeMillis val timeout = configuration.doubleTapTimeoutMillis delay(minTime) doubleKeyClickStates[keyCode]?.doubleTapMinTimeMillisElapsed = true // Delay the remainder until we are at timeout delay(timeout - minTime) // If there was no second key press after the timeout, invoke // onClick as normal onClick() } ) } } // Second click else -> { // Invoke onDoubleClick if the second click was not a long click if (!longClickInvoked) { onDoubleClick?.invoke() } doubleKeyClickStates.remove(keyCode) } } } else { if (!longClickInvoked) { onClick() } } return true } override fun onCancelKeyInput() { resetKeyPressState() } override fun onReset() { super.onReset() resetKeyPressState() } private fun resetKeyPressState() { longKeyPressJobs.apply { forEachValue { it.cancel() } clear() } doubleKeyClickStates.apply { forEachValue { it.job.cancel() } clear() } } } internal abstract class AbstractClickableNode( private var interactionSource: MutableInteractionSource?, private var indicationNodeFactory: IndicationNodeFactory?, private var useLocalIndication: Boolean, enabled: Boolean, private var onClickLabel: String?, private var role: Role?, onClick: () -> Unit, ) : DelegatingNode(), PointerInputModifierNode, KeyInputModifierNode, SemanticsModifierNode, TraversableNode, CompositionLocalConsumerModifierNode, ObserverModifierNode, IndirectPointerInputModifierNode, GestureConnection { protected var enabled = enabled private set protected var onClick = onClick private set final override val shouldAutoInvalidate: Boolean = false private val focusableNode: FocusableNode = FocusableNode( interactionSource, focusability = Focusability.SystemDefined, onFocusChange = ::onFocusChange, ) private var localIndicationNodeFactory: IndicationNodeFactory? = null private var pointerInputNode: SuspendingPointerInputModifierNode? = null private var gestureNode: DelegatableNode? = null private var indicationNode: DelegatableNode? = null private var pressInteraction: PressInteraction.Press? = null private var hoverInteraction: HoverInteraction.Enter? = null private val currentKeyPressInteractions = mutableLongObjectMapOf() private var centerOffset: Offset = Offset.Zero private var indirectPointerPressInteraction: PressInteraction.Press? = null private var indirectPointerEventPressPosition: Offset? = null // Track separately from interactionSource, as we will create our own internal // InteractionSource if needed private var userProvidedInteractionSource: MutableInteractionSource? = interactionSource private var lazilyCreateIndication = shouldLazilyCreateIndication() private fun shouldLazilyCreateIndication() = userProvidedInteractionSource == null /** * Handles subclass-specific click related pointer input logic. Hover is already handled * elsewhere, so this should only handle clicks. * * TODO(b/477836055) Migrate to non-suspending API. */ open fun createPointerInputNodeIfNeeded(): SuspendingPointerInputModifierNode? = null open fun SemanticsPropertyReceiver.applyAdditionalSemantics() {} protected fun updateCommon( interactionSource: MutableInteractionSource?, indicationNodeFactory: IndicationNodeFactory?, useLocalIndication: Boolean, enabled: Boolean, onClickLabel: String?, role: Role?, onClick: () -> Unit, ) { var isIndicationNodeDirty = false // Compare against userProvidedInteractionSource, as we will create a new InteractionSource // lazily if the userProvidedInteractionSource is null, and assign it to interactionSource if (userProvidedInteractionSource != interactionSource) { disposeInteractions() userProvidedInteractionSource = interactionSource this.interactionSource = interactionSource isIndicationNodeDirty = true } if (this.indicationNodeFactory != indicationNodeFactory) { this.indicationNodeFactory = indicationNodeFactory isIndicationNodeDirty = true } if (this.useLocalIndication != useLocalIndication) { this.useLocalIndication = useLocalIndication if (useLocalIndication) { // Need to update localIndicationNodeFactory, and start observing changes onObservedReadsChanged() } isIndicationNodeDirty = true } if (this.enabled != enabled) { if (enabled) { delegate(focusableNode) } else { // TODO: Should we remove indicationNode? Previously we always emitted indication undelegate(focusableNode) disposeInteractions() } invalidateSemantics() this.enabled = enabled } if (this.onClickLabel != onClickLabel) { this.onClickLabel = onClickLabel invalidateSemantics() } if (this.role != role) { this.role = role invalidateSemantics() } this.onClick = onClick if (lazilyCreateIndication != shouldLazilyCreateIndication()) { lazilyCreateIndication = shouldLazilyCreateIndication() // If we are no longer lazily creating the node, and we haven't created the node yet, // create it if (!lazilyCreateIndication && indicationNode == null) isIndicationNodeDirty = true } // Create / recreate indication node if (isIndicationNodeDirty) { recreateIndicationIfNeeded() } focusableNode.update(this.interactionSource) } protected fun getExtendedTouchPadding(size: IntSize): Size { // copied from SuspendingPointerInputModifierNodeImpl.extendedTouchPadding: // TODO expose this as a new public api available outside of suspending apis b/422396609 val minimumTouchTargetSizeDp = currentValueOf(LocalViewConfiguration).minimumTouchTargetSize val minimumTouchTargetSize = with(requireDensity()) { minimumTouchTargetSizeDp.toSize() } val horizontal = max(0f, minimumTouchTargetSize.width - size.width) / 2f val vertical = max(0f, minimumTouchTargetSize.height - size.height) / 2f return Size(horizontal, vertical) } override fun onIndirectPointerEvent(event: IndirectPointerEvent, pass: PointerEventPass) { initializeIndicationAndInteractionSourceIfNeeded() if (enabled) { initializeGestureCoordination() } } final override fun onAttach() { onObservedReadsChanged() if (!lazilyCreateIndication) { initializeIndicationAndInteractionSourceIfNeeded() } if (enabled) { delegate(focusableNode) } } override fun onObservedReadsChanged() { if (useLocalIndication) { observeReads { val indication = currentValueOf(LocalIndication) requirePrecondition(indication is IndicationNodeFactory) { unsupportedIndicationExceptionMessage(indication) } val previousFactory = localIndicationNodeFactory localIndicationNodeFactory = indication // If we are changing from a non-null factory to a different factory, recreate // indication if needed if (previousFactory != null && localIndicationNodeFactory != previousFactory) { recreateIndicationIfNeeded() } } } } final override fun onDetach() { disposeInteractions() // If we lazily created an interaction source, reset it in case we are reused / moved. Note // that we need to do it here instead of onReset() - since onReset won't be called in the // movableContent case but we still want to dispose for that case if (userProvidedInteractionSource == null) { interactionSource = null } // Remove indication in case we are reused / moved - we will create a new node when needed indicationNode?.let { undelegate(it) } indicationNode = null gestureNode?.let { undelegate(it) } gestureNode = null } protected fun disposeInteractions() { interactionSource?.let { interactionSource -> pressInteraction?.let { oldValue -> val interaction = PressInteraction.Cancel(oldValue) interactionSource.tryEmit(interaction) } indirectPointerPressInteraction?.let { oldValue -> val interaction = PressInteraction.Cancel(oldValue) interactionSource.tryEmit(interaction) } hoverInteraction?.let { oldValue -> val interaction = HoverInteraction.Exit(oldValue) interactionSource.tryEmit(interaction) } currentKeyPressInteractions.forEachValue { interactionSource.tryEmit(PressInteraction.Cancel(it)) } } pressInteraction = null indirectPointerPressInteraction = null indirectPointerEventPressPosition = null hoverInteraction = null currentKeyPressInteractions.clear() } private fun onFocusChange(isFocused: Boolean) { if (isFocused) { initializeIndicationAndInteractionSourceIfNeeded() } else { // If we are no longer focused while we are tracking existing key presses, we need to // clear them and cancel the presses. if (interactionSource != null) { currentKeyPressInteractions.forEachValue { coroutineScope.launch { interactionSource?.emit(PressInteraction.Cancel(it)) } } indirectPointerPressInteraction?.let { coroutineScope.launch { interactionSource?.emit(PressInteraction.Cancel(it)) } } } currentKeyPressInteractions.clear() indirectPointerPressInteraction = null onCancelKeyInput() } } private fun recreateIndicationIfNeeded() { // If we already created a node lazily, or we are not lazily creating the node, create if (indicationNode != null || !lazilyCreateIndication) { indicationNode?.let { undelegate(it) } indicationNode = null initializeIndicationAndInteractionSourceIfNeeded() } } private fun initializeIndicationAndInteractionSourceIfNeeded() { // We have already created the node, no need to do any work if (indicationNode != null) return val indicationFactory = if (useLocalIndication) localIndicationNodeFactory else indicationNodeFactory indicationFactory?.let { factory -> if (interactionSource == null) { interactionSource = MutableInteractionSource() } focusableNode.update(interactionSource) val node = factory.create(interactionSource!!) delegate(node) indicationNode = node } } @OptIn(ExperimentalFoundationApi::class) private fun initializeGestureCoordination() { if (!isDelayPressesUsingGestureConsumptionEnabled) return if (gestureNode == null) { gestureNode = delegate(gestureNode(this)) } } override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { centerOffset = bounds.center.toOffset() initializeIndicationAndInteractionSourceIfNeeded() if (enabled) { initializeGestureCoordination() if (pass == PointerEventPass.Main) { when (pointerEvent.type) { PointerEventType.Enter -> coroutineScope.launch { emitHoverEnter() } PointerEventType.Exit -> coroutineScope.launch { emitHoverExit() } } } } if (pointerInputNode == null) { val node = createPointerInputNodeIfNeeded() if (node != null) { pointerInputNode = delegate(node) } } pointerInputNode?.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { // Press cancellation is handled as part of detecting presses interactionSource?.let { interactionSource -> hoverInteraction?.let { oldValue -> val interaction = HoverInteraction.Exit(oldValue) interactionSource.tryEmit(interaction) } } hoverInteraction = null pointerInputNode?.onCancelPointerInput() } final override fun onKeyEvent(event: KeyEvent): Boolean { // Key events usually require focus, but if a focused child does not handle the KeyEvent, // the event can bubble up without this clickable ever being focused, and hence without // this being initialized through the focus path initializeIndicationAndInteractionSourceIfNeeded() val keyCode = event.key.keyCode return when { enabled && event.isPress -> { // If the key already exists in the map, keyEvent is a repeat event. // We ignore it as we only want to emit an interaction for the initial key press. var wasInteractionHandled = false if (!currentKeyPressInteractions.containsKey(keyCode)) { val press = PressInteraction.Press(centerOffset) currentKeyPressInteractions[keyCode] = press // Even if the interactionSource is null, we still want to intercept the presses // so we always track them above, and return true if (interactionSource != null) { coroutineScope.launch { interactionSource?.emit(press) } } wasInteractionHandled = true } onClickKeyDownEvent(event) || wasInteractionHandled } enabled && event.isClick -> { val press = currentKeyPressInteractions.remove(keyCode) if (press != null) { if (interactionSource != null) { coroutineScope.launch { interactionSource?.emit(PressInteraction.Release(press)) } } // Don't invoke onClick if we were not pressed - this could happen if we became // focused after the down event, or if the node was reused after the down event. onClickKeyUpEvent(event) } // Only consume if we were previously pressed for this key event press != null } else -> false } } protected abstract fun onClickKeyDownEvent(event: KeyEvent): Boolean protected abstract fun onClickKeyUpEvent(event: KeyEvent): Boolean /** * Called when focus is lost, to allow cleaning up and resetting the state for ongoing key * presses */ protected open fun onCancelKeyInput() {} final override fun onPreKeyEvent(event: KeyEvent) = false final override val shouldMergeDescendantSemantics: Boolean get() = true final override fun SemanticsPropertyReceiver.applySemantics() { if (this@AbstractClickableNode.role != null) { role = this@AbstractClickableNode.role!! } onClick( action = { onClick() true }, label = onClickLabel, ) if (enabled) { with(focusableNode) { applySemantics() } } else { disabled() } applyAdditionalSemantics() } protected fun resetPointerInputHandler() = pointerInputNode?.resetPointerInputHandler() private var delayJob: Job? = null /** Handles emitting a [PressInteraction.Press]. */ protected fun handlePressInteractionStart(event: IndirectPointerInputChange) { interactionSource?.let { interactionSource -> val press = PressInteraction.Press(event.position) if (delayPressInteraction(event)) { delayJob = coroutineScope.launch { delay(TapIndicationDelay) interactionSource.emit(press) indirectPointerPressInteraction = press } } else { indirectPointerPressInteraction = press coroutineScope.launch { interactionSource.emit(press) } } } } protected fun handlePressInteractionStart(event: PointerInputChange) { interactionSource?.let { interactionSource -> val press = PressInteraction.Press(event.position) if (delayPressInteraction(event)) { delayJob = coroutineScope.launch { delay(TapIndicationDelay) interactionSource.emit(press) pressInteraction = press } } else { pressInteraction = press coroutineScope.launch { interactionSource.emit(press) } } } } @OptIn(ExperimentalFoundationApi::class) protected fun handlePressInteractionStart(offset: Offset, indirectPointer: Boolean) { interactionSource?.let { interactionSource -> val press = PressInteraction.Press(offset) val shouldDelayPress = if (isDelayPressesUsingGestureConsumptionEnabled) { delayPressInteraction(null) } else { delayPressInteraction() } if (shouldDelayPress) { delayJob = coroutineScope.launch { delay(TapIndicationDelay) interactionSource.emit(press) if (indirectPointer) { indirectPointerPressInteraction = press } else { pressInteraction = press } } } else { if (indirectPointer) { indirectPointerPressInteraction = press } else { pressInteraction = press } coroutineScope.launch { interactionSource.emit(press) } } } } /** * Handles emitting a [PressInteraction.Release]. * * @param offset offset of the press * @param indirectPointer whether the source of this press was indirect pointer. False for * pointer input. */ protected fun handlePressInteractionRelease(offset: Offset, indirectPointer: Boolean) { interactionSource?.let { interactionSource -> // To resolve b/414319919 it is important that we capture a reference to `delayJob` // outside the coroutine block - when the CPU is busy we can end up handling // press-release-press before the coroutine starts to execute, which means we can launch // two jobs and mutate delayJob twice. At the time this function is called, delayJob // points to the correct corresponding press event, so we just reference this instance // to make sure that there is no issue if coroutines are executed after the next set of // gestures have been processed. val job = delayJob if (job?.isActive == true) { // Immediately cancel the job to avoid a race condition from coroutine launching - // if we wait until inside the launch to cancel it could be executed after the job // is no longer active. An alternative approach would be to launch with // start = CoroutineStart.UNDISPATCHED, but it is more reasonable to cancel // outside the coroutine in any case. job.cancel() coroutineScope.launch { // Wait for cancelling the job to finish if needed job.join() // The press released successfully, before the timeout duration - emit the press // interaction instantly. val press = PressInteraction.Press(offset) val release = PressInteraction.Release(press) interactionSource.emit(press) interactionSource.emit(release) } } else { val interaction = if (indirectPointer) indirectPointerPressInteraction else pressInteraction interaction?.let { coroutineScope.launch { // Important that we capture `interaction` outside the `launch`, rather than // referring to it in here - the underlying fields are mutable and could // change by the time this coroutine is executed val endInteraction = PressInteraction.Release(it) interactionSource.emit(endInteraction) } } } if (indirectPointer) { indirectPointerPressInteraction = null } else { pressInteraction = null } } } /** * Handles emitting a [PressInteraction.Cancel]. * * @param indirectPointer whether the source of this press was indirect pointer. False for * pointer input. */ protected fun handlePressInteractionCancel(indirectPointer: Boolean) { interactionSource?.let { interactionSource -> if (delayJob?.isActive == true) { // We didn't finish sending the press, and we are cancelled, so we don't emit // any interaction. delayJob?.cancel() } else { val interaction = if (indirectPointer) indirectPointerPressInteraction else pressInteraction interaction?.let { val endInteraction = PressInteraction.Cancel(it) // If this is being called from inside onDetach(), we are still attached, but // the scope will be cancelled soon after - so the launch {} might not even // start before it is cancelled. We don't want to use // CoroutineStart.UNDISPATCHED, or always call tryEmit() as this will break // other timing / cause some events to be missed for other cases. Instead just // make sure we call tryEmit if we cancel the scope, before we finish emitting. val handler = coroutineScope.coroutineContext[Job]?.invokeOnCompletion { interactionSource.tryEmit(endInteraction) } coroutineScope.launch { interactionSource.emit(endInteraction) handler?.dispose() } } } if (indirectPointer) { indirectPointerPressInteraction = null } else { pressInteraction = null } } } @OptIn(ExperimentalFoundationApi::class) protected suspend fun PressGestureScope.handlePressInteraction(offset: Offset) { interactionSource?.let { interactionSource -> coroutineScope { val delayJob = launch { val shouldDelayPress = if (isDelayPressesUsingGestureConsumptionEnabled) { delayPressInteraction(null) } else { delayPressInteraction() } if (shouldDelayPress) { delay(TapIndicationDelay) } val press = PressInteraction.Press(offset) interactionSource.emit(press) pressInteraction = press } val success = tryAwaitRelease() if (delayJob.isActive) { delayJob.cancelAndJoin() // The press released successfully, before the timeout duration - emit the press // interaction instantly. No else branch - if the press was cancelled before the // timeout, we don't want to emit a press interaction. if (success) { val press = PressInteraction.Press(offset) val release = PressInteraction.Release(press) interactionSource.emit(press) interactionSource.emit(release) } } else { pressInteraction?.let { pressInteraction -> val endInteraction = if (success) { PressInteraction.Release(pressInteraction) } else { PressInteraction.Cancel(pressInteraction) } interactionSource.emit(endInteraction) } } pressInteraction = null } } } private fun delayPressInteraction(): Boolean = hasScrollableContainer() || isComposeRootInScrollableContainer() private fun delayPressInteraction(event: PointerInputChange?): Boolean { val hasInterestedParent = if (event == null) { parentGestureConnection != null } else { hasInterestedParent(event) } return hasInterestedParent || isComposeRootInScrollableContainer() } private fun delayPressInteraction(event: IndirectPointerInputChange): Boolean = hasInterestedParent(event) || isComposeRootInScrollableContainer() private fun emitHoverEnter() { if (hoverInteraction == null) { val interaction = HoverInteraction.Enter() interactionSource?.let { interactionSource -> coroutineScope.launch { interactionSource.emit(interaction) } } hoverInteraction = interaction } } private fun emitHoverExit() { hoverInteraction?.let { oldValue -> val interaction = HoverInteraction.Exit(oldValue) interactionSource?.let { interactionSource -> coroutineScope.launch { interactionSource.emit(interaction) } } hoverInteraction = null } } override val traverseKey: Any = TraverseKey companion object TraverseKey } internal fun DelegatingNode.hasInterestedParent(event: IndirectPointerInputChange): Boolean { var hasInterestedParent = false traverseAncestorGestureConnections { coordinator -> val isCoordinatorInterested = coordinator.isInterested(event) hasInterestedParent = hasInterestedParent || isCoordinatorInterested !hasInterestedParent } return hasInterestedParent } internal fun DelegatingNode.hasInterestedParent(event: PointerInputChange): Boolean { var hasInterestedParent = false traverseAncestorGestureConnections { coordinator -> val isCoordinatorInterested = coordinator.isInterested(event) hasInterestedParent = hasInterestedParent || isCoordinatorInterested !hasInterestedParent } return hasInterestedParent } internal fun TraversableNode.hasScrollableContainer(): Boolean { var hasScrollable = false traverseAncestors(ScrollableContainerNode.TraverseKey) { node -> hasScrollable = hasScrollable || (node as ScrollableContainerNode).enabled !hasScrollable } return hasScrollable } private fun unsupportedIndicationExceptionMessage(indication: Indication): String { return "clickable only supports IndicationNodeFactory instances provided to LocalIndication, " + "but Indication was provided instead. Either migrate the Indication implementation to " + "implement IndicationNodeFactory, or use the other clickable overload that takes an " + "Indication parameter, and explicitly pass LocalIndication.current there. The Indication" + " instance provided here was: $indication" } private fun IndirectPointerInputChange.changedToUp() = !isConsumed && previousPressed && !pressed private fun IndirectPointerInputChange.changedToUpIgnoreConsumed() = previousPressed && !pressed ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyList.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.foundation.lazy import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.checkScrollableContainerConstraints import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.internal.requirePreconditionNotNull import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.lazy.layout.CacheWindowLogic import androidx.compose.foundation.lazy.layout.LazyLayout import androidx.compose.foundation.lazy.layout.LazyLayoutMeasurePolicy import androidx.compose.foundation.lazy.layout.StickyItemsPlacement import androidx.compose.foundation.lazy.layout.calculateLazyLayoutPinnedIndices import androidx.compose.foundation.lazy.layout.lazyLayoutBeyondBoundsModifier import androidx.compose.foundation.lazy.layout.lazyLayoutSemantics import androidx.compose.foundation.scrollableArea import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.GraphicsContext import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalGraphicsContext import androidx.compose.ui.platform.LocalScrollCaptureInProgress import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.offset import androidx.compose.ui.util.trace import kotlinx.coroutines.CoroutineScope @OptIn(ExperimentalFoundationApi::class) @Composable internal fun LazyList( /** Modifier to be applied for the inner layout */ modifier: Modifier, /** State controlling the scroll position */ state: LazyListState, /** The inner padding to be added for the whole content(not for each individual item) */ contentPadding: PaddingValues, /** reverse the direction of scrolling and layout */ reverseLayout: Boolean, /** The layout orientation of the list */ isVertical: Boolean, /** fling behavior to be used for flinging */ flingBehavior: FlingBehavior, /** Whether scrolling via the user gestures is allowed. */ userScrollEnabled: Boolean, /** The overscroll effect to render and dispatch events to */ overscrollEffect: OverscrollEffect?, /** Number of items to layout before and after the visible items */ beyondBoundsItemCount: Int = defaultLazyListBeyondBoundsItemCount(), /** The alignment to align items horizontally. Required when isVertical is true */ horizontalAlignment: Alignment.Horizontal? = null, /** The vertical arrangement for items. Required when isVertical is true */ verticalArrangement: Arrangement.Vertical? = null, /** The alignment to align items vertically. Required when isVertical is false */ verticalAlignment: Alignment.Vertical? = null, /** The horizontal arrangement for items. Required when isVertical is false */ horizontalArrangement: Arrangement.Horizontal? = null, /** The content of the list */ content: LazyListScope.() -> Unit, ) { val itemProviderLambda = rememberLazyListItemProviderLambda(state, content) val semanticState = rememberLazyListSemanticState(state, isVertical) val coroutineScope = rememberCoroutineScope() val graphicsContext = LocalGraphicsContext.current val stickyHeadersEnabled = !LocalScrollCaptureInProgress.current val measurePolicy = rememberLazyListMeasurePolicy( itemProviderLambda, state, contentPadding, reverseLayout, isVertical, beyondBoundsItemCount, horizontalAlignment, verticalAlignment, horizontalArrangement, verticalArrangement, coroutineScope, graphicsContext, if (stickyHeadersEnabled) StickyItemsPlacement.StickToTopPlacement else null, ) val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal val beyondBoundsModifier = if (userScrollEnabled) { Modifier.lazyLayoutBeyondBoundsModifier( state = rememberLazyListBeyondBoundsState( state = state, beyondBoundsItemCount = beyondBoundsItemCount, ), beyondBoundsInfo = state.beyondBoundsInfo, reverseLayout = reverseLayout, orientation = orientation, ) } else { Modifier } LazyLayout( modifier = modifier .then(state.remeasurementModifier) .then(state.awaitLayoutModifier) .lazyLayoutSemantics( itemProviderLambda = itemProviderLambda, state = semanticState, orientation = orientation, userScrollEnabled = userScrollEnabled, reverseScrolling = reverseLayout, ) .then(beyondBoundsModifier) .then(state.itemAnimator.modifier) .scrollableArea( state = state, orientation = orientation, enabled = userScrollEnabled, reverseScrolling = reverseLayout, flingBehavior = flingBehavior, interactionSource = state.internalInteractionSource, overscrollEffect = overscrollEffect, ), prefetchState = state.prefetchState, measurePolicy = measurePolicy, itemProvider = itemProviderLambda, ) } @ExperimentalFoundationApi @Composable private fun rememberLazyListMeasurePolicy( /** Items provider of the list. */ itemProviderLambda: () -> LazyListItemProvider, /** The state of the list. */ state: LazyListState, /** The inner padding to be added for the whole content(nor for each individual item) */ contentPadding: PaddingValues, /** reverse the direction of scrolling and layout */ reverseLayout: Boolean, /** The layout orientation of the list */ isVertical: Boolean, /** Number of items to layout before and after the visible items */ beyondBoundsItemCount: Int, /** The alignment to align items horizontally */ horizontalAlignment: Alignment.Horizontal?, /** The alignment to align items vertically */ verticalAlignment: Alignment.Vertical?, /** The horizontal arrangement for items */ horizontalArrangement: Arrangement.Horizontal?, /** The vertical arrangement for items */ verticalArrangement: Arrangement.Vertical?, /** Scope for animations */ coroutineScope: CoroutineScope, /** Used for creating graphics layers */ graphicsContext: GraphicsContext, /** Scroll behavior for sticky items */ stickyItemsPlacement: StickyItemsPlacement?, ) = remember( state, contentPadding, reverseLayout, isVertical, beyondBoundsItemCount, horizontalAlignment, verticalAlignment, horizontalArrangement, verticalArrangement, graphicsContext, stickyItemsPlacement, ) { LazyLayoutMeasurePolicy { containerConstraints -> state.measurementScopeInvalidator.attachToScope() // Tracks if the lookahead pass has occurred val hasLookaheadOccurred = state.hasLookaheadOccurred || isLookingAhead checkScrollableContainerConstraints( containerConstraints, if (isVertical) Orientation.Vertical else Orientation.Horizontal, ) // resolve content paddings val startPadding = if (isVertical) { contentPadding.calculateLeftPadding(layoutDirection).roundToPx() } else { // in horizontal configuration, padding is reversed by placeRelative contentPadding.calculateStartPadding(layoutDirection).roundToPx() } val endPadding = if (isVertical) { contentPadding.calculateRightPadding(layoutDirection).roundToPx() } else { // in horizontal configuration, padding is reversed by placeRelative contentPadding.calculateEndPadding(layoutDirection).roundToPx() } val topPadding = contentPadding.calculateTopPadding().roundToPx() val bottomPadding = contentPadding.calculateBottomPadding().roundToPx() val totalVerticalPadding = topPadding + bottomPadding val totalHorizontalPadding = startPadding + endPadding val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding val beforeContentPadding = when { isVertical && !reverseLayout -> topPadding isVertical && reverseLayout -> bottomPadding !isVertical && !reverseLayout -> startPadding else -> endPadding // !isVertical && reverseLayout } val afterContentPadding = totalMainAxisPadding - beforeContentPadding val contentConstraints = containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding) val itemProvider = itemProviderLambda() // this will update the scope used by the item composables itemProvider.itemScope.setMaxSize( width = contentConstraints.maxWidth, height = contentConstraints.maxHeight, ) val spaceBetweenItemsDp = if (isVertical) { requirePreconditionNotNull(verticalArrangement) { "null verticalArrangement when isVertical == true" } .spacing } else { requirePreconditionNotNull(horizontalArrangement) { "null horizontalAlignment when isVertical == false" } .spacing } val spaceBetweenItems = spaceBetweenItemsDp.roundToPx() val itemsCount = itemProvider.itemCount // can be negative if the content padding is larger than the max size from constraints val mainAxisAvailableSize = if (isVertical) { containerConstraints.maxHeight - totalVerticalPadding } else { containerConstraints.maxWidth - totalHorizontalPadding } val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) { IntOffset(startPadding, topPadding) } else { // When layout is reversed and paddings together take >100% of the available // space, // layout size is coerced to 0 when positioning. To take that space into // account, // we offset start padding by negative space between paddings. IntOffset( if (isVertical) startPadding else startPadding + mainAxisAvailableSize, if (isVertical) topPadding + mainAxisAvailableSize else topPadding, ) } val measuredItemProvider = object : LazyListMeasuredItemProvider( contentConstraints, isVertical, itemProvider, this, ) { override fun createItem( index: Int, key: Any, contentType: Any?, placeables: List, constraints: Constraints, ): LazyListMeasuredItem { // we add spaceBetweenItems as an extra spacing for all items apart from the // last one so // the lazy list measuring logic will take it into account. val spacing = if (index == itemsCount - 1) 0 else spaceBetweenItems return LazyListMeasuredItem( index = index, placeables = placeables, isVertical = isVertical, horizontalAlignment = horizontalAlignment, verticalAlignment = verticalAlignment, layoutDirection = layoutDirection, reverseLayout = reverseLayout, beforeContentPadding = beforeContentPadding, afterContentPadding = afterContentPadding, spacing = spacing, visualOffset = visualItemOffset, key = key, contentType = contentType, animator = state.itemAnimator, constraints = constraints, ) } } val firstVisibleItemIndex: Int val firstVisibleScrollOffset: Int Snapshot.withoutReadObservation { firstVisibleItemIndex = state.updateScrollPositionIfTheFirstItemWasMoved( itemProvider, state.firstVisibleItemIndex, ) firstVisibleScrollOffset = state.firstVisibleItemScrollOffset } val pinnedItems = itemProvider.calculateLazyLayoutPinnedIndices( pinnedItemList = state.pinnedItems, beyondBoundsInfo = state.beyondBoundsInfo, ) val scrollToBeConsumed = if (isLookingAhead || !hasLookaheadOccurred) { state.scrollToBeConsumed } else { state.scrollDeltaBetweenPasses } // todo: wrap with snapshot when b/341782245 is resolved val measureResult = measureLazyList( itemsCount = itemsCount, measuredItemProvider = measuredItemProvider, mainAxisAvailableSize = mainAxisAvailableSize, beforeContentPadding = beforeContentPadding, afterContentPadding = afterContentPadding, spaceBetweenItems = spaceBetweenItems, firstVisibleItemIndex = firstVisibleItemIndex, firstVisibleItemScrollOffset = firstVisibleScrollOffset, scrollToBeConsumed = scrollToBeConsumed, constraints = contentConstraints, isVertical = isVertical, verticalArrangement = verticalArrangement, horizontalArrangement = horizontalArrangement, reverseLayout = reverseLayout, density = this, itemAnimator = state.itemAnimator, beyondBoundsItemCount = beyondBoundsItemCount, pinnedItems = pinnedItems, hasLookaheadOccurred = hasLookaheadOccurred, isLookingAhead = isLookingAhead, coroutineScope = coroutineScope, placementScopeInvalidator = state.placementScopeInvalidator, graphicsContext = graphicsContext, stickyItemsPlacement = stickyItemsPlacement, layout = { width, height, placement -> layout( containerConstraints.constrainWidth(width + totalHorizontalPadding), containerConstraints.constrainHeight(height + totalVerticalPadding), emptyMap(), placement, ) }, ) state.applyMeasureResult(measureResult, isLookingAhead) // apply keep around after updating the strategy with measure result. (state.prefetchStrategy as? CacheWindowLogic)?.keepAroundItems( measureResult.visibleItemsInfo, measuredItemProvider, ) measureResult } } @OptIn(ExperimentalFoundationApi::class) private fun CacheWindowLogic.keepAroundItems( visibleItemsList: List, measuredItemProvider: LazyListMeasuredItemProvider, ) { trace("compose:lazy:cache_window:keepAroundItems") { // only run if window and new layout info is available if (hasValidBounds() && visibleItemsList.isNotEmpty()) { val firstVisibleItemIndex = visibleItemsList.first().index val lastVisibleItemIndex = visibleItemsList.last().index // we must send a message in case of changing directions for items // that were keep around and become prefetch forward for (item in prefetchWindowStartLine..() internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() @Suppress("DEPRECATION") // b/420551535 internal val prefetchState = LazyLayoutPrefetchState(prefetchStrategy.prefetchScheduler) { with(prefetchStrategy) { onNestedPrefetch(Snapshot.withoutReadObservation { firstVisibleItemIndex }) } } private val prefetchScope: LazyListPrefetchScope = object : LazyListPrefetchScope { override fun schedulePrefetch( index: Int, onPrefetchFinished: (LazyListPrefetchResultScope.() -> Unit)?, ): LazyLayoutPrefetchState.PrefetchHandle { // Without read observation since this can be triggered from scroll - this will then // cause us to recompose when the measure result changes. We don't care since the // prefetch is best effort. val lastMeasureResult = Snapshot.withoutReadObservation { layoutInfoState.value } return prefetchState.schedulePrecompositionAndPremeasure( index, lastMeasureResult.childConstraints, executeRequestsInHighPriorityMode, ) { if (onPrefetchFinished != null) { var mainAxisItemSize = 0 repeat(placeablesCount) { mainAxisItemSize += if (lastMeasureResult.orientation == Orientation.Vertical) { getSize(it).height } else { getSize(it).width } } onPrefetchFinished.invoke( LazyListPrefetchResultScopeImpl(index, mainAxisItemSize) ) } } } } private val _scrollIndicatorState = object : ScrollIndicatorState { override val scrollOffset: Int get() = calculateScrollOffset() override val contentSize: Int get() = layoutInfo.calculateContentSize() override val viewportSize: Int get() = layoutInfo.singleAxisViewportSize } private fun calculateScrollOffset(): Int { return (layoutInfo.visibleItemsAverageSize() * firstVisibleItemIndex) + firstVisibleItemScrollOffset } /** Stores currently pinned items which are always composed. */ internal val pinnedItems = LazyLayoutPinnedItemList() internal val nearestRange: IntRange by scrollPosition.nearestRangeState /** * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset] * pixels. * * @param index the index to which to scroll. Must be non-negative. * @param scrollOffset the offset that the item should end up after the scroll. Note that * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will * scroll the item further upward (taking it partly offscreen). */ suspend fun scrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { scroll { snapToItemIndexInternal(index, scrollOffset, forceRemeasure = true) } } internal val measurementScopeInvalidator = ObservableScopeInvalidator() /** * Requests the item at [index] to be at the start of the viewport during the next remeasure, * offset by [scrollOffset], and schedules a remeasure. * * The scroll position will be updated to the requested position rather than maintain the index * based on the first visible item key (when a data set change will also be applied during the * next remeasure), but *only* for the next remeasure. * * Any scroll in progress will be cancelled. * * @param index the index to which to scroll. Must be non-negative. * @param scrollOffset the offset that the item should end up after the scroll. Note that * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will * scroll the item further upward (taking it partly offscreen). */ fun requestScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { // Cancel any scroll in progress. if (isScrollInProgress) { layoutInfoState.value.coroutineScope.launch { scroll {} } } snapToItemIndexInternal(index, scrollOffset, forceRemeasure = false) } /** * Snaps to the requested scroll position. Synchronously executes remeasure if [forceRemeasure] * is true, and schedules a remeasure if false. */ internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int, forceRemeasure: Boolean) { val positionChanged = scrollPosition.index != index || scrollPosition.scrollOffset != scrollOffset // sometimes this method is called not to scroll, but to stay on the same index when // the data changes, as by default we maintain the scroll position by key, not index. // when this happens we don't need to reset the animations as from the user perspective // we didn't scroll anywhere and if there is an offset change for an item, this change // should be animated. // however, when the request is to really scroll to a different position, we have to // reset previously known item positions as we don't want offset changes to be animated. // this offset should be considered as a scroll, not the placement change. if (positionChanged) { itemAnimator.reset() // we changed positions, cancel existing requests and wait for the next scroll to // refill the window (prefetchStrategy as? CacheWindowLogic)?.resetStrategy() } scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset) if (forceRemeasure) { remeasurement?.forceRemeasure() } else { measurementScopeInvalidator.invalidateScope() } } /** * Call this function to take control of scrolling and gain the ability to send scroll events * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be * performed within a [scroll] block (even if they don't call any other methods on this object) * in order to guarantee that mutual exclusion is enforced. * * If [scroll] is called from elsewhere, this will be canceled. */ override suspend fun scroll( scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit, ) { if (layoutInfoState.value === EmptyLazyListMeasureResult) { awaitLayoutModifier.waitForFirstLayout() } scrollableState.scroll(scrollPriority, block) } override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta) override val isScrollInProgress: Boolean get() = scrollableState.isScrollInProgress override var canScrollForward: Boolean by mutableStateOf(false) private set override var canScrollBackward: Boolean by mutableStateOf(false) private set @get:Suppress("GetterSetterNames") override val lastScrolledForward: Boolean get() = scrollableState.lastScrolledForward @get:Suppress("GetterSetterNames") override val lastScrolledBackward: Boolean get() = scrollableState.lastScrolledBackward override val scrollIndicatorState: ScrollIndicatorState? get() = _scrollIndicatorState internal val placementScopeInvalidator = ObservableScopeInvalidator() // TODO: Coroutine scrolling APIs will allow this to be private again once we have more // fine-grained control over scrolling /*@VisibleForTesting*/ internal fun onScroll(distance: Float): Float { if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) { return 0f } checkPrecondition(abs(scrollToBeConsumed) <= 0.5f) { "entered drag with non-zero pending scroll" } executeRequestsInHighPriorityMode = true scrollToBeConsumed += distance // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if // we have less than 0.5 pixels if (abs(scrollToBeConsumed) > 0.5f) { val preScrollToBeConsumed = scrollToBeConsumed val intDelta = scrollToBeConsumed.fastRoundToInt() var scrolledLayoutInfo = layoutInfoState.value.copyWithScrollDeltaWithoutRemeasure( delta = intDelta, updateAnimations = !hasLookaheadOccurred, ) if (scrolledLayoutInfo != null && this.approachLayoutInfo != null) { // if we were able to scroll the lookahead layout info without remeasure, lets // try to do the same for approach layout info (sometimes they diverge). val scrolledApproachLayoutInfo = approachLayoutInfo?.copyWithScrollDeltaWithoutRemeasure( delta = intDelta, updateAnimations = true, ) if (scrolledApproachLayoutInfo != null) { // we can apply scroll delta for both phases without remeasure approachLayoutInfo = scrolledApproachLayoutInfo } else { // we can't apply scroll delta for approach, so we have to remeasure scrolledLayoutInfo = null } } if (scrolledLayoutInfo != null) { applyMeasureResult( result = scrolledLayoutInfo, isLookingAhead = hasLookaheadOccurred, visibleItemsStayedTheSame = true, ) // we don't need to remeasure, so we only trigger re-placement: placementScopeInvalidator.invalidateScope() notifyPrefetchOnScroll( preScrollToBeConsumed - scrollToBeConsumed, scrolledLayoutInfo, ) } else { remeasurement?.forceRemeasure() notifyPrefetchOnScroll(preScrollToBeConsumed - scrollToBeConsumed, this.layoutInfo) } } // here scrollToBeConsumed is already consumed during the forceRemeasure invocation if (abs(scrollToBeConsumed) <= 0.5f) { // We consumed all of it - we'll hold onto the fractional scroll for later, so report // that we consumed the whole thing return distance } else { val scrollConsumed = distance - scrollToBeConsumed // We did not consume all of it - return the rest to be consumed elsewhere (e.g., // nested scrolling) scrollToBeConsumed = 0f // We're not consuming the rest, give it back return scrollConsumed } } private fun notifyPrefetchOnScroll(delta: Float, layoutInfo: LazyListLayoutInfo) { if (prefetchingEnabled) { with(prefetchStrategy) { prefetchScope.onScroll(delta, layoutInfo) } } } /** * Animate (smooth scroll) to the given item. * * @param index the index to which to scroll. Must be non-negative. * @param scrollOffset the offset that the item should end up after the scroll. Note that * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will * scroll the item further upward (taking it partly offscreen). */ suspend fun animateScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { scroll { LazyLayoutScrollScope(this@LazyListState, this) .animateScrollToItem(index, scrollOffset, NumberOfItemsToTeleport, density) } } /** Updates the state with the new calculated scroll position and consumed scroll. */ internal fun applyMeasureResult( result: LazyListMeasureResult, isLookingAhead: Boolean, visibleItemsStayedTheSame: Boolean = false, ) { // update the prefetch state with the number of nested prefetch items this layout // should use. prefetchState.idealNestedPrefetchCount = result.visibleItemsInfo.size if (!isLookingAhead && hasLookaheadOccurred) { // If there was already a lookahead pass, record this result as approach result approachLayoutInfo = result Snapshot.withoutReadObservation { if ( _lazyLayoutScrollDeltaBetweenPasses.isActive && result.firstVisibleItem?.index == scrollPosition.index && result.firstVisibleItemScrollOffset == scrollPosition.scrollOffset ) { _lazyLayoutScrollDeltaBetweenPasses.stop() } } } else { if (isLookingAhead) { hasLookaheadOccurred = true } canScrollBackward = result.canScrollBackward canScrollForward = result.canScrollForward scrollToBeConsumed -= result.consumedScroll layoutInfoState.value = result if (visibleItemsStayedTheSame) { scrollPosition.updateScrollOffset(result.firstVisibleItemScrollOffset) } else { traceVisibleItems(result) // trace when visible window changed scrollPosition.updateFromMeasureResult(result) if (prefetchingEnabled) { with(prefetchStrategy) { prefetchScope.onVisibleItemsUpdated(result) } } } if (isLookingAhead) { _lazyLayoutScrollDeltaBetweenPasses.updateScrollDeltaForApproach( result.scrollBackAmount, result.density, result.coroutineScope, ) } numMeasurePasses++ } } private fun traceVisibleItems(measureResult: LazyListMeasureResult) { val firstVisibleItem = measureResult.visibleItemsInfo.firstOrNull() val lastVisibleItem = measureResult.visibleItemsInfo.lastOrNull() traceValue("firstVisibleItem:index", firstVisibleItem?.index?.toLong() ?: -1L) traceValue("lastVisibleItem:index", lastVisibleItem?.index?.toLong() ?: -1L) } internal val scrollDeltaBetweenPasses: Float get() = _lazyLayoutScrollDeltaBetweenPasses.scrollDeltaBetweenPasses private val _lazyLayoutScrollDeltaBetweenPasses = LazyLayoutScrollDeltaBetweenPasses() /** * When the user provided custom keys for the items we can try to detect when there were items * added or removed before our current first visible item and keep this item as the first * visible one even given that its index has been changed. The scroll position will not be * updated if [requestScrollToItem] was called since the last time this method was called. */ internal fun updateScrollPositionIfTheFirstItemWasMoved( itemProvider: LazyListItemProvider, firstItemIndex: Int, ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) companion object { /** The default [Saver] implementation for [LazyListState]. */ val Saver: Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { LazyListState( firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1], ) }, ) /** * A [Saver] implementation for [LazyListState] that handles setting a custom * [LazyListPrefetchStrategy]. */ internal fun saver(prefetchStrategy: LazyListPrefetchStrategy): Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { LazyListState( firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1], prefetchStrategy, ) }, ) /** * A [Saver] implementation for [LazyListState] that handles setting a custom * [LazyLayoutCacheWindow]. */ internal fun saver(cacheWindow: LazyLayoutCacheWindow): Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { LazyListState( firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1], cacheWindow = cacheWindow, ) }, ) } } private val EmptyLazyListMeasureResult = LazyListMeasureResult( firstVisibleItem = null, firstVisibleItemScrollOffset = 0, canScrollForward = false, consumedScroll = 0f, measureResult = object : MeasureResult { override val width: Int = 0 override val height: Int = 0 @Suppress("PrimitiveInCollection") override val alignmentLines: Map = emptyMap() override fun placeChildren() {} }, scrollBackAmount = 0f, visibleItemsInfo = emptyList(), viewportStartOffset = 0, viewportEndOffset = 0, totalItemsCount = 0, reverseLayout = false, orientation = Orientation.Vertical, afterContentPadding = 0, mainAxisItemSpacing = 0, remeasureNeeded = false, coroutineScope = CoroutineScope(EmptyCoroutineContext), density = Density(1f), childConstraints = Constraints(), ) private const val NumberOfItemsToTeleport = 100 ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyDsl.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation.lazy import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.internal.JvmDefaultWithCompatibility import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp /** Receiver scope which is used by [LazyColumn] and [LazyRow]. */ @LazyScopeMarker @JvmDefaultWithCompatibility interface LazyListScope { /** * Adds a single item. * * @param key a stable and unique key representing the item. Using the same key for multiple * items in the list is not allowed. Type of the key should be saveable via Bundle on Android. * If null is passed the position in the list will represent the key. When you specify the key * the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the * 'LazyListState'. * @param contentType the type of the content of this item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param content the content of the item */ fun item( key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.() -> Unit, ) { error("The method is not implemented") } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) fun item(key: Any? = null, content: @Composable LazyItemScope.() -> Unit) { item(key, null, content) } /** * Adds a [count] of items. * * @param count the items count * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the list will represent the key. When you * specify the key the scroll position will be maintained based on the key, which means if you * add/remove items before the current visible item the item with the given key will be kept * as the first visible one. This can be overridden by calling 'requestScrollToItem' on the * 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of the * same type could be reused more efficiently. Note that null is a valid type and items of * such type will be considered compatible. * @param itemContent the content displayed by a single item */ fun items( count: Int, key: ((index: Int) -> Any)? = null, contentType: (index: Int) -> Any? = { null }, itemContent: @Composable LazyItemScope.(index: Int) -> Unit, ) { error("The method is not implemented") } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) fun items( count: Int, key: ((index: Int) -> Any)? = null, itemContent: @Composable LazyItemScope.(index: Int) -> Unit, ) { items(count, key, { null }, itemContent) } /** * Adds a sticky header item, which will remain pinned even when scrolling after it. The header * will remain pinned until the next header will take its place. * * @sample androidx.compose.foundation.samples.StickyHeaderListSample * @param key a stable and unique key representing the item. Using the same key for multiple * items in the list is not allowed. Type of the key should be saveable via Bundle on Android. * If null is passed the position in the list will represent the key. When you specify the key * the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the * 'LazyListState'. * @param contentType the type of the content of this item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param content the content of the header */ @Deprecated( "Please use the overload with indexing capabilities.", level = DeprecationLevel.HIDDEN, replaceWith = ReplaceWith("stickyHeader(key, contentType, { _ -> content() })"), ) fun stickyHeader( key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.() -> Unit, ) = stickyHeader(key, contentType) { _ -> content() } /** * Adds a sticky header item, which will remain pinned even when scrolling after it. The header * will remain pinned until the next header will take its place. * * @sample androidx.compose.foundation.samples.StickyHeaderListSample * @sample androidx.compose.foundation.samples.StickyHeaderHeaderIndexSample * @param key a stable and unique key representing the item. Using the same key for multiple * items in the list is not allowed. Type of the key should be saveable via Bundle on Android. * If null is passed the position in the list will represent the key. When you specify the key * the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the * 'LazyListState'. * @param contentType the type of the content of this item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param content the content of the header, the header index is provided, this is the item * position within the total set of items in this lazy list (the global index). */ fun stickyHeader( key: Any? = null, contentType: Any? = null, content: @Composable LazyItemScope.(Int) -> Unit, ) { item(key, contentType) { content.invoke(this, 0) } } } /** * Adds a list of items. * * @param items the data list * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the list will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyListScope.items( items: List, noinline key: ((item: T) -> Any)? = null, noinline contentType: (item: T) -> Any? = { null }, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(items[index]) } else null, contentType = { index: Int -> contentType(items[index]) }, ) { itemContent(items[it]) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) inline fun LazyListScope.items( items: List, noinline key: ((item: T) -> Any)? = null, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit, ) = items(items, key, itemContent = itemContent) /** * Adds a list of items where the content of an item is aware of its index. * * @param items the data list * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the list will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyListScope.itemsIndexed( items: List, noinline key: ((index: Int, item: T) -> Any)? = null, crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null }, crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(index, items[index]) } else null, contentType = { index -> contentType(index, items[index]) }, ) { itemContent(it, items[it]) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) inline fun LazyListScope.itemsIndexed( items: List, noinline key: ((index: Int, item: T) -> Any)? = null, crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit, ) = itemsIndexed(items, key, itemContent = itemContent) /** * Adds an array of items. * * @param items the data array * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the list will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyListScope.items( items: Array, noinline key: ((item: T) -> Any)? = null, noinline contentType: (item: T) -> Any? = { null }, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(items[index]) } else null, contentType = { index: Int -> contentType(items[index]) }, ) { itemContent(items[it]) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) inline fun LazyListScope.items( items: Array, noinline key: ((item: T) -> Any)? = null, crossinline itemContent: @Composable LazyItemScope.(item: T) -> Unit, ) = items(items, key, itemContent = itemContent) /** * Adds an array of items where the content of an item is aware of its index. * * @param items the data array * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the list is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the list will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the 'LazyListState'. * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyListScope.itemsIndexed( items: Array, noinline key: ((index: Int, item: T) -> Any)? = null, crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null }, crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(index, items[index]) } else null, contentType = { index -> contentType(index, items[index]) }, ) { itemContent(it, items[it]) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) inline fun LazyListScope.itemsIndexed( items: Array, noinline key: ((index: Int, item: T) -> Any)? = null, crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit, ) = itemsIndexed(items, key, itemContent = itemContent) /** * The horizontally scrolling list that only composes and lays out the currently visible items. The * [content] block defines a DSL which allows you to emit items of different types. For example you * can use [LazyListScope.item] to add a single item and [LazyListScope.items] to add a list of * items. * * @sample androidx.compose.foundation.samples.LazyRowSample * @param modifier the modifier to apply to this layout * @param state the state object to be used to control or observe the list's state * @param contentPadding a padding around the whole content. This will add padding for the content * after it has been clipped, which is not possible via [modifier] param. You can use it to add a * padding before the first item or after the last one. If you want to add a spacing between each * item use [horizontalArrangement]. * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are laid * out in the reverse order and [LazyListState.firstVisibleItemIndex] == 0 means that row is * scrolled to the end. Note that [reverseLayout] does not change the behavior of * [horizontalArrangement], e.g. with [Arrangement.Start] [123###] becomes [321###]. * @param horizontalArrangement The horizontal arrangement of the layout's children. This allows to * add a spacing between items and specify the arrangement of the items when we have not enough of * them to fill the whole minimum size. * @param verticalAlignment the vertical alignment applied to the items * @param flingBehavior logic describing fling behavior. * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is * allowed. You can still scroll programmatically using the state even when it is disabled. * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not * need to use Modifier.overscroll separately. * @param content a block which describes the content. Inside this block you can use methods like * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items. */ @Composable fun LazyRow( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End, verticalAlignment: Alignment.Vertical = Alignment.Top, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), content: LazyListScope.() -> Unit, ) { LazyList( modifier = modifier, state = state, contentPadding = contentPadding, verticalAlignment = verticalAlignment, horizontalArrangement = horizontalArrangement, isVertical = false, flingBehavior = flingBehavior, reverseLayout = reverseLayout, userScrollEnabled = userScrollEnabled, overscrollEffect = overscrollEffect, content = content, ) } /** * The vertically scrolling list that only composes and lays out the currently visible items. The * [content] block defines a DSL which allows you to emit items of different types. For example you * can use [LazyListScope.item] to add a single item and [LazyListScope.items] to add a list of * items. * * @sample androidx.compose.foundation.samples.LazyColumnSample * @param modifier the modifier to apply to this layout. * @param state the state object to be used to control or observe the list's state. * @param contentPadding a padding around the whole content. This will add padding for the. content * after it has been clipped, which is not possible via [modifier] param. You can use it to add a * padding before the first item or after the last one. If you want to add a spacing between each * item use [verticalArrangement]. * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are laid * out in the reverse order and [LazyListState.firstVisibleItemIndex] == 0 means that column is * scrolled to the bottom. Note that [reverseLayout] does not change the behavior of * [verticalArrangement], e.g. with [Arrangement.Top] (top) 123### (bottom) becomes (top) 321### * (bottom). * @param verticalArrangement The vertical arrangement of the layout's children. This allows to add * a spacing between items and specify the arrangement of the items when we have not enough of * them to fill the whole minimum size. * @param horizontalAlignment the horizontal alignment applied to the items. * @param flingBehavior logic describing fling behavior. * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is * allowed. You can still scroll programmatically using the state even when it is disabled * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not * need to use Modifier.overscroll separately. * @param content a block which describes the content. Inside this block you can use methods like * [LazyListScope.item] to add a single item or [LazyListScope.items] to add a list of items. */ @Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), content: LazyListScope.() -> Unit, ) { LazyList( modifier = modifier, state = state, contentPadding = contentPadding, flingBehavior = flingBehavior, horizontalAlignment = horizontalAlignment, verticalArrangement = verticalArrangement, isVertical = true, reverseLayout = reverseLayout, userScrollEnabled = userScrollEnabled, overscrollEffect = overscrollEffect, content = content, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, content: LazyListScope.() -> Unit, ) { LazyColumn( modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, overscrollEffect = rememberOverscrollEffect(), content = content, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun LazyColumn( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalAlignment: Alignment.Horizontal = Alignment.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), content: LazyListScope.() -> Unit, ) { LazyColumn( modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, verticalArrangement = verticalArrangement, horizontalAlignment = horizontalAlignment, flingBehavior = flingBehavior, userScrollEnabled = true, content = content, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun LazyRow( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End, verticalAlignment: Alignment.Vertical = Alignment.Top, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, content: LazyListScope.() -> Unit, ) { LazyRow( modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, horizontalArrangement = horizontalArrangement, verticalAlignment = verticalAlignment, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, overscrollEffect = rememberOverscrollEffect(), content = content, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun LazyRow( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End, verticalAlignment: Alignment.Vertical = Alignment.Top, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), content: LazyListScope.() -> Unit, ) { LazyRow( modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, horizontalArrangement = horizontalArrangement, verticalAlignment = verticalAlignment, flingBehavior = flingBehavior, userScrollEnabled = true, content = content, ) } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/LazyListItemProvider.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.foundation.lazy import androidx.collection.IntList import androidx.compose.foundation.lazy.layout.LazyLayoutItemProvider import androidx.compose.foundation.lazy.layout.LazyLayoutKeyIndexMap import androidx.compose.foundation.lazy.layout.LazyLayoutPinnableItem import androidx.compose.foundation.lazy.layout.NearestRangeKeyIndexMap import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.referentialEqualityPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState internal interface LazyListItemProvider : LazyLayoutItemProvider { val keyIndexMap: LazyLayoutKeyIndexMap /** The list of indexes of the sticky header items */ val headerIndexes: IntList /** The scope used by the item content lambdas */ val itemScope: LazyItemScopeImpl } @Composable internal fun rememberLazyListItemProviderLambda( state: LazyListState, content: LazyListScope.() -> Unit, ): () -> LazyListItemProvider { val latestContent = rememberUpdatedState(content) return remember(state) { val scope = LazyItemScopeImpl() val intervalContentState = derivedStateOf(referentialEqualityPolicy()) { LazyListIntervalContent(latestContent.value) } val itemProviderState = derivedStateOf(referentialEqualityPolicy()) { val intervalContent = intervalContentState.value val map = NearestRangeKeyIndexMap(state.nearestRange, intervalContent) LazyListItemProviderImpl( state = state, intervalContent = intervalContent, itemScope = scope, keyIndexMap = map, ) } itemProviderState::value } } private class LazyListItemProviderImpl constructor( private val state: LazyListState, private val intervalContent: LazyListIntervalContent, override val itemScope: LazyItemScopeImpl, override val keyIndexMap: LazyLayoutKeyIndexMap, ) : LazyListItemProvider { override val itemCount: Int get() = intervalContent.itemCount @Composable override fun Item(index: Int, key: Any) { LazyLayoutPinnableItem(key, index, state.pinnedItems) { intervalContent.withInterval(index) { localIndex, content -> content.item(itemScope, localIndex) } } } override fun getKey(index: Int): Any = keyIndexMap.getKey(index) ?: intervalContent.getKey(index) override fun getContentType(index: Int): Any? = intervalContent.getContentType(index) override val headerIndexes: IntList get() = intervalContent.headerIndexes override fun getIndex(key: Any): Int = keyIndexMap.getIndex(key) override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is LazyListItemProviderImpl) return false // the identity of this class is represented by intervalContent object. // having equals() allows us to skip items recomposition when intervalContent didn't change return intervalContent == other.intervalContent } override fun hashCode(): Int { return intervalContent.hashCode() } } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGrid.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.foundation.lazy.grid import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.checkScrollableContainerConstraints import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.internal.requirePreconditionNotNull import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.lazy.layout.CacheWindowLogic import androidx.compose.foundation.lazy.layout.LazyLayout import androidx.compose.foundation.lazy.layout.LazyLayoutMeasurePolicy import androidx.compose.foundation.lazy.layout.StickyItemsPlacement import androidx.compose.foundation.lazy.layout.calculateLazyLayoutPinnedIndices import androidx.compose.foundation.lazy.layout.lazyLayoutBeyondBoundsModifier import androidx.compose.foundation.lazy.layout.lazyLayoutSemantics import androidx.compose.foundation.scrollableArea import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.GraphicsContext import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalGraphicsContext import androidx.compose.ui.platform.LocalScrollCaptureInProgress import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.trace import kotlinx.coroutines.CoroutineScope @OptIn(ExperimentalFoundationApi::class) @Composable internal fun LazyGrid( /** Modifier to be applied for the inner layout */ modifier: Modifier = Modifier, /** State controlling the scroll position */ state: LazyGridState, /** Prefix sums of cross axis sizes of slots per line, e.g. the columns for vertical grid. */ slots: LazyGridSlotsProvider, /** The inner padding to be added for the whole content (not for each individual item) */ contentPadding: PaddingValues = PaddingValues(0.dp), /** reverse the direction of scrolling and layout */ reverseLayout: Boolean = false, /** The layout orientation of the grid */ isVertical: Boolean, /** fling behavior to be used for flinging */ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), /** Whether scrolling via the user gestures is allowed. */ userScrollEnabled: Boolean, /** The overscroll effect to render and dispatch events to */ overscrollEffect: OverscrollEffect?, /** The vertical arrangement for items/lines. */ verticalArrangement: Arrangement.Vertical, /** The horizontal arrangement for items/lines. */ horizontalArrangement: Arrangement.Horizontal, /** The content of the grid */ content: LazyGridScope.() -> Unit, ) { val itemProviderLambda = rememberLazyGridItemProviderLambda(state, content) val semanticState = rememberLazyGridSemanticState(state, reverseLayout) val coroutineScope = rememberCoroutineScope() val graphicsContext = LocalGraphicsContext.current val stickyHeadersEnabled = !LocalScrollCaptureInProgress.current val measurePolicy = rememberLazyGridMeasurePolicy( itemProviderLambda, state, slots, contentPadding, reverseLayout, isVertical, horizontalArrangement, verticalArrangement, coroutineScope, graphicsContext, if (stickyHeadersEnabled) StickyItemsPlacement.StickToTopPlacement else null, ) val orientation = if (isVertical) Orientation.Vertical else Orientation.Horizontal val beyondBoundsModifier = if (userScrollEnabled) { Modifier.lazyLayoutBeyondBoundsModifier( state = rememberLazyGridBeyondBoundsState(state = state), beyondBoundsInfo = state.beyondBoundsInfo, reverseLayout = reverseLayout, orientation = orientation, ) } else { Modifier } LazyLayout( modifier = modifier .then(state.remeasurementModifier) .then(state.awaitLayoutModifier) .lazyLayoutSemantics( itemProviderLambda = itemProviderLambda, state = semanticState, orientation = orientation, userScrollEnabled = userScrollEnabled, reverseScrolling = reverseLayout, ) .then(beyondBoundsModifier) .then(state.itemAnimator.modifier) .scrollableArea( state = state, orientation = orientation, enabled = userScrollEnabled, reverseScrolling = reverseLayout, flingBehavior = flingBehavior, interactionSource = state.internalInteractionSource, overscrollEffect = overscrollEffect, ), prefetchState = state.prefetchState, measurePolicy = measurePolicy, itemProvider = itemProviderLambda, ) } /** lazy grid slots configuration */ internal class LazyGridSlots(val sizes: IntArray, val positions: IntArray) @OptIn(ExperimentalFoundationApi::class) @Composable private fun rememberLazyGridMeasurePolicy( /** Items provider of the list. */ itemProviderLambda: () -> LazyGridItemProvider, /** The state of the list. */ state: LazyGridState, /** Prefix sums of cross axis sizes of slots of the grid. */ slots: LazyGridSlotsProvider, /** The inner padding to be added for the whole content(nor for each individual item) */ contentPadding: PaddingValues, /** reverse the direction of scrolling and layout */ reverseLayout: Boolean, /** The layout orientation of the list */ isVertical: Boolean, /** The horizontal arrangement for items */ horizontalArrangement: Arrangement.Horizontal?, /** The vertical arrangement for items */ verticalArrangement: Arrangement.Vertical?, /** Coroutine scope for item animations */ coroutineScope: CoroutineScope, /** Used for creating graphics layers */ graphicsContext: GraphicsContext, /** Configures the placement of sticky items */ stickyItemsScrollBehavior: StickyItemsPlacement?, ) = remember( state, slots, contentPadding, reverseLayout, isVertical, horizontalArrangement, verticalArrangement, graphicsContext, ) { LazyLayoutMeasurePolicy { containerConstraints -> state.measurementScopeInvalidator.attachToScope() // Tracks if the lookahead pass has occurred val isInLookaheadScope = state.hasLookaheadOccurred || isLookingAhead checkScrollableContainerConstraints( containerConstraints, if (isVertical) Orientation.Vertical else Orientation.Horizontal, ) // resolve content paddings val startPadding = if (isVertical) { contentPadding.calculateLeftPadding(layoutDirection).roundToPx() } else { // in horizontal configuration, padding is reversed by placeRelative contentPadding.calculateStartPadding(layoutDirection).roundToPx() } val endPadding = if (isVertical) { contentPadding.calculateRightPadding(layoutDirection).roundToPx() } else { // in horizontal configuration, padding is reversed by placeRelative contentPadding.calculateEndPadding(layoutDirection).roundToPx() } val topPadding = contentPadding.calculateTopPadding().roundToPx() val bottomPadding = contentPadding.calculateBottomPadding().roundToPx() val totalVerticalPadding = topPadding + bottomPadding val totalHorizontalPadding = startPadding + endPadding val totalMainAxisPadding = if (isVertical) totalVerticalPadding else totalHorizontalPadding val beforeContentPadding = when { isVertical && !reverseLayout -> topPadding isVertical && reverseLayout -> bottomPadding !isVertical && !reverseLayout -> startPadding else -> endPadding // !isVertical && reverseLayout } val afterContentPadding = totalMainAxisPadding - beforeContentPadding val contentConstraints = containerConstraints.offset(-totalHorizontalPadding, -totalVerticalPadding) val itemProvider = itemProviderLambda() val spanLayoutProvider = itemProvider.spanLayoutProvider val resolvedSlots = slots.invoke(density = this, constraints = contentConstraints) val slotsPerLine = resolvedSlots.sizes.size spanLayoutProvider.slotsPerLine = slotsPerLine val spaceBetweenLinesDp = if (isVertical) { requirePreconditionNotNull(verticalArrangement) { "null verticalArrangement when isVertical == true" } .spacing } else { requirePreconditionNotNull(horizontalArrangement) { "null horizontalArrangement when isVertical == false" } .spacing } val spaceBetweenLines = spaceBetweenLinesDp.roundToPx() val itemsCount = itemProvider.itemCount // can be negative if the content padding is larger than the max size from constraints val mainAxisAvailableSize = if (isVertical) { containerConstraints.maxHeight - totalVerticalPadding } else { containerConstraints.maxWidth - totalHorizontalPadding } val visualItemOffset = if (!reverseLayout || mainAxisAvailableSize > 0) { IntOffset(startPadding, topPadding) } else { // When layout is reversed and paddings together take >100% of the available // space, // layout size is coerced to 0 when positioning. To take that space into // account, // we offset start padding by negative space between paddings. IntOffset( if (isVertical) startPadding else startPadding + mainAxisAvailableSize, if (isVertical) topPadding + mainAxisAvailableSize else topPadding, ) } val measuredItemProvider = object : LazyGridMeasuredItemProvider(itemProvider, this, spaceBetweenLines) { override fun createItem( index: Int, key: Any, contentType: Any?, crossAxisSize: Int, mainAxisSpacing: Int, placeables: List, constraints: Constraints, lane: Int, span: Int, ) = LazyGridMeasuredItem( index = index, key = key, isVertical = isVertical, crossAxisSize = crossAxisSize, mainAxisSpacing = mainAxisSpacing, reverseLayout = reverseLayout, layoutDirection = layoutDirection, beforeContentPadding = beforeContentPadding, afterContentPadding = afterContentPadding, visualOffset = visualItemOffset, placeables = placeables, contentType = contentType, animator = state.itemAnimator, constraints = constraints, lane = lane, span = span, ) } val measuredLineProvider = object : LazyGridMeasuredLineProvider( isVertical = isVertical, slots = resolvedSlots, gridItemsCount = itemsCount, spaceBetweenLines = spaceBetweenLines, measuredItemProvider = measuredItemProvider, spanLayoutProvider = spanLayoutProvider, ) { override fun createLine( index: Int, items: Array, spans: List, mainAxisSpacing: Int, ) = LazyGridMeasuredLine( index = index, items = items, spans = spans, slots = resolvedSlots, isVertical = isVertical, mainAxisSpacing = mainAxisSpacing, ) } val prefetchInfoRetriever: (line: Int) -> List> = { line -> val lineConfiguration = spanLayoutProvider.getLineConfiguration(line) var index = lineConfiguration.firstItemIndex var slot = 0 val result = ArrayList>(lineConfiguration.spans.size) lineConfiguration.spans.fastForEach { val span = it.currentLineSpan result.add(index to measuredLineProvider.childConstraints(slot, span)) ++index slot += span } result } val lineIndexProvider: (itemIndex: Int) -> Int = { itemIndex -> spanLayoutProvider.getLineIndexOfItem(itemIndex) } val firstVisibleLineIndex: Int val firstVisibleLineScrollOffset: Int Snapshot.withoutReadObservation { val index = state.updateScrollPositionIfTheFirstItemWasMoved( itemProvider, state.firstVisibleItemIndex, ) if (index < itemsCount || itemsCount <= 0) { firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(index) firstVisibleLineScrollOffset = state.firstVisibleItemScrollOffset } else { // the data set has been updated and now we have less items that we were // scrolled to before firstVisibleLineIndex = spanLayoutProvider.getLineIndexOfItem(itemsCount - 1) firstVisibleLineScrollOffset = 0 } } val pinnedItems = itemProvider.calculateLazyLayoutPinnedIndices( state.pinnedItems, state.beyondBoundsInfo, ) val scrollToBeConsumed = if (isLookingAhead || !isInLookaheadScope) { state.scrollToBeConsumed } else { state.scrollDeltaBetweenPasses } // todo: wrap with snapshot when b/341782245 is resolved val measureResult = measureLazyGrid( itemsCount = itemsCount, measuredLineProvider = measuredLineProvider, measuredItemProvider = measuredItemProvider, mainAxisAvailableSize = mainAxisAvailableSize, beforeContentPadding = beforeContentPadding, afterContentPadding = afterContentPadding, spaceBetweenLines = spaceBetweenLines, firstVisibleLineIndex = firstVisibleLineIndex, firstVisibleLineScrollOffset = firstVisibleLineScrollOffset, scrollToBeConsumed = scrollToBeConsumed, constraints = contentConstraints, isVertical = isVertical, verticalArrangement = verticalArrangement, horizontalArrangement = horizontalArrangement, reverseLayout = reverseLayout, density = this, itemAnimator = state.itemAnimator, slotsPerLine = slotsPerLine, pinnedItems = pinnedItems, isInLookaheadScope = isInLookaheadScope, isLookingAhead = isLookingAhead, approachLayoutInfo = state.approachLayoutInfo, coroutineScope = coroutineScope, placementScopeInvalidator = state.placementScopeInvalidator, prefetchInfoRetriever = prefetchInfoRetriever, lineIndexProvider = lineIndexProvider, graphicsContext = graphicsContext, stickyItemsScrollBehavior = stickyItemsScrollBehavior, layout = { width, height, placement -> layout( containerConstraints.constrainWidth(width + totalHorizontalPadding), containerConstraints.constrainHeight(height + totalVerticalPadding), emptyMap(), placement, ) }, ) state.applyMeasureResult(measureResult, isLookingAhead = isLookingAhead) // apply keep around after updating the strategy with measure result. (state.prefetchStrategy as? CacheWindowLogic)?.keepAroundItems( measureResult.orientation, measureResult.visibleItemsInfo, measuredLineProvider, ) measureResult } } @OptIn(ExperimentalFoundationApi::class) private fun CacheWindowLogic.keepAroundItems( orientation: Orientation, visibleItemsList: List, measuredLineProvider: LazyGridMeasuredLineProvider, ) { trace("compose:lazy:cache_window:keepAroundItems") { // only run if window and new layout info is available if (hasValidBounds() && visibleItemsList.isNotEmpty()) { val firstVisibleItemIndex = visibleItemsList.first().lineIndex(orientation) val lastVisibleItemIndex = visibleItemsList.last().lineIndex(orientation) // we must send a message in case of changing directions for items // that were keep around and become prefetch forward for (line in prefetchWindowStartLine.. Unit, ) { LazyGrid( slots = rememberColumnWidthSums(columns, horizontalArrangement), modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, isVertical = true, horizontalArrangement = horizontalArrangement, verticalArrangement = verticalArrangement, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, overscrollEffect = overscrollEffect, content = content, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun LazyVerticalGrid( columns: GridCells, modifier: Modifier = Modifier, state: LazyGridState = rememberLazyGridState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = if (!reverseLayout) Arrangement.Top else Arrangement.Bottom, horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, content: LazyGridScope.() -> Unit, ) { LazyVerticalGrid( columns = columns, modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, verticalArrangement = verticalArrangement, horizontalArrangement = horizontalArrangement, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, overscrollEffect = rememberOverscrollEffect(), content = content, ) } /** * A lazy horizontal grid layout. It composes only visible columns of the grid. * * Sample: * * @sample androidx.compose.foundation.samples.LazyHorizontalGridSample * * Sample with custom item spans: * * @sample androidx.compose.foundation.samples.LazyHorizontalGridSpanSample * @param rows a class describing how cells form rows, see [GridCells] doc for more information * @param modifier the modifier to apply to this layout * @param state the state object to be used to control or observe the list's state * @param contentPadding specify a padding around the whole content * @param reverseLayout reverse the direction of scrolling and layout. When `true`, items are laid * out in the reverse order and [LazyGridState.firstVisibleItemIndex] == 0 means that grid is * scrolled to the end. Note that [reverseLayout] does not change the behavior of * [horizontalArrangement], e.g. with [Arrangement.Start] [123###] becomes [321###]. * @param verticalArrangement The vertical arrangement of the layout's children * @param horizontalArrangement The horizontal arrangement of the layout's children * @param flingBehavior logic describing fling behavior * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is * allowed. You can still scroll programmatically using the state even when it is disabled. * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this * layout. Note that the [OverscrollEffect.node] will be applied internally as well - you do not * need to use Modifier.overscroll separately. * @param content the [LazyGridScope] which describes the content */ @Composable fun LazyHorizontalGrid( rows: GridCells, modifier: Modifier = Modifier, state: LazyGridState = rememberLazyGridState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End, verticalArrangement: Arrangement.Vertical = Arrangement.Top, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), content: LazyGridScope.() -> Unit, ) { LazyGrid( slots = rememberRowHeightSums(rows, verticalArrangement), modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, isVertical = false, horizontalArrangement = horizontalArrangement, verticalArrangement = verticalArrangement, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, overscrollEffect = overscrollEffect, content = content, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun LazyHorizontalGrid( rows: GridCells, modifier: Modifier = Modifier, state: LazyGridState = rememberLazyGridState(), contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, horizontalArrangement: Arrangement.Horizontal = if (!reverseLayout) Arrangement.Start else Arrangement.End, verticalArrangement: Arrangement.Vertical = Arrangement.Top, flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), userScrollEnabled: Boolean = true, content: LazyGridScope.() -> Unit, ) { LazyHorizontalGrid( rows = rows, modifier = modifier, state = state, contentPadding = contentPadding, reverseLayout = reverseLayout, horizontalArrangement = horizontalArrangement, verticalArrangement = verticalArrangement, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, overscrollEffect = rememberOverscrollEffect(), content = content, ) } /** Returns prefix sums of column widths. */ @Composable private fun rememberColumnWidthSums( columns: GridCells, horizontalArrangement: Arrangement.Horizontal, ) = remember(columns, horizontalArrangement) { GridSlotCache { constraints -> requirePrecondition(constraints.maxWidth != Constraints.Infinity) { "LazyVerticalGrid's width should be bound by parent." } val gridWidth = constraints.maxWidth with(columns) { calculateCrossAxisCellSizes(gridWidth, horizontalArrangement.spacing.roundToPx()) .toIntArray() .let { sizes -> val positions = IntArray(sizes.size) with(horizontalArrangement) { arrange(gridWidth, sizes, LayoutDirection.Ltr, positions) } LazyGridSlots(sizes, positions) } } } } /** Returns prefix sums of row heights. */ @Composable private fun rememberRowHeightSums(rows: GridCells, verticalArrangement: Arrangement.Vertical) = remember(rows, verticalArrangement) { GridSlotCache { constraints -> requirePrecondition(constraints.maxHeight != Constraints.Infinity) { "LazyHorizontalGrid's height should be bound by parent." } val gridHeight = constraints.maxHeight with(rows) { calculateCrossAxisCellSizes(gridHeight, verticalArrangement.spacing.roundToPx()) .toIntArray() .let { sizes -> val positions = IntArray(sizes.size) with(verticalArrangement) { arrange(gridHeight, sizes, positions) } LazyGridSlots(sizes, positions) } } } } // Note: Implementing function interface is prohibited in K/JS (class A: () -> Unit) // therefore we workaround this limitation by inheriting a fun interface instead internal fun interface LazyGridSlotsProvider { fun invoke(density: Density, constraints: Constraints): LazyGridSlots } /** measurement cache to avoid recalculating row/column sizes on each scroll. */ private class GridSlotCache(private val calculation: Density.(Constraints) -> LazyGridSlots) : LazyGridSlotsProvider { private var cachedConstraints = Constraints() private var cachedDensity: Float = 0f private var cachedSizes: LazyGridSlots? = null override fun invoke(density: Density, constraints: Constraints): LazyGridSlots { with(density) { if ( cachedSizes != null && cachedConstraints == constraints && cachedDensity == this.density ) { return cachedSizes!! } cachedConstraints = constraints cachedDensity = this.density return calculation(constraints).also { cachedSizes = it } } } } /** * This class describes the count and the sizes of columns in vertical grids, or rows in horizontal * grids. */ @Stable interface GridCells { /** * Calculates the number of cells and their cross axis size based on [availableSize] and * [spacing]. * * For example, in vertical grids, [spacing] is passed from the grid's [Arrangement.Horizontal]. * The [Arrangement.Horizontal] will also be used to arrange items in a row if the grid is wider * than the calculated sum of columns. * * Note that the calculated cross axis sizes will be considered in an RTL-aware manner -- if the * grid is vertical and the layout direction is RTL, the first width in the returned list will * correspond to the rightmost column. * * @param availableSize available size on cross axis, e.g. width of [LazyVerticalGrid]. * @param spacing cross axis spacing, e.g. horizontal spacing for [LazyVerticalGrid]. The * spacing is passed from the corresponding [Arrangement] param of the lazy grid. */ fun Density.calculateCrossAxisCellSizes(availableSize: Int, spacing: Int): List /** * Defines a grid with fixed number of rows or columns. * * For example, for the vertical [LazyVerticalGrid] Fixed(3) would mean that there are 3 columns * 1/3 of the parent width. */ class Fixed(private val count: Int) : GridCells { init { requirePrecondition(count > 0) { "Provided count should be larger than zero" } } override fun Density.calculateCrossAxisCellSizes( availableSize: Int, spacing: Int, ): List { return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing) } override fun hashCode(): Int { return -count // Different sign from Adaptive. } override fun equals(other: Any?): Boolean { return other is Fixed && count == other.count } } /** * Defines a grid with as many rows or columns as possible on the condition that every cell has * at least [minSize] space and all extra space distributed evenly. * * For example, for the vertical [LazyVerticalGrid] Adaptive(20.dp) would mean that there will * be as many columns as possible and every column will be at least 20.dp and all the columns * will have equal width. If the screen is 88.dp wide then there will be 4 columns 22.dp each. */ class Adaptive(private val minSize: Dp) : GridCells { init { requirePrecondition(minSize > 0.dp) { "Provided min size should be larger than zero." } } override fun Density.calculateCrossAxisCellSizes( availableSize: Int, spacing: Int, ): List { val count = maxOf((availableSize + spacing) / (minSize.roundToPx() + spacing), 1) return calculateCellsCrossAxisSizeImpl(availableSize, count, spacing) } override fun hashCode(): Int { return minSize.hashCode() } override fun equals(other: Any?): Boolean { return other is Adaptive && minSize == other.minSize } } /** * Defines a grid with as many rows or columns as possible on the condition that every cell * takes exactly [size] space. The remaining space will be arranged through [LazyGrid] * arrangements on corresponding axis. If [size] is larger than container size, the cell will be * size to match the container. * * For example, for the vertical [LazyGrid] FixedSize(20.dp) would mean that there will be as * many columns as possible and every column will be exactly 20.dp. If the screen is 88.dp wide * tne there will be 4 columns 20.dp each with remaining 8.dp distributed through * [Arrangement.Horizontal]. */ class FixedSize(private val size: Dp) : GridCells { init { requirePrecondition(size > 0.dp) { "Provided size should be larger than zero." } } override fun Density.calculateCrossAxisCellSizes( availableSize: Int, spacing: Int, ): List { val cellSize = size.roundToPx() return if (cellSize + spacing < availableSize + spacing) { val cellCount = (availableSize + spacing) / (cellSize + spacing) List(cellCount) { cellSize } } else { List(1) { availableSize } } } override fun hashCode(): Int { return size.hashCode() } override fun equals(other: Any?): Boolean { return other is FixedSize && size == other.size } } } private fun calculateCellsCrossAxisSizeImpl( gridSize: Int, slotCount: Int, spacing: Int, ): List { val gridSizeWithoutSpacing = gridSize - spacing * (slotCount - 1) val slotSize = gridSizeWithoutSpacing / slotCount val remainingPixels = gridSizeWithoutSpacing % slotCount return List(slotCount) { slotSize + if (it < remainingPixels) 1 else 0 } } /** Receiver scope which is used by [LazyVerticalGrid]. */ @LazyGridScopeMarker sealed interface LazyGridScope { /** * Adds a single item to the scope. * * @param key a stable and unique key representing the item. Using the same key for multiple * items in the grid is not allowed. Type of the key should be saveable via Bundle on Android. * If null is passed the position in the grid will represent the key. When you specify the key * the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling [LazyGridState.requestScrollToItem]. * @param span the span of the item. Default is 1x1. It is good practice to leave it `null` when * this matches the intended behavior, as providing a custom implementation impacts * performance * @param contentType the type of the content of this item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param content the content of the item */ fun item( key: Any? = null, span: (LazyGridItemSpanScope.() -> GridItemSpan)? = null, contentType: Any? = null, content: @Composable LazyGridItemScope.() -> Unit, ) /** * Adds a [count] of items. * * @param count the items count * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the grid will represent the key. When you * specify the key the scroll position will be maintained based on the key, which means if you * add/remove items before the current visible item the item with the given key will be kept * as the first visible one.This can be overridden by calling * [LazyGridState.requestScrollToItem]. * @param span define custom spans for the items. Default is 1x1. It is good practice to leave * it `null` when this matches the intended behavior, as providing a custom implementation * impacts performance * @param contentType a factory of the content types for the item. The item compositions of the * same type could be reused more efficiently. Note that null is a valid type and items of * such type will be considered compatible. * @param itemContent the content displayed by a single item */ fun items( count: Int, key: ((index: Int) -> Any)? = null, span: (LazyGridItemSpanScope.(index: Int) -> GridItemSpan)? = null, contentType: (index: Int) -> Any? = { null }, itemContent: @Composable LazyGridItemScope.(index: Int) -> Unit, ) /** * Adds a sticky header item, which will remain pinned even when scrolling after it. The header * will remain pinned until the next header will take its place. Sticky Headers are full span * items, that is, they will occupy [LazyGridItemSpanScope.maxLineSpan]. * * @sample androidx.compose.foundation.samples.StickyHeaderGridSample * @param key a stable and unique key representing the item. Using the same key for multiple * items in the list is not allowed. Type of the key should be saveable via Bundle on Android. * If null is passed the position in the list will represent the key. When you specify the key * the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling 'requestScrollToItem' on the * 'LazyGridState'. * @param contentType the type of the content of this item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such * type will be considered compatible. * @param content the content of the header. The header index is provided, this is the item * position within the total set of items in this lazy list (the global index). */ fun stickyHeader( key: Any? = null, contentType: Any? = null, content: @Composable LazyGridItemScope.(Int) -> Unit, ) } /** * Adds a list of items. * * @param items the data list * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the grid will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling [LazyGridState.requestScrollToItem]. * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it * `null` when this matches the intended behavior, as providing a custom implementation impacts * performance * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyGridScope.items( items: List, noinline key: ((item: T) -> Any)? = null, noinline span: (LazyGridItemSpanScope.(item: T) -> GridItemSpan)? = null, noinline contentType: (item: T) -> Any? = { null }, crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(items[index]) } else null, span = if (span != null) { { span(items[it]) } } else null, contentType = { index: Int -> contentType(items[index]) }, ) { itemContent(items[it]) } /** * Adds a list of items where the content of an item is aware of its index. * * @param items the data list * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the grid will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling [LazyGridState.requestScrollToItem]. * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it * `null` when this matches the intended behavior, as providing a custom implementation impacts * performance * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyGridScope.itemsIndexed( items: List, noinline key: ((index: Int, item: T) -> Any)? = null, noinline span: (LazyGridItemSpanScope.(index: Int, item: T) -> GridItemSpan)? = null, crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null }, crossinline itemContent: @Composable LazyGridItemScope.(index: Int, item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(index, items[index]) } else null, span = if (span != null) { { span(it, items[it]) } } else null, contentType = { index -> contentType(index, items[index]) }, ) { itemContent(it, items[it]) } /** * Adds an array of items. * * @param items the data array * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the grid will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one.This can be overridden by calling [LazyGridState.requestScrollToItem]. * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it * `null` when this matches the intended behavior, as providing a custom implementation impacts * performance * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyGridScope.items( items: Array, noinline key: ((item: T) -> Any)? = null, noinline span: (LazyGridItemSpanScope.(item: T) -> GridItemSpan)? = null, noinline contentType: (item: T) -> Any? = { null }, crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(items[index]) } else null, span = if (span != null) { { span(items[it]) } } else null, contentType = { index: Int -> contentType(items[index]) }, ) { itemContent(items[it]) } /** * Adds an array of items where the content of an item is aware of its index. * * @param items the data array * @param key a factory of stable and unique keys representing the item. Using the same key for * multiple items in the grid is not allowed. Type of the key should be saveable via Bundle on * Android. If null is passed the position in the grid will represent the key. When you specify * the key the scroll position will be maintained based on the key, which means if you add/remove * items before the current visible item the item with the given key will be kept as the first * visible one. This can be overridden by calling [LazyGridState.requestScrollToItem]. * @param span define custom spans for the items. Default is 1x1. It is good practice to leave it * `null` when this matches the intended behavior, as providing a custom implementation impacts * performance * @param contentType a factory of the content types for the item. The item compositions of the same * type could be reused more efficiently. Note that null is a valid type and items of such type * will be considered compatible. * @param itemContent the content displayed by a single item */ inline fun LazyGridScope.itemsIndexed( items: Array, noinline key: ((index: Int, item: T) -> Any)? = null, noinline span: (LazyGridItemSpanScope.(index: Int, item: T) -> GridItemSpan)? = null, crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null }, crossinline itemContent: @Composable LazyGridItemScope.(index: Int, item: T) -> Unit, ) = items( count = items.size, key = if (key != null) { index: Int -> key(index, items[index]) } else null, span = if (span != null) { { span(it, items[it]) } } else null, contentType = { index -> contentType(index, items[index]) }, ) { itemContent(it, items[it]) } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/grid/LazyGridState.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.foundation.lazy.grid import androidx.annotation.IntRange as AndroidXIntRange import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.ScrollIndicatorState import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.gestures.stopScroll import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.internal.checkPrecondition import androidx.compose.foundation.lazy.grid.LazyGridState.Companion.Saver import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier import androidx.compose.foundation.lazy.layout.CacheWindowLogic import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow import androidx.compose.foundation.lazy.layout.LazyLayoutItemAnimator import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState import androidx.compose.foundation.lazy.layout.LazyLayoutScrollDeltaBetweenPasses import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator import androidx.compose.foundation.lazy.layout.animateScrollToItem import androidx.compose.foundation.lazy.singleAxisViewportSize import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.annotation.FrequentlyChangingValue import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.layout.AlignmentLine import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier import androidx.compose.ui.unit.Density import androidx.compose.ui.util.fastForEach import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.abs import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** * Creates a [LazyGridState] that is remembered across compositions. * * Changes to the provided initial values will **not** result in the state being recreated or * changed in any way if it has already been created. * * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex] * @param initialFirstVisibleItemScrollOffset the initial value for * [LazyGridState.firstVisibleItemScrollOffset] */ @Composable fun rememberLazyGridState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0, ): LazyGridState { return rememberSaveable(saver = LazyGridState.Saver) { LazyGridState(initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset) } } /** * Creates a [LazyGridState] that is remembered across compositions. * * Changes to the provided initial values will **not** result in the state being recreated or * changed in any way if it has already been created. * * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex] * @param initialFirstVisibleItemScrollOffset the initial value for * [LazyGridState.firstVisibleItemScrollOffset] * @param prefetchStrategy the [LazyGridPrefetchStrategy] to use for prefetching content in this * grid */ @ExperimentalFoundationApi @Composable fun rememberLazyGridState( initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0, prefetchStrategy: LazyGridPrefetchStrategy = remember { LazyGridPrefetchStrategy() }, ): LazyGridState { return rememberSaveable(prefetchStrategy, saver = LazyGridState.saver(prefetchStrategy)) { LazyGridState( initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, prefetchStrategy, ) } } /** * Creates a [LazyGridState] that is remembered across compositions. * * Changes to the provided initial values will **not** result in the state being recreated or * changed in any way if it has already been created. * * @param cacheWindow specifies the size of the ahead and behind window to be used as per * [LazyLayoutCacheWindow]. * @param initialFirstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex] * @param initialFirstVisibleItemScrollOffset the initial value for * [LazyGridState.firstVisibleItemScrollOffset] */ @ExperimentalFoundationApi @Composable fun rememberLazyGridState( cacheWindow: LazyLayoutCacheWindow, initialFirstVisibleItemIndex: Int = 0, initialFirstVisibleItemScrollOffset: Int = 0, ): LazyGridState { return rememberSaveable(cacheWindow, saver = LazyGridState.saver(cacheWindow)) { LazyGridState( cacheWindow, initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset, ) } } /** * A state object that can be hoisted to control and observe scrolling. * * In most cases, this will be created via [rememberLazyGridState]. * * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex] * @param firstVisibleItemScrollOffset the initial value for * [LazyGridState.firstVisibleItemScrollOffset] * @param prefetchStrategy the [LazyGridPrefetchStrategy] to use for prefetching content in this * grid */ @OptIn(ExperimentalFoundationApi::class) @Stable class LazyGridState @ExperimentalFoundationApi constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0, internal val prefetchStrategy: LazyGridPrefetchStrategy = LazyGridPrefetchStrategy(), ) : ScrollableState { /** * @param cacheWindow specifies the size of the ahead and behind window to be used as per * [LazyLayoutCacheWindow]. * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex] * @param firstVisibleItemScrollOffset the initial value for * [LazyGridState.firstVisibleItemScrollOffset] */ @ExperimentalFoundationApi constructor( cacheWindow: LazyLayoutCacheWindow, firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0, ) : this( firstVisibleItemIndex, firstVisibleItemScrollOffset, LazyGridCacheWindowPrefetchStrategy(cacheWindow), ) /** * @param firstVisibleItemIndex the initial value for [LazyGridState.firstVisibleItemIndex] * @param firstVisibleItemScrollOffset the initial value for * [LazyGridState.firstVisibleItemScrollOffset] */ constructor( firstVisibleItemIndex: Int = 0, firstVisibleItemScrollOffset: Int = 0, ) : this(firstVisibleItemIndex, firstVisibleItemScrollOffset, LazyGridPrefetchStrategy()) internal var hasLookaheadOccurred: Boolean = false private set internal var approachLayoutInfo: LazyGridMeasureResult? = null private set // always execute requests in high priority private var executeRequestsInHighPriorityMode = false /** The holder class for the current scroll position. */ private val scrollPosition = LazyGridScrollPosition(firstVisibleItemIndex, firstVisibleItemScrollOffset) /** * The index of the first item that is visible within the scrollable viewport area, this means, * not including items in the content padding region. For the first visible item that includes * items in the content padding please use [LazyGridLayoutInfo.visibleItemsInfo]. * * Note that this property is observable and if you use it in the composable function it will be * recomposed on every change causing potential performance issues. * * If you want to run some side effects like sending an analytics event or updating a state * based on this value consider using "snapshotFlow": * * @sample androidx.compose.foundation.samples.UsingGridScrollPositionForSideEffectSample * * If you need to use it in the composition then consider wrapping the calculation into a * derived state in order to only have recompositions when the derived value changes: * * @sample androidx.compose.foundation.samples.UsingGridScrollPositionInCompositionSample */ val firstVisibleItemIndex: Int @FrequentlyChangingValue get() = scrollPosition.index /** * The scroll offset of the first visible item. Scrolling forward is positive - i.e., the amount * that the item is offset backwards */ val firstVisibleItemScrollOffset: Int @FrequentlyChangingValue get() = scrollPosition.scrollOffset /** Backing state for [layoutInfo] */ private val layoutInfoState = mutableStateOf(EmptyLazyGridLayoutInfo, neverEqualPolicy()) /** * The object of [LazyGridLayoutInfo] calculated during the last layout pass. For example, you * can use it to calculate what items are currently visible. * * Note that this property is observable and is updated after every scroll or remeasure. If you * use it in the composable function it will be recomposed on every change causing potential * performance issues including infinity recomposition loop. Therefore, avoid using it in the * composition. * * If you want to run some side effects like sending an analytics event or updating a state * based on this value consider using "snapshotFlow": * * @sample androidx.compose.foundation.samples.UsingGridLayoutInfoForSideEffectSample */ val layoutInfo: LazyGridLayoutInfo @FrequentlyChangingValue get() = layoutInfoState.value /** * [InteractionSource] that will be used to dispatch drag events when this grid is being * dragged. If you want to know whether the fling (or animated scroll) is in progress, use * [isScrollInProgress]. */ val interactionSource: InteractionSource get() = internalInteractionSource internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource() /** * The amount of scroll to be consumed in the next layout pass. Scrolling forward is negative * - that is, it is the amount that the items are offset in y */ internal var scrollToBeConsumed = 0f private set internal val slotsPerLine: Int get() = layoutInfoState.value.slotsPerLine internal val density: Density get() = layoutInfoState.value.density /** * The ScrollableController instance. We keep it as we need to call stopAnimation on it once we * reached the end of the grid. */ private val scrollableState = ScrollableState { -onScroll(-it) } /** Only used for testing to confirm that we're not making too many measure passes */ /*@VisibleForTesting*/ internal var numMeasurePasses: Int = 0 private set /** Only used for testing to disable prefetching when needed to test the main logic. */ /*@VisibleForTesting*/ internal var prefetchingEnabled: Boolean = true /** * The [Remeasurement] object associated with our layout. It allows us to remeasure * synchronously during scroll. */ internal var remeasurement: Remeasurement? = null private set /** The modifier which provides [remeasurement]. */ internal val remeasurementModifier = object : RemeasurementModifier { override fun onRemeasurementAvailable(remeasurement: Remeasurement) { this@LazyGridState.remeasurement = remeasurement } } /** * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is * ready. */ internal val awaitLayoutModifier = AwaitFirstLayoutModifier() internal val itemAnimator = LazyLayoutItemAnimator() internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() @Suppress("DEPRECATION") // b/420551535 internal val prefetchState = LazyLayoutPrefetchState(prefetchStrategy.prefetchScheduler) { with(prefetchStrategy) { onNestedPrefetch(Snapshot.withoutReadObservation { firstVisibleItemIndex }) } } private val prefetchScope: LazyGridPrefetchScope = object : LazyGridPrefetchScope { override fun scheduleLinePrefetch( lineIndex: Int ): List { return scheduleLinePrefetch(lineIndex, null) } @Suppress("PrimitiveInCollection") override fun scheduleLinePrefetch( lineIndex: Int, onPrefetchFinished: (LazyGridPrefetchResultScope.() -> Unit)?, ): List { // Without read observation since this can be triggered from scroll - this will then // cause us to recompose when the measure result changes. We don't care since the // prefetch is best effort. val prefetchHandles = mutableListOf() val itemSizes: MutableList? = if (onPrefetchFinished == null) null else mutableListOf() Snapshot.withoutReadObservation { val layoutInfo = if (hasLookaheadOccurred) { approachLayoutInfo } else { layoutInfoState.value } layoutInfo?.let { measureResult -> var completedCount = 1 val itemsInLineInfo = measureResult.prefetchInfoRetriever(lineIndex) itemsInLineInfo.fastForEach { lineInfo -> prefetchHandles.add( prefetchState.schedulePrecompositionAndPremeasure( lineInfo.first, lineInfo.second, executeRequestsInHighPriorityMode, ) { var itemMainAxisItemSize = 0 repeat(placeablesCount) { itemMainAxisItemSize += if (measureResult.orientation == Orientation.Vertical) { getSize(it).height } else { getSize(it).width } } itemSizes?.add(itemMainAxisItemSize) // all items in this line were prefetched, report the size if (completedCount == itemsInLineInfo.size) { if (onPrefetchFinished != null && itemSizes != null) { onPrefetchFinished.invoke( LazyGridPrefetchResultScopeImpl( lineIndex, itemSizes, ) ) } } else { completedCount++ } } ) } } } return prefetchHandles } } private val _scrollIndicatorState = object : ScrollIndicatorState { override val scrollOffset: Int get() = calculateScrollOffset() override val contentSize: Int get() = layoutInfo.calculateContentSize() override val viewportSize: Int get() = layoutInfo.singleAxisViewportSize } private fun calculateScrollOffset(): Int { val info = layoutInfo return (info.visibleLinesAverageMainAxisSize() * info.firstVisibleItemLineIndex) + firstVisibleItemScrollOffset } /** Stores currently pinned items which are always composed. */ internal val pinnedItems = LazyLayoutPinnedItemList() internal val nearestRange: IntRange by scrollPosition.nearestRangeState internal val placementScopeInvalidator = ObservableScopeInvalidator() /** * Instantly brings the item at [index] to the top of the viewport, offset by [scrollOffset] * pixels. * * @param index the index to which to scroll. Must be non-negative. * @param scrollOffset the offset that the item should end up after the scroll. Note that * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will * scroll the item further upward (taking it partly offscreen). */ suspend fun scrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { scroll { snapToItemIndexInternal(index, scrollOffset, forceRemeasure = true) } } internal val measurementScopeInvalidator = ObservableScopeInvalidator() /** * Requests the item at [index] to be at the start of the viewport during the next remeasure, * offset by [scrollOffset], and schedules a remeasure. * * The scroll position will be updated to the requested position rather than maintain the index * based on the first visible item key (when a data set change will also be applied during the * next remeasure), but *only* for the next remeasure. * * Any scroll in progress will be cancelled. * * @param index the index to which to scroll. Must be non-negative. * @param scrollOffset the offset that the item should end up after the scroll. Note that * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will * scroll the item further upward (taking it partly offscreen). */ fun requestScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { // Cancel any scroll in progress. if (isScrollInProgress) { layoutInfoState.value.coroutineScope.launch { stopScroll() } } snapToItemIndexInternal(index, scrollOffset, forceRemeasure = false) } internal fun snapToItemIndexInternal(index: Int, scrollOffset: Int, forceRemeasure: Boolean) { val positionChanged = scrollPosition.index != index || scrollPosition.scrollOffset != scrollOffset // sometimes this method is called not to scroll, but to stay on the same index when // the data changes, as by default we maintain the scroll position by key, not index. // when this happens we don't need to reset the animations as from the user perspective // we didn't scroll anywhere and if there is an offset change for an item, this change // should be animated. // however, when the request is to really scroll to a different position, we have to // reset previously known item positions as we don't want offset changes to be animated. // this offset should be considered as a scroll, not the placement change. if (positionChanged) { itemAnimator.reset() // we changed positions, cancel existing requests and wait for the next scroll to // refill the window (prefetchStrategy as? CacheWindowLogic)?.resetStrategy() } scrollPosition.requestPositionAndForgetLastKnownKey(index, scrollOffset) if (forceRemeasure) { remeasurement?.forceRemeasure() } else { measurementScopeInvalidator.invalidateScope() } } /** * Call this function to take control of scrolling and gain the ability to send scroll events * via [ScrollScope.scrollBy]. All actions that change the logical scroll position must be * performed within a [scroll] block (even if they don't call any other methods on this object) * in order to guarantee that mutual exclusion is enforced. * * If [scroll] is called from elsewhere, this will be canceled. */ override suspend fun scroll( scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit, ) { if (layoutInfoState.value === EmptyLazyGridLayoutInfo) { awaitLayoutModifier.waitForFirstLayout() } scrollableState.scroll(scrollPriority, block) } override fun dispatchRawDelta(delta: Float): Float = scrollableState.dispatchRawDelta(delta) override val isScrollInProgress: Boolean get() = scrollableState.isScrollInProgress override var canScrollForward: Boolean by mutableStateOf(false) private set override var canScrollBackward: Boolean by mutableStateOf(false) private set @get:Suppress("GetterSetterNames") override val lastScrolledForward: Boolean get() = scrollableState.lastScrolledForward @get:Suppress("GetterSetterNames") override val lastScrolledBackward: Boolean get() = scrollableState.lastScrolledBackward override val scrollIndicatorState: ScrollIndicatorState? get() = _scrollIndicatorState // TODO: Coroutine scrolling APIs will allow this to be private again once we have more // fine-grained control over scrolling /*@VisibleForTesting*/ internal fun onScroll(distance: Float): Float { if (distance < 0 && !canScrollForward || distance > 0 && !canScrollBackward) { return 0f } checkPrecondition(abs(scrollToBeConsumed) <= 0.5f) { "entered drag with non-zero pending scroll" } scrollToBeConsumed += distance // scrollToBeConsumed will be consumed synchronously during the forceRemeasure invocation // inside measuring we do scrollToBeConsumed.roundToInt() so there will be no scroll if // we have less than 0.5 pixels if (abs(scrollToBeConsumed) > 0.5f) { val preScrollToBeConsumed = scrollToBeConsumed val intDelta = scrollToBeConsumed.roundToInt() var scrolledLayoutInfo = layoutInfoState.value.copyWithScrollDeltaWithoutRemeasure( delta = intDelta, updateAnimations = !hasLookaheadOccurred, ) if (scrolledLayoutInfo != null && this.approachLayoutInfo != null) { // if we were able to scroll the lookahead layout info without remeasure, lets // try to do the same for post lookahead layout info (sometimes they diverge). val scrolledApproachLayoutInfo = approachLayoutInfo?.copyWithScrollDeltaWithoutRemeasure( delta = intDelta, updateAnimations = true, ) if (scrolledApproachLayoutInfo != null) { // we can apply scroll delta for both phases without remeasure approachLayoutInfo = scrolledApproachLayoutInfo } else { // we can't apply scroll delta for post lookahead, so we have to remeasure scrolledLayoutInfo = null } } if (scrolledLayoutInfo != null) { applyMeasureResult( result = scrolledLayoutInfo, isLookingAhead = hasLookaheadOccurred, visibleItemsStayedTheSame = true, ) // we don't need to remeasure, so we only trigger re-placement: placementScopeInvalidator.invalidateScope() notifyPrefetchOnScroll( preScrollToBeConsumed - scrollToBeConsumed, scrolledLayoutInfo, ) } else { remeasurement?.forceRemeasure() notifyPrefetchOnScroll(preScrollToBeConsumed - scrollToBeConsumed, this.layoutInfo) } } // here scrollToBeConsumed is already consumed during the forceRemeasure invocation if (abs(scrollToBeConsumed) <= 0.5f) { // We consumed all of it - we'll hold onto the fractional scroll for later, so report // that we consumed the whole thing return distance } else { val scrollConsumed = distance - scrollToBeConsumed // We did not consume all of it - return the rest to be consumed elsewhere (e.g., // nested scrolling) scrollToBeConsumed = 0f // We're not consuming the rest, give it back return scrollConsumed } } private fun notifyPrefetchOnScroll(delta: Float, layoutInfo: LazyGridLayoutInfo) { if (prefetchingEnabled) { with(prefetchStrategy) { prefetchScope.onScroll(delta, layoutInfo) } } } private val numOfItemsToTeleport: Int get() = 100 * slotsPerLine /** * Animate (smooth scroll) to the given item. * * @param index the index to which to scroll. Must be non-negative. * @param scrollOffset the offset that the item should end up after the scroll. Note that * positive offset refers to forward scroll, so in a top-to-bottom list, positive offset will * scroll the item further upward (taking it partly offscreen). */ suspend fun animateScrollToItem(@AndroidXIntRange(from = 0) index: Int, scrollOffset: Int = 0) { scroll { LazyLayoutScrollScope(this@LazyGridState, this) .animateScrollToItem(index, scrollOffset, numOfItemsToTeleport, density) } } /** Updates the state with the new calculated scroll position and consumed scroll. */ internal fun applyMeasureResult( result: LazyGridMeasureResult, isLookingAhead: Boolean, visibleItemsStayedTheSame: Boolean = false, ) { // update the prefetch state with the number of nested prefetch items this layout // should use. prefetchState.idealNestedPrefetchCount = result.visibleItemsInfo.size if (!isLookingAhead && hasLookaheadOccurred) { // If there was already a lookahead pass, record this result as Approach result approachLayoutInfo = result } else { if (isLookingAhead) { hasLookaheadOccurred = true } scrollToBeConsumed -= result.consumedScroll layoutInfoState.value = result canScrollBackward = result.canScrollBackward canScrollForward = result.canScrollForward if (visibleItemsStayedTheSame) { scrollPosition.updateScrollOffset(result.firstVisibleLineScrollOffset) } else { scrollPosition.updateFromMeasureResult(result) if (prefetchingEnabled) { with(prefetchStrategy) { prefetchScope.onVisibleItemsUpdated(result) } } } if (isLookingAhead) { _lazyLayoutScrollDeltaBetweenPasses.updateScrollDeltaForApproach( result.scrollBackAmount, result.density, result.coroutineScope, ) } numMeasurePasses++ } } private val _lazyLayoutScrollDeltaBetweenPasses = LazyLayoutScrollDeltaBetweenPasses() internal val scrollDeltaBetweenPasses get() = _lazyLayoutScrollDeltaBetweenPasses.scrollDeltaBetweenPasses /** * When the user provided custom keys for the items we can try to detect when there were items * added or removed before our current first visible item and keep this item as the first * visible one even given that its index has been changed. */ internal fun updateScrollPositionIfTheFirstItemWasMoved( itemProvider: LazyGridItemProvider, firstItemIndex: Int, ): Int = scrollPosition.updateScrollPositionIfTheFirstItemWasMoved(itemProvider, firstItemIndex) companion object { /** The default [Saver] implementation for [LazyGridState]. */ val Saver: Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { LazyGridState( firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1], ) }, ) /** * A [Saver] implementation for [LazyGridState] that handles setting a custom * [LazyGridPrefetchStrategy]. */ @ExperimentalFoundationApi internal fun saver(prefetchStrategy: LazyGridPrefetchStrategy): Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { LazyGridState( firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1], prefetchStrategy, ) }, ) /** * A [Saver] implementation for [LazyGridState] that handles setting a custom * [LazyLayoutCacheWindow]. */ @ExperimentalFoundationApi internal fun saver(cacheWindow: LazyLayoutCacheWindow): Saver = listSaver( save = { listOf(it.firstVisibleItemIndex, it.firstVisibleItemScrollOffset) }, restore = { LazyGridState( cacheWindow = cacheWindow, firstVisibleItemIndex = it[0], firstVisibleItemScrollOffset = it[1], ) }, ) } } private val EmptyLazyGridLayoutInfo = LazyGridMeasureResult( firstVisibleLine = null, firstVisibleLineScrollOffset = 0, canScrollForward = false, consumedScroll = 0f, measureResult = object : MeasureResult { override val width: Int = 0 override val height: Int = 0 @Suppress("PrimitiveInCollection") override val alignmentLines: Map = emptyMap() override fun placeChildren() {} }, scrollBackAmount = 0f, visibleItemsInfo = emptyList(), viewportStartOffset = 0, viewportEndOffset = 0, totalItemsCount = 0, reverseLayout = false, orientation = Orientation.Vertical, afterContentPadding = 0, mainAxisItemSpacing = 0, remeasureNeeded = false, density = Density(1f), slotsPerLine = 0, coroutineScope = CoroutineScope(EmptyCoroutineContext), prefetchInfoRetriever = { emptyList() }, lineIndexProvider = { -1 }, ) ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation.text import androidx.compose.foundation.ComposeFoundationFlags import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ScrollState import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.scrollable import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.contextmenu.modifier.ToolbarRequesterImpl import androidx.compose.foundation.text.handwriting.stylusHandwriting import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.KeyboardActionHandler import androidx.compose.foundation.text.input.OutputTransformation import androidx.compose.foundation.text.input.TextFieldDecorator import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.internal.CodepointTransformation import androidx.compose.foundation.text.input.internal.SingleLineCodepointTransformation import androidx.compose.foundation.text.input.internal.TextFieldCoreModifier import androidx.compose.foundation.text.input.internal.TextFieldDecoratorModifier import androidx.compose.foundation.text.input.internal.TextFieldTextLayoutModifier import androidx.compose.foundation.text.input.internal.TextLayoutState import androidx.compose.foundation.text.input.internal.TransformedTextFieldState import androidx.compose.foundation.text.input.internal.collectIsDragAndDropHoveredAsState import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState import androidx.compose.foundation.text.input.internal.selection.TextFieldSelectionState.InputType import androidx.compose.foundation.text.input.internal.selection.TextToolbarHandler import androidx.compose.foundation.text.input.internal.selection.TextToolbarState import androidx.compose.foundation.text.input.internal.selection.addBasicTextFieldTextContextMenuComponents import androidx.compose.foundation.text.input.internal.selection.menuItem import androidx.compose.foundation.text.selection.SelectedTextType import androidx.compose.foundation.text.selection.SelectionHandle import androidx.compose.foundation.text.selection.rememberPlatformSelectionBehaviors import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalTextToolbar import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.TextToolbarStatus import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.intl.LocaleList import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.constrain import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch private object BasicTextFieldDefaults { val CursorBrush = SolidColor(Color.Black) } /** * Basic text composable that provides an interactive box that accepts text input through software * or hardware keyboard, but provides no decorations like hint or placeholder. * * All the editing state of this composable is hoisted through [state]. Whenever the contents of * this composable change via user input or semantics, [TextFieldState.text] gets updated. * Similarly, all the programmatic updates made to [state] also reflect on this composable. * * If you want to add decorations to your text field, such as icon or similar, and increase the hit * target area, use the decorator. * * In order to filter (e.g. only allow digits, limit the number of characters), or change (e.g. * convert every character to uppercase) the input received from the user, use an * [InputTransformation]. * * Limiting the height of the [BasicTextField] in terms of line count and choosing a scroll * direction can be achieved by using [TextFieldLineLimits]. * * Scroll state of the composable is also hoisted to enable observation and manipulation of the * scroll behavior by the developer, e.g. bringing a searched keyword into view by scrolling to its * position without focusing, or changing selection. * * It's also possible to internally wrap around an existing TextFieldState and expose a more * lightweight state hoisting mechanism through a value that dictates the content of the TextField * and an onValueChange callback that communicates the changes to this value. * * @param state [TextFieldState] object that holds the internal editing state of [BasicTextField]. * @param modifier optional [Modifier] for this text field. * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text field * will be neither editable nor focusable, the input of the text field will not be selectable. * @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text field * can not be modified, however, a user can focus it and copy text from it. Read-only text fields * are usually used to display pre-filled forms that user can not edit. * @param inputTransformation Optional [InputTransformation] that will be used to transform changes * to the [TextFieldState] made by the user. The transformation will be applied to changes made by * hardware and software keyboard events, pasting or dropping text, accessibility services, and * tests. The transformation will _not_ be applied when changing the [state] programmatically, or * when the transformation is changed. If the transformation is changed on an existing text field, * it will be applied to the next user edit. the transformation will not immediately affect the * current [state]. * @param textStyle Typographic and graphic style configuration for text content that's displayed in * the editor. * @param keyboardOptions Software keyboard options that contain configurations such as * [KeyboardType] and [ImeAction]. * @param onKeyboardAction Called when the user presses the action button in the input method editor * (IME), or by pressing the enter key on a hardware keyboard if the [lineLimits] is configured as * [TextFieldLineLimits.SingleLine]. By default this parameter is null, and would execute the * default behavior for a received IME Action e.g., [ImeAction.Done] would close the keyboard, * [ImeAction.Next] would switch the focus to the next focusable item on the screen. * @param lineLimits Whether the text field should be [SingleLine], scroll horizontally, and ignore * newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline * characters ('\n') within the text will be replaced with regular whitespace (' '), ensuring that * the contents of the text field are presented in a single line. * @param onTextLayout Callback that is executed when the text layout becomes queryable. The * callback receives a function that returns a [TextLayoutResult] if the layout can be calculated, * or null if it cannot. The function reads the layout result from a snapshot state object, and * will invalidate its caller when the layout result changes. A [TextLayoutResult] object contains * paragraph information, size of the text, baselines and other details. The callback can be used * to add additional decoration or functionality to the text. For example, to draw a cursor or * selection around the text. [Density] scope is the one that was used while creating the given * text layout. * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s * for this TextField. You can create and pass in your own remembered [MutableInteractionSource] * if you want to observe [Interaction]s and customize the appearance / behavior of this TextField * for different [Interaction]s. * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] * provided, then no cursor will be drawn. * @param outputTransformation An [OutputTransformation] that transforms how the contents of the * text field are presented. * @param decorator Allows to add decorations around text field, such as icon, placeholder, helper * messages or similar, and automatically increase the hit target area of the text field. * @param scrollState Scroll state that manages either horizontal or vertical scroll of TextField. * If [lineLimits] is [SingleLine], this text field is treated as single line with horizontal * scroll behavior. In other cases the text field becomes vertically scrollable. * @sample androidx.compose.foundation.samples.BasicTextFieldDecoratorSample * @sample androidx.compose.foundation.samples.BasicTextFieldCustomInputTransformationSample * @sample androidx.compose.foundation.samples.BasicTextFieldWithValueOnValueChangeSample */ // This takes a composable lambda, but it is not primarily a container. @Suppress("ComposableLambdaParameterPosition") @Composable fun BasicTextField( state: TextFieldState, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, inputTransformation: InputTransformation? = null, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, onKeyboardAction: KeyboardActionHandler? = null, lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, cursorBrush: Brush = BasicTextFieldDefaults.CursorBrush, outputTransformation: OutputTransformation? = null, decorator: TextFieldDecorator? = null, scrollState: ScrollState = rememberScrollState(), // Last parameter must not be a function unless it's intended to be commonly used as a trailing // lambda. ) { BasicTextField( state = state, modifier = modifier, enabled = enabled, readOnly = readOnly, inputTransformation = inputTransformation, textStyle = textStyle, keyboardOptions = keyboardOptions, onKeyboardAction = onKeyboardAction, lineLimits = lineLimits, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, codepointTransformation = null, outputTransformation = outputTransformation, decorator = decorator, scrollState = scrollState, ) } /** * Internal core text field that accepts a [CodepointTransformation]. * * @param codepointTransformation Visual transformation interface that provides a 1-to-1 mapping of * codepoints. */ // This takes a composable lambda, but it is not primarily a container. @Suppress("ComposableLambdaParameterPosition") @Composable internal fun BasicTextField( state: TextFieldState, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, inputTransformation: InputTransformation? = null, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, onKeyboardAction: KeyboardActionHandler? = null, lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null, interactionSource: MutableInteractionSource? = null, cursorBrush: Brush = BasicTextFieldDefaults.CursorBrush, codepointTransformation: CodepointTransformation? = null, outputTransformation: OutputTransformation? = null, decorator: TextFieldDecorator? = null, scrollState: ScrollState = rememberScrollState(), isPassword: Boolean = false, // Last parameter must not be a function unless it's intended to be commonly used as a trailing // lambda. ) { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current val singleLine = lineLimits == SingleLine // We're using this to communicate focus state to cursor for now. @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val orientation = if (singleLine) Orientation.Horizontal else Orientation.Vertical val isFocused = interactionSource.collectIsFocusedAsState().value val isDragHovered = interactionSource.collectIsDragAndDropHoveredAsState().value // Avoid reading LocalWindowInfo.current.isWindowFocused when the text field is not focused; // otherwise all text fields in a window will be recomposed when it becomes focused. val isWindowAndTextFieldFocused = isFocused && LocalWindowInfo.current.isWindowFocused val stylusHandwritingTrigger = remember { MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_LATEST) } val transformedState = remember(state, codepointTransformation, outputTransformation) { // First prefer provided codepointTransformation if not null, e.g. BasicSecureTextField // would send PasswordTransformation. Second, apply a SingleLineCodepointTransformation // if // text field is configured to be single line. Else, don't apply any visual // transformation. val appliedCodepointTransformation = codepointTransformation ?: SingleLineCodepointTransformation.takeIf { singleLine } TransformedTextFieldState( textFieldState = state, inputTransformation = inputTransformation, codepointTransformation = appliedCodepointTransformation, outputTransformation = outputTransformation, ) } // Invalidate textLayoutState if TextFieldState itself has changed, since TextLayoutState // would be carrying an invalid TextFieldState in its nonMeasureInputs. val textLayoutState = remember(transformedState) { TextLayoutState() } // InputTransformation.keyboardOptions might be backed by Snapshot state. // Read in a restartable composable scope to make sure the resolved value is always up-to-date. val resolvedKeyboardOptions = keyboardOptions.fillUnspecifiedValuesWith(inputTransformation?.keyboardOptions) val coroutineScope = rememberCoroutineScope() @OptIn(ExperimentalFoundationApi::class) val platformSelectionBehaviors = if (ComposeFoundationFlags.isSmartSelectionEnabled) { val resolvedLocaleList = textStyle.localeList ?: LocaleList.current rememberPlatformSelectionBehaviors(SelectedTextType.EditableText, resolvedLocaleList) } else { null } val toolbarRequester = remember { ToolbarRequesterImpl() } val currentClipboard = LocalClipboard.current val textFieldSelectionState = remember(transformedState) { TextFieldSelectionState( textFieldState = transformedState, textLayoutState = textLayoutState, density = density, enabled = enabled, readOnly = readOnly, isFocused = isWindowAndTextFieldFocused, isPassword = isPassword, toolbarRequester = toolbarRequester, coroutineScope = coroutineScope, platformSelectionBehaviors = platformSelectionBehaviors, clipboard = currentClipboard, ) } val currentHapticFeedback = LocalHapticFeedback.current val currentTextToolbar = LocalTextToolbar.current val textToolbarHandler = remember(coroutineScope, currentTextToolbar) { object : TextToolbarHandler { override suspend fun showTextToolbar( selectionState: TextFieldSelectionState, rect: Rect, ) = with(selectionState) { selectionState.updateClipboardEntry() currentTextToolbar.showMenu( rect = rect, onCopyRequested = menuItem(canShowCopyMenuItem(), TextToolbarState.None) { coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { copy() } }, onPasteRequested = menuItem(canShowPasteMenuItem(), TextToolbarState.None) { coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { paste() } }, onCutRequested = menuItem(canShowCutMenuItem(), TextToolbarState.None) { coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { cut() } }, onSelectAllRequested = menuItem(canShowSelectAllMenuItem(), TextToolbarState.Selection) { selectAll() }, onAutofillRequested = menuItem(canShowAutofillMenuItem(), TextToolbarState.None) { autofill() }, ) } override fun hideTextToolbar() { if (currentTextToolbar.status == TextToolbarStatus.Shown) { currentTextToolbar.hide() } } } } rememberClipboardEventsHandler( isEnabled = isFocused, onPaste = { textFieldSelectionState.onPasteEvent(it) }, onCopy = { textFieldSelectionState.copyWithResult() }, onCut = { textFieldSelectionState.cutWithResult() }, ) SideEffect { // These properties are not backed by snapshot state, so they can't be updated directly in // composition. transformedState.update(inputTransformation) textFieldSelectionState.update( hapticFeedBack = currentHapticFeedback, clipboard = currentClipboard, density = density, enabled = enabled, readOnly = readOnly, isPassword = isPassword, showTextToolbar = textToolbarHandler, ) } DisposableEffect(textFieldSelectionState) { onDispose { textFieldSelectionState.dispose() } } val overscrollEffect = rememberTextFieldOverscrollEffect() val handwritingEnabled = !isPassword && keyboardOptions.keyboardType != KeyboardType.Password && keyboardOptions.keyboardType != KeyboardType.NumberPassword val decorationModifiers = modifier .stylusHandwriting(enabled, handwritingEnabled) { // If this is a password field, we can't trigger handwriting. // The expected behavior is 1) request focus 2) show software keyboard. // Note: TextField will show software keyboard automatically when it // gain focus. 3) show a toast message telling that handwriting is not // supported for password fields. TODO(b/335294152) if (handwritingEnabled) { // Send the handwriting start signal to platform. // The editor should send the signal when it is focused or is about // to gain focus, Here are more details: // 1) if the editor already has an active input session, the // platform handwriting service should already listen to this flow // and it'll start handwriting right away. // // 2) if the editor is not focused, but it'll be focused and // create a new input session, one handwriting signal will be // replayed when the platform collect this flow. And the platform // should trigger handwriting accordingly. stylusHandwritingTrigger.tryEmit(Unit) } } .then( // semantics + some focus + input session + touch to focus TextFieldDecoratorModifier( textFieldState = transformedState, textLayoutState = textLayoutState, textFieldSelectionState = textFieldSelectionState, filter = inputTransformation, enabled = enabled, readOnly = readOnly, keyboardOptions = resolvedKeyboardOptions, keyboardActionHandler = onKeyboardAction, singleLine = singleLine, interactionSource = interactionSource, isPassword = isPassword, stylusHandwritingTrigger = stylusHandwritingTrigger, ) ) .scrollable( state = scrollState, orientation = orientation, // Disable scrolling when textField is disabled or another dragging gesture is // taking place enabled = enabled && textFieldSelectionState.directDragGestureInitiator == InputType.None, reverseDirection = ScrollableDefaults.reverseDirection( layoutDirection = layoutDirection, orientation = orientation, reverseScrolling = false, ), interactionSource = interactionSource, overscrollEffect = overscrollEffect, ) .pointerHoverIcon(PointerIcon.Text) .addContextMenuComponents(textFieldSelectionState, coroutineScope) Box(decorationModifiers, propagateMinConstraints = true) { ContextMenuArea(textFieldSelectionState, enabled) { val nonNullDecorator = decorator ?: DefaultTextFieldDecorator nonNullDecorator.Decoration { val minLines: Int val maxLines: Int if (lineLimits is MultiLine) { minLines = lineLimits.minHeightInLines maxLines = lineLimits.maxHeightInLines } else { minLines = 1 maxLines = 1 } Box( propagateMinConstraints = true, modifier = Modifier.minHeightForSingleLineField(textLayoutState) .heightInLines( textStyle = textStyle, minLines = minLines, maxLines = maxLines, ) .textFieldMinSize(textStyle) .clipToBounds() .then( TextFieldCoreModifier( isFocused = isWindowAndTextFieldFocused, isDragHovered = isDragHovered, textLayoutState = textLayoutState, textFieldState = transformedState, textFieldSelectionState = textFieldSelectionState, cursorBrush = cursorBrush, writeable = enabled && !readOnly, scrollState = scrollState, orientation = orientation, toolbarRequester = toolbarRequester, platformSelectionBehaviors = platformSelectionBehaviors, ) ), ) { Box( modifier = TextFieldTextLayoutModifier( textLayoutState = textLayoutState, textFieldState = transformedState, textStyle = textStyle, singleLine = singleLine, onTextLayout = onTextLayout, keyboardOptions = resolvedKeyboardOptions, ) ) if ( enabled && isWindowAndTextFieldFocused && textFieldSelectionState.isInTouchMode ) { TextFieldSelectionHandles(selectionState = textFieldSelectionState) if (!readOnly) { TextFieldCursorHandle(selectionState = textFieldSelectionState) } } } } } } } @OptIn(ExperimentalFoundationApi::class) private fun Modifier.minHeightForSingleLineField(textLayoutState: TextLayoutState) = if (ComposeFoundationFlags.isBasicTextFieldMinSizeOptimizationEnabled) { layout { measurable, constraints -> val wrappedConstraints = constraints.constrain( Constraints( minWidth = 0, maxWidth = Constraints.Infinity, minHeight = textLayoutState.minHeightForSingleLineField.roundToPx(), maxHeight = Constraints.Infinity, ) ) val placeable = measurable.measure(wrappedConstraints) layout(placeable.width, placeable.height) { placeable.placeRelative(0, 0) } } } else { heightIn(min = textLayoutState.minHeightForSingleLineField) } @OptIn(ExperimentalFoundationApi::class) private fun Modifier.addContextMenuComponents( textFieldSelectionState: TextFieldSelectionState, coroutineScope: CoroutineScope, ): Modifier = if (ComposeFoundationFlags.isNewContextMenuEnabled) addBasicTextFieldTextContextMenuComponents(textFieldSelectionState, coroutineScope) else this @Composable internal fun TextFieldCursorHandle(selectionState: TextFieldSelectionState) { // Does not recompose if only position of the handle changes. val cursorHandleVisible by remember(selectionState) { derivedStateOf { selectionState.getCursorHandleState(includePosition = false).visible } } if (cursorHandleVisible) { CursorHandle( offsetProvider = { selectionState.getCursorHandleState(includePosition = true).position }, modifier = Modifier.pointerInput(selectionState) { with(selectionState) { cursorHandleGestures() } }, minTouchTargetSize = MinTouchTargetSizeForHandles, ) } } @Composable internal fun TextFieldSelectionHandles(selectionState: TextFieldSelectionState) { // Does not recompose if only position of the handle changes. val startHandleState by remember(selectionState) { derivedStateOf { selectionState.getSelectionHandleState( isStartHandle = true, includePosition = false, ) } } // Read once here to avoid repeating derived state reads val startHandle = startHandleState if (startHandle.visible) { SelectionHandle( offsetProvider = { selectionState .getSelectionHandleState(isStartHandle = true, includePosition = true) .position }, isStartHandle = true, direction = startHandle.direction, handlesCrossed = startHandle.handlesCrossed, modifier = Modifier.pointerInput(selectionState) { with(selectionState) { selectionHandleGestures(true) } }, lineHeight = startHandle.lineHeight, minTouchTargetSize = MinTouchTargetSizeForHandles, ) } // Does not recompose if only position of the handle changes. val endHandleState by remember(selectionState) { derivedStateOf { selectionState.getSelectionHandleState( isStartHandle = false, includePosition = false, ) } } // Read once here to avoid repeating derived state reads val endHandle = endHandleState if (endHandle.visible) { SelectionHandle( offsetProvider = { selectionState .getSelectionHandleState(isStartHandle = false, includePosition = true) .position }, isStartHandle = false, direction = endHandle.direction, handlesCrossed = endHandle.handlesCrossed, modifier = Modifier.pointerInput(selectionState) { with(selectionState) { selectionHandleGestures(false) } }, lineHeight = endHandle.lineHeight, minTouchTargetSize = MinTouchTargetSizeForHandles, ) } } private val DefaultTextFieldDecorator = TextFieldDecorator { it() } /** * Defines a minimum touch target area size for Selection and Cursor handles. * * Although BasicTextField is not part of Material spec, this accessibility feature is important * enough to be included at foundation layer, and also TextField cannot change selection handles * provided by BasicTextField to somehow achieve this accessibility requirement. * * This value is adopted from Android platform's TextView implementation. */ private val MinTouchTargetSizeForHandles = DpSize(40.dp, 40.dp) /** * Basic composable that enables users to edit text via hardware or software keyboard, but provides * no decorations like hint or placeholder. * * Whenever the user edits the text, [onValueChange] is called with the most up to date state * represented by [String] with which developer is expected to update their state. * * Unlike [TextFieldValue] overload, this composable does not let the developer control selection, * cursor and text composition information. Please check [TextFieldValue] and corresponding * [BasicTextField] overload for more information. * * It is crucial that the value provided to the [onValueChange] is fed back into [BasicTextField] in * order to actually display and continue to edit that text in the field. The value you feed back * into the field may be different than the one provided to the [onValueChange] callback, however * the following caveats apply: * - The new value must be provided to [BasicTextField] immediately (i.e. by the next frame), or the * text field may appear to glitch, e.g. the cursor may jump around. For more information about * this requirement, see * [this article](https://developer.android.com/jetpack/compose/text/user-input#state-practices). * - The value fed back into the field may be different from the one passed to [onValueChange], * although this may result in the input connection being restarted, which can make the keyboard * flicker for the user. This is acceptable when you're using the callback to, for example, filter * out certain types of input, but should probably not be done on every update when entering * freeform text. * * This composable provides basic text editing functionality, however does not include any * decorations such as borders, hints/placeholder. A design system based implementation such as * Material Design Filled text field is typically what is needed to cover most of the needs. This * composable is designed to be used when a custom implementation for different design system is * needed. * * Example usage: * * @sample androidx.compose.foundation.samples.BasicTextFieldWithStringSample * * For example, if you need to include a placeholder in your TextField, you can write a composable * using the decoration box like this: * * @sample androidx.compose.foundation.samples.PlaceholderBasicTextFieldSample * * If you want to add decorations to your text field, such as icon or similar, and increase the hit * target area, use the decoration box: * * @sample androidx.compose.foundation.samples.TextFieldWithIconSample * * In order to create formatted text field, for example for entering a phone number or a social * security number, use a [visualTransformation] parameter. Below is the example of the text field * for entering a credit card number: * * @sample androidx.compose.foundation.samples.CreditCardSample * * Note: This overload does not support [KeyboardOptions.showKeyboardOnFocus]. * * @param value the input [String] text to be shown in the text field * @param onValueChange the callback that is triggered when the input service updates the text. An * updated text comes as a parameter of the callback * @param modifier optional [Modifier] for this text field. * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text field * will be neither editable nor focusable, the input of the text field will not be selectable * @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text field * can not be modified, however, a user can focus it and copy text from it. Read-only text fields * are usually used to display pre-filled forms that user can not edit * @param textStyle Style configuration that applies at character level such as color, font etc. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction]. * @param keyboardActions when the input service emits an IME action, the corresponding callback is * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction]. * @param singleLine when set to true, this text field becomes a single horizontally scrolling text * field instead of wrapping onto multiple lines. The keyboard will be informed to not show the * return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are automatically * set to 1. * @param maxLines the maximum height in terms of maximum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param visualTransformation The visual transformation filter for changing the visual * representation of the input. By default no visual transformation is applied. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw a cursor or selection around the text. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] * provided, there will be no cursor drawn * @param decorationBox Composable lambda that allows to add decorations around text field, such as * icon, placeholder, helper messages or similar, and automatically increase the hit target area * of the text field. To allow you to control the placement of the inner text field relative to * your decorations, the text field implementation will pass in a framework-controlled composable * parameter "innerTextField" to the decorationBox lambda you provide. You must call * innerTextField exactly once. */ @Composable fun BasicTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, visualTransformation: VisualTransformation = VisualTransformation.None, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource? = null, cursorBrush: Brush = SolidColor(Color.Black), decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() }, ) { // Holds the latest internal TextFieldValue state. We need to keep it to have the correct value // of the composition. var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = value)) } // Holds the latest TextFieldValue that BasicTextField was recomposed with. We couldn't simply // pass `TextFieldValue(text = value)` to the CoreTextField because we need to preserve the // composition. val textFieldValue = textFieldValueState.copy(text = value) SideEffect { if ( textFieldValue.selection != textFieldValueState.selection || textFieldValue.composition != textFieldValueState.composition ) { textFieldValueState = textFieldValue } } // Last String value that either text field was recomposed with or updated in the onValueChange // callback. We keep track of it to prevent calling onValueChange(String) for same String when // CoreTextField's onValueChange is called multiple times without recomposition in between. var lastTextValue by remember(value) { mutableStateOf(value) } CoreTextField( value = textFieldValue, onValueChange = { newTextFieldValueState -> textFieldValueState = newTextFieldValueState val stringChangedSinceLastInvocation = lastTextValue != newTextFieldValueState.text lastTextValue = newTextFieldValueState.text if (stringChangedSinceLastInvocation) { onValueChange(newTextFieldValueState.text) } }, modifier = modifier, textStyle = textStyle, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine), keyboardActions = keyboardActions, softWrap = !singleLine, minLines = if (singleLine) 1 else minLines, maxLines = if (singleLine) 1 else maxLines, decorationBox = decorationBox, enabled = enabled, readOnly = readOnly, ) } /** * Basic composable that enables users to edit text via hardware or software keyboard, but provides * no decorations like hint or placeholder. * * Whenever the user edits the text, [onValueChange] is called with the most up to date state * represented by [TextFieldValue]. [TextFieldValue] contains the text entered by user, as well as * selection, cursor and text composition information. Please check [TextFieldValue] for the * description of its contents. * * It is crucial that the value provided to the [onValueChange] is fed back into [BasicTextField] in * order to actually display and continue to edit that text in the field. The value you feed back * into the field may be different than the one provided to the [onValueChange] callback, however * the following caveats apply: * - The new value must be provided to [BasicTextField] immediately (i.e. by the next frame), or the * text field may appear to glitch, e.g. the cursor may jump around. For more information about * this requirement, see * [this article](https://developer.android.com/jetpack/compose/text/user-input#state-practices). * - The value fed back into the field may be different from the one passed to [onValueChange], * although this may result in the input connection being restarted, which can make the keyboard * flicker for the user. This is acceptable when you're using the callback to, for example, filter * out certain types of input, but should probably not be done on every update when entering * freeform text. * * This composable provides basic text editing functionality, however does not include any * decorations such as borders, hints/placeholder. A design system based implementation such as * Material Design Filled text field is typically what is needed to cover most of the needs. This * composable is designed to be used when a custom implementation for different design system is * needed. * * Example usage: * * @sample androidx.compose.foundation.samples.BasicTextFieldSample * * For example, if you need to include a placeholder in your TextField, you can write a composable * using the decoration box like this: * * @sample androidx.compose.foundation.samples.PlaceholderBasicTextFieldSample * * If you want to add decorations to your text field, such as icon or similar, and increase the hit * target area, use the decoration box: * * @sample androidx.compose.foundation.samples.TextFieldWithIconSample * * Note: This overload does not support [KeyboardOptions.showKeyboardOnFocus]. * * @param value The [androidx.compose.ui.text.input.TextFieldValue] to be shown in the * [BasicTextField]. * @param onValueChange Called when the input service updates the values in [TextFieldValue]. * @param modifier optional [Modifier] for this text field. * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text field * will be neither editable nor focusable, the input of the text field will not be selectable * @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text field * can not be modified, however, a user can focus it and copy text from it. Read-only text fields * are usually used to display pre-filled forms that user can not edit * @param textStyle Style configuration that applies at character level such as color, font etc. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction]. * @param keyboardActions when the input service emits an IME action, the corresponding callback is * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction]. * @param singleLine when set to true, this text field becomes a single horizontally scrolling text * field instead of wrapping onto multiple lines. The keyboard will be informed to not show the * return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are automatically * set to 1. * @param maxLines the maximum height in terms of maximum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param visualTransformation The visual transformation filter for changing the visual * representation of the input. By default no visual transformation is applied. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw a cursor or selection around the text. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] * provided, there will be no cursor drawn * @param decorationBox Composable lambda that allows to add decorations around text field, such as * icon, placeholder, helper messages or similar, and automatically increase the hit target area * of the text field. To allow you to control the placement of the inner text field relative to * your decorations, the text field implementation will pass in a framework-controlled composable * parameter "innerTextField" to the decorationBox lambda you provide. You must call * innerTextField exactly once. */ @Composable fun BasicTextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, visualTransformation: VisualTransformation = VisualTransformation.None, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource? = null, cursorBrush: Brush = SolidColor(Color.Black), decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() }, ) { CoreTextField( value = value, onValueChange = { if (value != it) { onValueChange(it) } }, modifier = modifier, textStyle = textStyle, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine), keyboardActions = keyboardActions, softWrap = !singleLine, minLines = if (singleLine) 1 else minLines, maxLines = if (singleLine) 1 else maxLines, decorationBox = decorationBox, enabled = enabled, readOnly = readOnly, ) } @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @Composable fun BasicTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = Int.MAX_VALUE, visualTransformation: VisualTransformation = VisualTransformation.None, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(Color.Black), decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() }, ) { BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier, enabled = enabled, readOnly = readOnly, textStyle = textStyle, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = singleLine, minLines = 1, maxLines = maxLines, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, decorationBox = decorationBox, ) } @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @Composable fun BasicTextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = TextStyle.Default, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = Int.MAX_VALUE, visualTransformation: VisualTransformation = VisualTransformation.None, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(Color.Black), decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit = @Composable { innerTextField -> innerTextField() }, ) { BasicTextField( value = value, onValueChange = onValueChange, modifier = modifier, enabled = enabled, readOnly = readOnly, textStyle = textStyle, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = singleLine, minLines = 1, maxLines = maxLines, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, decorationBox = decorationBox, ) } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation.text import androidx.compose.foundation.text.modifiers.SelectableTextAnnotatedStringElement import androidx.compose.foundation.text.modifiers.SelectionController import androidx.compose.foundation.text.modifiers.TextAnnotatedStringElement import androidx.compose.foundation.text.modifiers.TextAnnotatedStringNode import androidx.compose.foundation.text.modifiers.TextStringSimpleElement import androidx.compose.foundation.text.modifiers.hasLinks import androidx.compose.foundation.text.selection.LocalSelectionRegistrar import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.SelectionRegistrar import androidx.compose.foundation.text.selection.hasSelection import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.NonRestartableComposable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.ColorProducer import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Constraints.Companion.fitPrioritizingWidth import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.util.fastFilter import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMapIndexedNotNull import androidx.compose.ui.util.fastRoundToInt import kotlin.math.floor /** * Basic element that displays text and provides semantics / accessibility information. Typically * you will instead want to use [androidx.compose.material.Text], which is a higher level Text * element that contains semantics and consumes style information from a theme. * * @param text The text to be displayed. * @param modifier [Modifier] to apply to this layout node. * @param style Style configuration for the text such as color, font, line height etc. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw selection around the text. * @param overflow How visual overflow should be handled. * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, * [overflow] and TextAlign may have unexpected effects. * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary. * If the text exceeds the given number of lines, it will be truncated according to [overflow] and * [softWrap]. It is required that 1 <= [minLines] <= [maxLines]. * @param minLines The minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. * @param color Overrides the text color provided in [style] * @param autoSize Enable auto sizing for this text composable. Finds the biggest font size that * fits in the available space and lays the text out with this size. This performs multiple layout * passes and can be slower than using a fixed font size. This takes precedence over sizes defined * through [style]. See [TextAutoSize] and * [androidx.compose.foundation.samples.TextAutoSizeBasicTextSample]. */ @Composable fun BasicText( text: String, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, color: ColorProducer? = null, autoSize: TextAutoSize? = null, ) { validateMinMaxLines(minLines = minLines, maxLines = maxLines) val selectionRegistrar = LocalSelectionRegistrar.current val selectionController = if (selectionRegistrar != null) { val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor val selectableId = rememberSaveable(selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) { selectionRegistrar.nextSelectableId() } remember(selectableId, selectionRegistrar, backgroundSelectionColor) { SelectionController(selectableId, selectionRegistrar, backgroundSelectionColor) } } else { null } val fontFamilyResolver = LocalFontFamilyResolver.current BackgroundTextMeasurement(text = text, style = style, fontFamilyResolver = fontFamilyResolver) val finalModifier = if (selectionController != null || onTextLayout != null || autoSize != null) { modifier.textModifier( AnnotatedString(text = text), style = style, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, fontFamilyResolver = LocalFontFamilyResolver.current, placeholders = null, onPlaceholderLayout = null, selectionController = selectionController, color = color, onShowTranslation = null, autoSize = autoSize, ) } else { modifier then TextStringSimpleElement( text = text, style = style, fontFamilyResolver = fontFamilyResolver, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, color = color, ) } Layout(finalModifier, EmptyMeasurePolicy) } /** * Basic element that displays text and provides semantics / accessibility information. Typically * you will instead want to use [androidx.compose.material.Text], which is a higher level Text * element that contains semantics and consumes style information from a theme. * * @param text The text to be displayed. * @param modifier [Modifier] to apply to this layout node. * @param style Style configuration for the text such as color, font, line height etc. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw selection around the text. * @param overflow How visual overflow should be handled. * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, * [overflow] and TextAlign may have unexpected effects. * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary. * If the text exceeds the given number of lines, it will be truncated according to [overflow] and * [softWrap]. It is required that 1 <= [minLines] <= [maxLines]. * @param minLines The minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. * @param inlineContent A map store composables that replaces certain ranges of the text. It's used * to insert composables into text layout. Check [InlineTextContent] for more information. * @param color Overrides the text color provided in [style] * @param autoSize Enable auto sizing for this text composable. Finds the biggest font size that * fits in the available space and lays the text out with this size. This performs multiple layout * passes and can be slower than using a fixed font size. This takes precedence over sizes defined * through [style]. See [TextAutoSize] and * [androidx.compose.foundation.samples.TextAutoSizeBasicTextSample]. */ @Composable fun BasicText( text: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, inlineContent: Map = mapOf(), color: ColorProducer? = null, autoSize: TextAutoSize? = null, ) { validateMinMaxLines(minLines = minLines, maxLines = maxLines) val selectionRegistrar = LocalSelectionRegistrar.current val selectionController = if (selectionRegistrar != null) { val backgroundSelectionColor = LocalTextSelectionColors.current.backgroundColor val selectableId = rememberSaveable(selectionRegistrar, saver = selectionIdSaver(selectionRegistrar)) { selectionRegistrar.nextSelectableId() } remember(selectableId, selectionRegistrar, backgroundSelectionColor) { SelectionController(selectableId, selectionRegistrar, backgroundSelectionColor) } } else { null } val hasInlineContent = text.hasInlineContent() val hasLinks = text.hasLinks() val fontFamilyResolver = LocalFontFamilyResolver.current if (!hasInlineContent && !hasLinks) { BackgroundTextMeasurement( text = text, style = style, fontFamilyResolver = fontFamilyResolver, placeholders = null, ) // this is the same as text: String, use all the early exits Layout( modifier = modifier.textModifier( text = text, style = style, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, fontFamilyResolver = fontFamilyResolver, placeholders = null, onPlaceholderLayout = null, selectionController = selectionController, color = color, onShowTranslation = null, autoSize = autoSize, ), EmptyMeasurePolicy, ) } else { // takes into account text substitution (for translation) that is happening inside the // TextAnnotatedStringNode var displayedText by remember(text) { mutableStateOf(text) } LayoutWithLinksAndInlineContent( modifier = modifier, text = displayedText, onTextLayout = onTextLayout, hasInlineContent = hasInlineContent, inlineContent = inlineContent, style = style, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, fontFamilyResolver = fontFamilyResolver, selectionController = selectionController, color = color, onShowTranslation = { substitutionValue -> displayedText = if (substitutionValue.isShowingSubstitution) { substitutionValue.substitution } else { substitutionValue.original } }, autoSize = autoSize, ) } } /** * Basic element that displays text and provides semantics / accessibility information. Typically * you will instead want to use [androidx.compose.material.Text], which is a higher level Text * element that contains semantics and consumes style information from a theme. * * @param text The text to be displayed. * @param modifier [Modifier] to apply to this layout node. * @param style Style configuration for the text such as color, font, line height etc. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw selection around the text. * @param overflow How visual overflow should be handled. * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, * [overflow] and TextAlign may have unexpected effects. * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary. * If the text exceeds the given number of lines, it will be truncated according to [overflow] and * [softWrap]. It is required that 1 <= [minLines] <= [maxLines]. * @param minLines The minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. * @param color Overrides the text color provided in [style] */ @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @Composable fun BasicText( text: String, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, color: ColorProducer? = null, ) { BasicText(text, modifier, style, onTextLayout, overflow, softWrap, maxLines, minLines, color) } /** * Basic element that displays text and provides semantics / accessibility information. Typically * you will instead want to use [androidx.compose.material.Text], which is a higher level Text * element that contains semantics and consumes style information from a theme. * * @param text The text to be displayed. * @param modifier [Modifier] to apply to this layout node. * @param style Style configuration for the text such as color, font, line height etc. * @param onTextLayout Callback that is executed when a new text layout is calculated. A * [TextLayoutResult] object that callback provides contains paragraph information, size of the * text, baselines and other details. The callback can be used to add additional decoration or * functionality to the text. For example, to draw selection around the text. * @param overflow How visual overflow should be handled. * @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, * [overflow] and TextAlign may have unexpected effects. * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary. * If the text exceeds the given number of lines, it will be truncated according to [overflow] and * [softWrap]. It is required that 1 <= [minLines] <= [maxLines]. * @param minLines The minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. * @param inlineContent A map store composables that replaces certain ranges of the text. It's used * to insert composables into text layout. Check [InlineTextContent] for more information. * @param color Overrides the text color provided in [style] */ @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @Composable fun BasicText( text: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, inlineContent: Map = mapOf(), color: ColorProducer? = null, ) { BasicText( text, modifier, style, onTextLayout, overflow, softWrap, maxLines, minLines, inlineContent, color, ) } @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @Composable fun BasicText( text: String, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, ) { BasicText( text = text, modifier = modifier, style = style, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, minLines = 1, maxLines = maxLines, ) } @Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN) @Composable fun BasicText( text: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, inlineContent: Map = mapOf(), ) { BasicText( text = text, modifier = modifier, style = style, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = 1, inlineContent = inlineContent, ) } @Deprecated("Maintained for binary compat", level = DeprecationLevel.HIDDEN) @Composable fun BasicText( text: String, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, ) = BasicText(text, modifier, style, onTextLayout, overflow, softWrap, maxLines, minLines) @Deprecated("Maintained for binary compat", level = DeprecationLevel.HIDDEN) @Composable fun BasicText( text: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, onTextLayout: ((TextLayoutResult) -> Unit)? = null, overflow: TextOverflow = TextOverflow.Clip, softWrap: Boolean = true, maxLines: Int = Int.MAX_VALUE, minLines: Int = 1, inlineContent: Map = mapOf(), ) = BasicText( text = text, modifier = modifier, style = style, onTextLayout = onTextLayout, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, inlineContent = inlineContent, ) /** A custom saver that won't save if no selection is active. */ private fun selectionIdSaver(selectionRegistrar: SelectionRegistrar?) = Saver( save = { if (selectionRegistrar.hasSelection(it)) it else null }, restore = { it }, ) private object EmptyMeasurePolicy : MeasurePolicy { private val placementBlock: Placeable.PlacementScope.() -> Unit = {} override fun MeasureScope.measure( measurables: List, constraints: Constraints, ): MeasureResult { return layout(constraints.maxWidth, constraints.maxHeight, placementBlock = placementBlock) } } /** Measure policy for inline content and links */ private class TextMeasurePolicy( private val shouldMeasureLinks: () -> Boolean, private val placements: () -> List?, ) : MeasurePolicy { override fun MeasureScope.measure( measurables: List, constraints: Constraints, ): MeasureResult { // inline content val inlineContentMeasurables = measurables.fastFilter { it.parentData !is TextRangeLayoutModifier } val inlineContentToPlace = placements()?.fastMapIndexedNotNull { index, rect -> // PlaceholderRect will be null if it's ellipsized. In that case, the corresponding // inline children won't be measured or placed. rect?.let { Pair( inlineContentMeasurables[index].measure( Constraints( maxWidth = floor(it.width).toInt(), maxHeight = floor(it.height).toInt(), ) ), IntOffset(it.left.fastRoundToInt(), it.top.fastRoundToInt()), ) } } // links val linksMeasurables = measurables.fastFilter { it.parentData is TextRangeLayoutModifier } val linksToPlace = measureWithTextRangeMeasureConstraints( measurables = linksMeasurables, shouldMeasureLinks = shouldMeasureLinks, ) return layout(constraints.maxWidth, constraints.maxHeight) { // inline content inlineContentToPlace?.fastForEach { (placeable, position) -> placeable.place(position) } // links linksToPlace?.fastForEach { (placeable, measureResult) -> placeable.place(measureResult?.invoke() ?: IntOffset.Zero) } } } } /** Measure policy for links only */ private class LinksTextMeasurePolicy(private val shouldMeasureLinks: () -> Boolean) : MeasurePolicy { override fun MeasureScope.measure( measurables: List, constraints: Constraints, ): MeasureResult { return layout(constraints.maxWidth, constraints.maxHeight) { val linksToPlace = measureWithTextRangeMeasureConstraints( measurables = measurables, shouldMeasureLinks = shouldMeasureLinks, ) linksToPlace?.fastForEach { (placeable, measureResult) -> placeable.place(measureResult?.invoke() ?: IntOffset.Zero) } } } } private fun measureWithTextRangeMeasureConstraints( measurables: List, shouldMeasureLinks: () -> Boolean, ): List IntOffset)?>>? { return if (shouldMeasureLinks()) { val textRangeLayoutMeasureScope = TextRangeLayoutMeasureScope() measurables.fastMapIndexedNotNull { _, measurable -> val rangeMeasurePolicy = (measurable.parentData as TextRangeLayoutModifier).measurePolicy val rangeMeasureResult = with(rangeMeasurePolicy) { textRangeLayoutMeasureScope.measure() } val placeable = measurable.measure( fitPrioritizingWidth( minWidth = rangeMeasureResult.width, maxWidth = rangeMeasureResult.width, minHeight = rangeMeasureResult.height, maxHeight = rangeMeasureResult.height, ) ) Pair(placeable, rangeMeasureResult.place) } } else { null } } private fun Modifier.textModifier( text: AnnotatedString, style: TextStyle, onTextLayout: ((TextLayoutResult) -> Unit)?, overflow: TextOverflow, softWrap: Boolean, maxLines: Int, minLines: Int, fontFamilyResolver: FontFamily.Resolver, placeholders: List>?, onPlaceholderLayout: ((List) -> Unit)?, selectionController: SelectionController?, color: ColorProducer?, onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)?, autoSize: TextAutoSize?, ): Modifier { if (selectionController == null) { val staticTextModifier = TextAnnotatedStringElement( text, style, fontFamilyResolver, onTextLayout, overflow, softWrap, maxLines, minLines, placeholders, onPlaceholderLayout, null, color, autoSize, onShowTranslation, ) return this then Modifier /* selection position */ then staticTextModifier } else { val selectableTextModifier = SelectableTextAnnotatedStringElement( text, style, fontFamilyResolver, onTextLayout, overflow, softWrap, maxLines, minLines, placeholders, onPlaceholderLayout, selectionController, color, autoSize, ) return this then selectionController.modifier then selectableTextModifier } } @Composable private fun LayoutWithLinksAndInlineContent( modifier: Modifier, text: AnnotatedString, onTextLayout: ((TextLayoutResult) -> Unit)?, hasInlineContent: Boolean, inlineContent: Map = mapOf(), style: TextStyle, overflow: TextOverflow, softWrap: Boolean, maxLines: Int, minLines: Int, fontFamilyResolver: FontFamily.Resolver, selectionController: SelectionController?, color: ColorProducer?, onShowTranslation: ((TextAnnotatedStringNode.TextSubstitutionValue) -> Unit)?, autoSize: TextAutoSize?, ) { val textScope = if (text.hasLinks()) { remember(text) { TextLinkScope(text) } } else null // only adds additional span styles to the existing link annotations, doesn't semantically // change the text val styledText: () -> AnnotatedString = if (text.hasLinks()) { remember(text, textScope) { { textScope?.applyAnnotators() ?: text } } } else { { text } } // do the inline content allocs val (placeholders, inlineComposables) = if (hasInlineContent) { text.resolveInlineContent(inlineContent = inlineContent) } else Pair(null, null) val measuredPlaceholderPositions = if (hasInlineContent) { remember?>> { mutableStateOf(null) } } else null val onPlaceholderLayout: ((List) -> Unit)? = if (hasInlineContent) { { measuredPlaceholderPositions?.value = it } } else null BackgroundTextMeasurement( text = text, style = style, fontFamilyResolver = fontFamilyResolver, placeholders = placeholders, ) Layout( content = { textScope?.LinksComposables() inlineComposables?.let { InlineChildren(text = text, inlineContents = it) } }, modifier = modifier.textModifier( text = styledText(), style = style, onTextLayout = { textScope?.textLayoutResult = it onTextLayout?.invoke(it) }, overflow = overflow, softWrap = softWrap, maxLines = maxLines, minLines = minLines, fontFamilyResolver = fontFamilyResolver, placeholders = placeholders, onPlaceholderLayout = onPlaceholderLayout, selectionController = selectionController, color = color, onShowTranslation = onShowTranslation, autoSize = autoSize, ), measurePolicy = if (!hasInlineContent) { LinksTextMeasurePolicy( shouldMeasureLinks = { textScope?.let { it.shouldMeasureLinks() } ?: false } ) } else { TextMeasurePolicy( shouldMeasureLinks = { textScope?.let { it.shouldMeasureLinks() } ?: false }, placements = { measuredPlaceholderPositions?.value }, ) }, ) } /** * This function pre-measures the text on Android platform to warm the platform text layout cache in * a background thread before the actual text layout begins. */ @Composable @NonRestartableComposable internal expect fun BackgroundTextMeasurement( text: String, style: TextStyle, fontFamilyResolver: FontFamily.Resolver, ) /** * This function pre-measures the text on Android platform to warm the platform text layout cache in * a background thread before the actual text layout begins. */ @Composable @NonRestartableComposable internal expect fun BackgroundTextMeasurement( text: AnnotatedString, style: TextStyle, fontFamilyResolver: FontFamily.Resolver, placeholders: List>?, ) ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Draggable.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.foundation.gestures import androidx.compose.foundation.ComposeFoundationFlags.isDelayPressesUsingGestureConsumptionEnabled import androidx.compose.foundation.ComposeFoundationFlags.isNestedDraggablesTouchConflictFixEnabled import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.GestureConnection import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.MutatorMutex import androidx.compose.foundation.gestureNode import androidx.compose.foundation.gestures.DragEvent.DragCancelled import androidx.compose.foundation.gestures.DragEvent.DragDelta import androidx.compose.foundation.gestures.DragEvent.DragStarted import androidx.compose.foundation.gestures.DragEvent.DragStopped import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.internal.JvmDefaultWithCompatibility import androidx.compose.foundation.parentGestureConnection import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.ExperimentalIndirectPointerApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.isSpecified import androidx.compose.ui.input.indirect.IndirectPointerEvent import androidx.compose.ui.input.indirect.IndirectPointerInputChange import androidx.compose.ui.input.indirect.IndirectPointerInputModifierNode import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.pointer.changedToDownIgnoreConsumed import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.input.pointer.positionChangeIgnoreConsumed import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.input.pointer.util.addPointerInputChange import androidx.compose.ui.layout.positionOnScreen import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.node.requireLayoutCoordinates import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.Velocity import androidx.compose.ui.util.fastAll import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFirstOrNull import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.isActive import kotlinx.coroutines.launch /** * State of [draggable]. Allows for a granular control of how deltas are consumed by the user as * well as to write custom drag methods using [drag] suspend function. */ @JvmDefaultWithCompatibility interface DraggableState { /** * Call this function to take control of drag logic. * * All actions that change the logical drag position must be performed within a [drag] block * (even if they don't call any other methods on this object) in order to guarantee that mutual * exclusion is enforced. * * If [drag] is called from elsewhere with the [dragPriority] higher or equal to ongoing drag, * ongoing drag will be canceled. * * @param dragPriority of the drag operation * @param block to perform drag in */ suspend fun drag( dragPriority: MutatePriority = MutatePriority.Default, block: suspend DragScope.() -> Unit, ) /** * Dispatch drag delta in pixels avoiding all drag related priority mechanisms. * * **NOTE:** unlike [drag], dispatching any delta with this method will bypass scrolling of any * priority. This method will also ignore `reverseDirection` and other parameters set in * [draggable]. * * This method is used internally for low level operations, allowing implementers of * [DraggableState] influence the consumption as suits them, e.g. introduce nested scrolling. * Manually dispatching delta via this method will likely result in a bad user experience, you * must prefer [drag] method over this one. * * @param delta amount of scroll dispatched in the nested drag process */ fun dispatchRawDelta(delta: Float) } /** Scope used for suspending drag blocks */ interface DragScope { /** Attempts to drag by [pixels] px. */ fun dragBy(pixels: Float) } /** * Default implementation of [DraggableState] interface that allows to pass a simple action that * will be invoked when the drag occurs. * * This is the simplest way to set up a [draggable] modifier. When constructing this * [DraggableState], you must provide a [onDelta] lambda, which will be invoked whenever drag * happens (by gesture input or a custom [DraggableState.drag] call) with the delta in pixels. * * If you are creating [DraggableState] in composition, consider using [rememberDraggableState]. * * @param onDelta callback invoked when drag occurs. The callback receives the delta in pixels. */ fun DraggableState(onDelta: (Float) -> Unit): DraggableState = DefaultDraggableState(onDelta) /** * Create and remember default implementation of [DraggableState] interface that allows to pass a * simple action that will be invoked when the drag occurs. * * This is the simplest way to set up a [draggable] modifier. When constructing this * [DraggableState], you must provide a [onDelta] lambda, which will be invoked whenever drag * happens (by gesture input or a custom [DraggableState.drag] call) with the delta in pixels. * * @param onDelta callback invoked when drag occurs. The callback receives the delta in pixels. */ @Composable fun rememberDraggableState(onDelta: (Float) -> Unit): DraggableState { val onDeltaState = rememberUpdatedState(onDelta) return remember { DraggableState { onDeltaState.value.invoke(it) } } } /** * Configure touch dragging for the UI element in a single [Orientation]. The drag distance reported * to [DraggableState], allowing users to react on the drag delta and update their state. * * The common usecase for this component is when you need to be able to drag something inside the * component on the screen and represent this state via one float value * * If you need to control the whole dragging flow, consider using [pointerInput] instead with the * helper functions like [detectDragGestures]. * * If you want to enable dragging in 2 dimensions, consider using [draggable2D]. * * If you are implementing scroll/fling behavior, consider using [scrollable]. * * @sample androidx.compose.foundation.samples.DraggableSample * @param state [DraggableState] state of the draggable. Defines how drag events will be interpreted * by the user land logic. * @param orientation orientation of the drag * @param enabled whether or not drag is enabled * @param interactionSource [MutableInteractionSource] that will be used to emit * [DragInteraction.Start] when this draggable is being dragged. * @param startDragImmediately when set to true, draggable will start dragging immediately and * prevent other gesture detectors from reacting to "down" events (in order to block composed * press-based gestures). This is intended to allow end users to "catch" an animating widget by * pressing on it. It's useful to set it when value you're dragging is settling / animating. * @param onDragStarted callback that will be invoked when drag is about to start at the starting * position, allowing user to suspend and perform preparation for drag, if desired. This suspend * function is invoked with the draggable scope, allowing for async processing, if desired. Note * that the scope used here is the one provided by the draggable node, for long running work that * needs to outlast the modifier being in the composition you should use a scope that fits the * lifecycle needed. * @param onDragStopped callback that will be invoked when drag is finished, allowing the user to * react on velocity and process it. This suspend function is invoked with the draggable scope, * allowing for async processing, if desired. Note that the scope used here is the one provided by * the draggable node, for long running work that needs to outlast the modifier being in the * composition you should use a scope that fits the lifecycle needed. * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave * like bottom to top and left to right will behave like right to left. */ @Stable fun Modifier.draggable( state: DraggableState, orientation: Orientation, enabled: Boolean = true, interactionSource: MutableInteractionSource? = null, startDragImmediately: Boolean = false, onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = NoOpOnDragStarted, onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = NoOpOnDragStopped, reverseDirection: Boolean = false, ): Modifier = this then DraggableElement( state = state, orientation = orientation, enabled = enabled, interactionSource = interactionSource, startDragImmediately = startDragImmediately, onDragStarted = onDragStarted, onDragStopped = onDragStopped, reverseDirection = reverseDirection, ) internal class DraggableElement( private val state: DraggableState, private val orientation: Orientation, private val enabled: Boolean, private val interactionSource: MutableInteractionSource?, private val startDragImmediately: Boolean, private val onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit, private val onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit, private val reverseDirection: Boolean, ) : ModifierNodeElement() { override fun create(): DraggableNode = DraggableNode( state, CanDrag, orientation, enabled, interactionSource, startDragImmediately, onDragStarted, onDragStopped, reverseDirection, ) override fun update(node: DraggableNode) { node.update( state, CanDrag, orientation, enabled, interactionSource, startDragImmediately, onDragStarted, onDragStopped, reverseDirection, ) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other === null) return false if (this::class != other::class) return false other as DraggableElement if (state != other.state) return false if (orientation != other.orientation) return false if (enabled != other.enabled) return false if (interactionSource != other.interactionSource) return false if (startDragImmediately != other.startDragImmediately) return false if (onDragStarted != other.onDragStarted) return false if (onDragStopped != other.onDragStopped) return false if (reverseDirection != other.reverseDirection) return false return true } override fun hashCode(): Int { var result = state.hashCode() result = 31 * result + orientation.hashCode() result = 31 * result + enabled.hashCode() result = 31 * result + (interactionSource?.hashCode() ?: 0) result = 31 * result + startDragImmediately.hashCode() result = 31 * result + onDragStarted.hashCode() result = 31 * result + onDragStopped.hashCode() result = 31 * result + reverseDirection.hashCode() return result } override fun InspectorInfo.inspectableProperties() { name = "draggable" properties["orientation"] = orientation properties["enabled"] = enabled properties["reverseDirection"] = reverseDirection properties["interactionSource"] = interactionSource properties["startDragImmediately"] = startDragImmediately properties["onDragStarted"] = onDragStarted properties["onDragStopped"] = onDragStopped properties["state"] = state } companion object { val CanDrag: (PointerType) -> Boolean = { true } } } internal class DraggableNode( private var state: DraggableState, canDrag: (PointerType) -> Boolean, private var orientation: Orientation, enabled: Boolean, interactionSource: MutableInteractionSource?, private var startDragImmediately: Boolean, private var onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit, private var onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit, private var reverseDirection: Boolean, ) : DragGestureNode( canDrag = canDrag, enabled = enabled, interactionSource = interactionSource, orientationLock = orientation, ) { override suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) { state.drag(MutatePriority.UserInput) { forEachDelta { dragDelta -> dragBy(dragDelta.delta.reverseIfNeeded().toFloat(orientation)) } } } override fun onDragStarted(startedPosition: Offset) { if (!isAttached || onDragStarted == NoOpOnDragStarted) return coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { this@DraggableNode.onDragStarted(this, startedPosition) } } override fun onDragStopped(event: DragStopped) { if (!isAttached || onDragStopped == NoOpOnDragStopped) return coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) { this@DraggableNode.onDragStopped( this, event.velocity.reverseIfNeeded().toFloat(orientation), ) } } override fun startDragImmediately(): Boolean = startDragImmediately fun update( state: DraggableState, canDrag: (PointerType) -> Boolean, orientation: Orientation, enabled: Boolean, interactionSource: MutableInteractionSource?, startDragImmediately: Boolean, onDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit, onDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit, reverseDirection: Boolean, ) { var resetPointerInputHandling = false if (this.state != state) { this.state = state resetPointerInputHandling = true } if (this.orientation != orientation) { this.orientation = orientation resetPointerInputHandling = true } if (this.reverseDirection != reverseDirection) { this.reverseDirection = reverseDirection resetPointerInputHandling = true } this.onDragStarted = onDragStarted this.onDragStopped = onDragStopped this.startDragImmediately = startDragImmediately update(canDrag, enabled, interactionSource, orientation, resetPointerInputHandling) } private fun Velocity.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f private fun Offset.reverseIfNeeded() = if (reverseDirection) this * -1f else this * 1f } /** A node that performs drag gesture recognition and event propagation. */ @OptIn(ExperimentalFoundationApi::class) internal abstract class DragGestureNode( canDrag: (PointerType) -> Boolean, enabled: Boolean, interactionSource: MutableInteractionSource?, var orientationLock: Orientation?, ) : DelegatingNode(), PointerInputModifierNode, IndirectPointerInputModifierNode, CompositionLocalConsumerModifierNode, GestureConnection { var canDrag = canDrag private set protected var enabled = enabled private set protected var interactionSource = interactionSource private set private var gestureNode: DelegatableNode? = null // Use wrapper lambdas here to make sure that if these properties are updated while we suspend, // we point to the new reference when we invoke them. startDragImmediately is a lambda since we // need the most recent value passed to it from Scrollable. private val _canDrag: (PointerType) -> Boolean = { this.canDrag(it) } private var channel: Channel? = null private var dragInteraction: DragInteraction.Start? = null internal var isListeningForEvents = false internal var isListeningForPointerInputEvents = false /** Store non-initialized states for re-use */ private var _awaitDownState: DragDetectionState.AwaitDown? = null private val awaitDownState: DragDetectionState.AwaitDown get() = _awaitDownState ?: DragDetectionState.AwaitDown().also { _awaitDownState = it } private var _draggingState: DragDetectionState.Dragging? = null private val draggingState: DragDetectionState.Dragging get() = _draggingState ?: DragDetectionState.Dragging().also { _draggingState = it } private var _awaitTouchSlopState: DragDetectionState.AwaitTouchSlop? = null private val awaitTouchSlopState: DragDetectionState.AwaitTouchSlop get() = _awaitTouchSlopState ?: DragDetectionState.AwaitTouchSlop().also { _awaitTouchSlopState = it } private var _awaitGesturePickupState: DragDetectionState.AwaitGesturePickup? = null private val awaitGesturePickupState: DragDetectionState.AwaitGesturePickup get() = _awaitGesturePickupState ?: DragDetectionState.AwaitGesturePickup().also { _awaitGesturePickupState = it } private var currentDragState: DragDetectionState? = null private var velocityTracker: VelocityTracker? = null private var previousPositionOnScreen = Offset.Unspecified private var touchSlopDetector: TouchSlopDetector? = null private var indirectPointerInputDragCycleDetector: IndirectPointerInputDragCycleDetector? = null /** * Accumulated position offset of this [Modifier.Node] that happened during a drag cycle. This * is used to correct the pointer input events that are added to the Velocity Tracker. If this * Node is static during the drag cycle, nothing will happen. On the other hand, if the position * of this node changes during the drag cycle, we need to correct the Pointer Input used for the * drag events, this is because Velocity Tracker doesn't have the knowledge about changes in the * position of the container that uses it, and because each Pointer Input event is related to * the container's root. */ private var nodeOffset = Offset.Zero /** * Responsible for the dragging behavior between the start and the end of the drag. It * continually invokes `forEachDelta` to process incoming events. In return, `forEachDelta` * calls `dragBy` method to process each individual delta. */ abstract suspend fun drag(forEachDelta: suspend ((dragDelta: DragDelta) -> Unit) -> Unit) /** * Passes the action needed when a drag starts. This gives the ability to pass the desired * behavior from other nodes implementing AbstractDraggableNode */ abstract fun onDragStarted(startedPosition: Offset) /** * Passes the action needed when a drag stops. This gives the ability to pass the desired * behavior from other nodes implementing AbstractDraggableNode */ abstract fun onDragStopped(event: DragStopped) /** * If touch slop recognition should be skipped. If this is true, this node will start * recognizing drag events immediately without waiting for touch slop. */ abstract fun startDragImmediately(): Boolean private fun requireVelocityTracker(): VelocityTracker = requireNotNull(velocityTracker) { "Velocity Tracker not initialized." } private fun requireChannel(): Channel = requireNotNull(channel) { "Events channel not initialized." } private fun requireTouchSlopDetector(): TouchSlopDetector = requireNotNull(touchSlopDetector) { "Touch slop detector not initialized." } @OptIn(ExperimentalFoundationApi::class) private fun startListeningForEvents() { isListeningForEvents = true if (channel == null) { channel = Channel(capacity = Channel.UNLIMITED) } /** * To preserve the original behavior we had (before the Modifier.Node migration) we need to * scope the DragStopped and DragCancel methods to the node's coroutine scope instead of * using the one provided by the pointer input modifier, this is to ensure that even when * the pointer input scope is reset we will continue any coroutine scope scope that we * started from these methods while the pointer input scope was active. */ coroutineScope.launch { while (isActive) { var event = channel?.receive() if (event !is DragStarted) continue processDragStart(event) try { drag { processDelta -> while (event !is DragStopped && event !is DragCancelled) { (event as? DragDelta)?.let(processDelta) event = channel?.receive() } } if (event is DragStopped) { processDragStop(event as DragStopped) } else if (event is DragCancelled) { processDragCancel() } } catch (c: CancellationException) { processDragCancel() } } } } override fun onDetach() { isListeningForEvents = false disposeInteractionSource() nodeOffset = Offset.Zero gestureNode?.let { undelegate(it) } gestureNode = null } protected fun initializeGestureCoordination() { if (!isDelayPressesUsingGestureConsumptionEnabled) return if (gestureNode == null) { gestureNode = delegate(gestureNode(this)) } } @OptIn(ExperimentalIndirectPointerApi::class) override fun isInterested(event: IndirectPointerInputChange): Boolean { // for now, if this is a down event it may become a drag so we're // interested. return event.changedToDownIgnoreConsumed() && enabled } @OptIn(ExperimentalFoundationApi::class) override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { isListeningForPointerInputEvents = true initializeGestureCoordination() if (enabled) { // initialize current state if (currentDragState == null) currentDragState = awaitDownState processRawPointerEvent(pointerEvent, pass) } } override fun onIndirectPointerEvent(event: IndirectPointerEvent, pass: PointerEventPass) { initializeGestureCoordination() if (enabled) { if (indirectPointerInputDragCycleDetector == null) { indirectPointerInputDragCycleDetector = IndirectPointerInputDragCycleDetector(this) } indirectPointerInputDragCycleDetector?.processIndirectPointerInputEvent(event, pass) } } override fun onCancelIndirectPointerInput() { indirectPointerInputDragCycleDetector?.resetDragDetectionState() } /** * Draggable containers will be interested in the following events: * 1) DOWN events. They may become a drag gesture later. * 2) The touch slop trigger event if the preceding deltas form an angle of interest. The touch * slop trigger event is when, effectively, draggables will start consuming. So at this * point, we look at the collected deltas since the first down event, and we decide if we're * interested based on the angle that those deltas form. We will favor vertical drags over * horizontal drags more because UX-wise there's more freedom and uncertainty when a user * performs a vertical gesture vs. a horizontal gesture. */ override fun isInterested(event: PointerInputChange): Boolean { if (event.changedToDownIgnoreConsumed()) return enabled if (!isNestedDraggablesTouchConflictFixEnabled) return false if (event.changedToUpIgnoreConsumed()) return false if (touchSlopDetector == null) { touchSlopDetector = TouchSlopDetector(orientationLock) } val touchSlop = currentValueOf(LocalViewConfiguration).touchSlop val positionChange = event.positionChange() return with(requireTouchSlopDetector()) { getPostSlopOffset(positionChange, touchSlop, false) != Offset.Unspecified && isDeltaAtAngleOfInterest(positionChange) } } override fun onCancelPointerInput() { if (isListeningForPointerInputEvents) resetDragDetectionState() isListeningForPointerInputEvents = false } private suspend fun processDragStart(event: DragStarted) { dragInteraction?.let { oldInteraction -> interactionSource?.emit(DragInteraction.Cancel(oldInteraction)) } val interaction = DragInteraction.Start() interactionSource?.emit(interaction) dragInteraction = interaction onDragStarted(event.startPoint) } private suspend fun processDragStop(event: DragStopped) { dragInteraction?.let { interaction -> interactionSource?.emit(DragInteraction.Stop(interaction)) dragInteraction = null } onDragStopped(event) } private suspend fun processDragCancel() { dragInteraction?.let { interaction -> interactionSource?.emit(DragInteraction.Cancel(interaction)) dragInteraction = null } onDragStopped(DragStopped(Velocity.Zero, isIndirectPointerEvent = false)) } fun disposeInteractionSource() { dragInteraction?.let { interaction -> interactionSource?.tryEmit(DragInteraction.Cancel(interaction)) dragInteraction = null } } fun update( canDrag: (PointerType) -> Boolean = this.canDrag, enabled: Boolean = this.enabled, interactionSource: MutableInteractionSource? = this.interactionSource, orientationLock: Orientation? = this.orientationLock, shouldResetPointerInputHandling: Boolean = false, ) { var resetPointerInputHandling = shouldResetPointerInputHandling this.canDrag = canDrag if (this.enabled != enabled) { this.enabled = enabled if (!enabled) { disposeInteractionSource() indirectPointerInputDragCycleDetector = null } resetPointerInputHandling = true } if (this.interactionSource != interactionSource) { disposeInteractionSource() this.interactionSource = interactionSource } if (this.orientationLock != orientationLock) { this.orientationLock = orientationLock resetPointerInputHandling = true } if (resetPointerInputHandling) { if (isListeningForPointerInputEvents) resetDragDetectionState() indirectPointerInputDragCycleDetector?.resetDragDetectionState() } } private fun processRawPointerEvent(pointerEvent: PointerEvent, pass: PointerEventPass) { when ( val state = requireNotNull(currentDragState) { "currentDragState should not be null" } ) { is DragDetectionState.AwaitDown -> processInitialDownState(pointerEvent, pass, state) is DragDetectionState.AwaitTouchSlop -> processAwaitTouchSlop(pointerEvent, pass, state) is DragDetectionState.AwaitGesturePickup -> processAwaitGesturePickup(pointerEvent, pass, state) is DragDetectionState.Dragging -> processDraggingState(pointerEvent, pass, state) } } private fun resetDragDetectionState() { moveToAwaitDownState() if (isListeningForEvents) sendDragCancelled() velocityTracker = null } private fun moveToAwaitTouchSlopState( initialDown: PointerInputChange, pointerId: PointerId, initialTouchSlopPositionChange: Offset = Offset.Zero, verifyConsumptionInFinalPass: Boolean = false, ) { currentDragState = awaitTouchSlopState.apply { this.initialDown = initialDown this.pointerId = pointerId if (touchSlopDetector == null) { touchSlopDetector = TouchSlopDetector(orientationLock) } else { touchSlopDetector?.orientation = orientationLock touchSlopDetector?.reset(initialTouchSlopPositionChange) } this.verifyConsumptionInFinalPass = verifyConsumptionInFinalPass } } private fun moveToDraggingState(pointerId: PointerId) { currentDragState = draggingState.apply { this.pointerId = pointerId } } private fun moveToAwaitDownState() { currentDragState = awaitDownState.apply { awaitTouchSlop = DragDetectionState.AwaitDown.AwaitTouchSlop.NotInitialized consumedOnInitial = false } } private fun moveToAwaitGesturePickupState( initialDown: PointerInputChange, pointerId: PointerId, touchSlopDetector: TouchSlopDetector, ) { currentDragState = awaitGesturePickupState.apply { this.initialDown = initialDown this.pointerId = pointerId this.touchSlopDetector = touchSlopDetector.also { it.reset() } } } private fun processInitialDownState( pointerEvent: PointerEvent, pass: PointerEventPass, state: DragDetectionState.AwaitDown, ) { /** Wait for a down event in any pass. */ if (pointerEvent.changes.isEmpty()) return if (!pointerEvent.isChangedToDown(requireUnconsumed = false)) return val firstDown = pointerEvent.changes.first() val awaitTouchSlop = when (state.awaitTouchSlop) { DragDetectionState.AwaitDown.AwaitTouchSlop.NotInitialized -> { if (!startDragImmediately()) { DragDetectionState.AwaitDown.AwaitTouchSlop.Yes } else { DragDetectionState.AwaitDown.AwaitTouchSlop.No } } else -> state.awaitTouchSlop } // update the touch slop in the current state state.awaitTouchSlop = awaitTouchSlop if (pass == PointerEventPass.Initial) { // If we shouldn't await touch slop, we consume the event immediately. if (awaitTouchSlop == DragDetectionState.AwaitDown.AwaitTouchSlop.No) { firstDown.consume() // Change state properties so we dispatch only later, this aligns with the previous // behavior where dispatching only happened during the main pass state.consumedOnInitial = true } } if (pass == PointerEventPass.Main) { /** * At this point we detected a Down event, if we should await the slop we move to the * next state. If we shouldn't await the slop and we already consumed the event we * dispatch the drag start events and start the dragging state. */ if (awaitTouchSlop == DragDetectionState.AwaitDown.AwaitTouchSlop.Yes) { moveToAwaitTouchSlopState(firstDown, firstDown.id) } else if (state.consumedOnInitial) { sendDragStart(firstDown, firstDown, Offset.Zero) sendDragEvent(firstDown, Offset.Zero) moveToDraggingState(firstDown.id) } } } private fun processAwaitTouchSlop( pointerEvent: PointerEvent, pass: PointerEventPass, state: DragDetectionState.AwaitTouchSlop, ) { /** Slop detection only cares about the main and final passes */ if (pass == PointerEventPass.Initial) return val eventFromPointerId = pointerEvent.changes.fastFirstOrNull { it.id == state.pointerId } /** * We lost this pointer, try to replace it. This is to cover the case where multiple * pointers were down, but the original one we tracked (state.pointerId) is no longer down, * try to move tracking to a different pointer */ val dragEvent = if (eventFromPointerId == null) { val otherDown = pointerEvent.changes.fastFirstOrNull { it.pressed } if (otherDown == null) { // There are no other pointers down, reset the state moveToAwaitDownState() return } else { // a new pointer was found, update the current state. state.pointerId = otherDown.id } otherDown } else { eventFromPointerId } /** * Slop detection routines happens during the Main pass. Do we have unconsumed events for * this pointer? */ if (pass == PointerEventPass.Main) { if (!dragEvent.isConsumed) { if (dragEvent.changedToUpIgnoreConsumed()) { /** The pointer lifted, look for another pointer */ val otherDown = pointerEvent.changes.fastFirstOrNull { it.pressed } if (otherDown == null) { // There are no other pointers down, reset the state moveToAwaitDownState() } else { // a new pointer was found, update the current state. state.pointerId = otherDown.id } } else { // this is a regular event (MOVE) val touchSlop = currentValueOf(LocalViewConfiguration).pointerSlop(dragEvent.type) // add data to the slop detector val postSlopOffset = requireTouchSlopDetector() .getPostSlopOffset(dragEvent.positionChangeIgnoreConsumed(), touchSlop) /** * Here we use the [gestureNode] and [GestureConnection] APIs to make a * decision. About this gesture. At this point we have all the triggers to start * a recognizing a gesture in this current * [androidx.compose.foundation.gestures.DragGestureNode]. This is the moment * that touch slop is recognized here in this node. During this time, before we * start consuming drag events we check the interested of the parent and our * self-interest. If the parent is interested and we're not (for this specific * event), we will give the parent a chance to do something by postponing the * remaining consumption to the final pass. */ if (isNestedDraggablesTouchConflictFixEnabled) { if (postSlopOffset.isSpecified) { val isSelfInterested = isInterested(dragEvent) val isParentInterested = parentGestureConnection?.isInterested(dragEvent) == true if (!isSelfInterested && isParentInterested) { state.verifyConsumptionInFinalPass = true } else { dragEvent.consume() sendDragStart(state.initialDown!!, dragEvent, postSlopOffset) sendDragEvent(dragEvent, postSlopOffset) moveToDraggingState(dragEvent.id) } } else { state.verifyConsumptionInFinalPass = true } } else { if (postSlopOffset.isSpecified) { dragEvent.consume() sendDragStart(state.initialDown!!, dragEvent, postSlopOffset) sendDragEvent(dragEvent, postSlopOffset) moveToDraggingState(dragEvent.id) } else { state.verifyConsumptionInFinalPass = true } } } } else { // This draggable "lost" the event as it was consumed by someone else, enter the // gesture pickup state if the feature is enabled. // Someone consumed this gesture, move this to the await pickup state. moveToAwaitGesturePickupState( requireNotNull(state.initialDown) { "AwaitTouchSlop.initialDown was not initialized" }, state.pointerId, requireNotNull(touchSlopDetector) { "AwaitTouchSlop.touchSlopDetector was not initialized" }, ) } } /** * This checks 2 cases: 1) A parent consumed in the main pass and this child can only see * that consumption during the final pass. 2) The parent actually consumed during the final * pass. */ if (pass == PointerEventPass.Final && state.verifyConsumptionInFinalPass) { if (dragEvent.isConsumed) { // This draggable "lost" the event as it was consumed by someone else, enter the // gesture pickup state if the feature is enabled. // Someone consumed this gesture, move this to the await pickup state. moveToAwaitGesturePickupState( requireNotNull(state.initialDown) { "AwaitTouchSlop.initialDown was not initialized" }, state.pointerId, requireNotNull(touchSlopDetector) { "AwaitTouchSlop.touchSlopDetector was not initialized" }, ) } else { /** * Self and nobody consumed dragEvent. We will only get here if self didn't consume * in the main pass OR if self wasn't interested during the main pass. In this case * we remain in the awaitTouchSlop state and wait for more information (events). */ state.verifyConsumptionInFinalPass = false } } } private fun processAwaitGesturePickup( pointerEvent: PointerEvent, pass: PointerEventPass, state: DragDetectionState.AwaitGesturePickup, ) { /** * Drag pickup only happens during the final pass so we're sure nobody else was interested * in this gesture. */ if (pass != PointerEventPass.Final) return val hasUnconsumedDrag = pointerEvent.changes.fastAll { !it.isConsumed } val hasDownPointers = pointerEvent.changes.fastAny { it.pressed } // all pointers are up, reset if (!hasDownPointers || pointerEvent.changes.isEmpty()) { moveToAwaitDownState() } else if (hasUnconsumedDrag) { // has pointers down with unconsumed events, a chance to pick up this gesture, // move to the touch slop detection phase val initialPositionChange = pointerEvent.changes.first().position - state.initialDown!!.position // await touch slop again, using the initial down as starting point. // For most cases this should return immediately since we probably moved // far enough from the initial down event. moveToAwaitTouchSlopState( requireNotNull(state.initialDown) { "AwaitGesturePickup.initialDown was not initialized." }, state.pointerId, initialPositionChange, ) } } private fun processDraggingState( pointerEvent: PointerEvent, pass: PointerEventPass, state: DragDetectionState.Dragging, ) { if (pass != PointerEventPass.Main) return val pointer = state.pointerId val dragEvent = pointerEvent.changes.fastFirstOrNull { it.id == pointer } ?: return if (dragEvent.changedToUpIgnoreConsumed()) { val otherDown = pointerEvent.changes.fastFirstOrNull { it.pressed } if (otherDown == null) { // This is the last "up" if (!dragEvent.isConsumed && dragEvent.changedToUpIgnoreConsumed()) { sendDragStopped(dragEvent) } else { sendDragCancelled() } moveToAwaitDownState() } else { state.pointerId = otherDown.id } } else { if (dragEvent.isConsumed) { sendDragCancelled() } else { val positionChange = dragEvent.positionChangeIgnoreConsumed() /** * During the gesture pickup we can pickup events at any direction so disable the * orientation lock. */ val motionChange = positionChange.getDistance() if (motionChange != 0.0f) { val positionChange = dragEvent.positionChange() sendDragEvent(dragEvent, positionChange) dragEvent.consume() } } } } private fun sendDragStart( down: PointerInputChange, slopTriggerChange: PointerInputChange, overSlopOffset: Offset, ) { if (velocityTracker == null) velocityTracker = VelocityTracker() requireVelocityTracker().addPointerInputChange(down) val dragStartedOffset = slopTriggerChange.position - overSlopOffset // the drag start event offset is the down event + touch slop value // or in this case the event that triggered the touch slop minus // the post slop offset nodeOffset = Offset.Zero // restart node offset if (canDrag(down.type)) { if (!isListeningForEvents) { if (channel == null) { channel = Channel(capacity = Channel.UNLIMITED) } startListeningForEvents() } previousPositionOnScreen = requireLayoutCoordinates().positionOnScreen() requireChannel().trySend(DragStarted(dragStartedOffset)) } } private fun sendDragEvent(change: PointerInputChange, dragAmount: Offset) { val currentPositionOnScreen = node.requireLayoutCoordinates().positionOnScreen() // container changed positions if ( previousPositionOnScreen != Offset.Unspecified && currentPositionOnScreen != previousPositionOnScreen ) { val delta = currentPositionOnScreen - previousPositionOnScreen nodeOffset += delta } previousPositionOnScreen = currentPositionOnScreen requireVelocityTracker().addPointerInputChange(event = change, offset = nodeOffset) requireChannel().trySend(DragDelta(dragAmount, false)) } private fun sendDragStopped(change: PointerInputChange) { requireVelocityTracker().addPointerInputChange(change) val maximumVelocity = currentValueOf(LocalViewConfiguration).maximumFlingVelocity val velocity = requireVelocityTracker().calculateVelocity(Velocity(maximumVelocity, maximumVelocity)) requireVelocityTracker().resetTracking() requireChannel().trySend(DragStopped(velocity.toValidVelocity(), false)) isListeningForPointerInputEvents = false } private fun sendDragCancelled() { requireChannel().trySend(DragCancelled) } fun onDragEvent(event: DragEvent) { if (event is DragStarted && !isListeningForEvents) { isListeningForEvents = true startListeningForEvents() } requireChannel().trySend(event) } } private class DefaultDraggableState(val onDelta: (Float) -> Unit) : DraggableState { private val dragScope: DragScope = object : DragScope { override fun dragBy(pixels: Float): Unit = onDelta(pixels) } private val scrollMutex = MutatorMutex() override suspend fun drag( dragPriority: MutatePriority, block: suspend DragScope.() -> Unit, ): Unit = coroutineScope { scrollMutex.mutateWith(dragScope, dragPriority, block) } override fun dispatchRawDelta(delta: Float) { return onDelta(delta) } } internal sealed class DragEvent { class DragStarted(val startPoint: Offset) : DragEvent() class DragStopped(val velocity: Velocity, val isIndirectPointerEvent: Boolean) : DragEvent() object DragCancelled : DragEvent() class DragDelta(val delta: Offset, val isIndirectPointerEvent: Boolean) : DragEvent() } internal fun Offset.toFloat(orientation: Orientation) = if (orientation == Orientation.Vertical) this.y else this.x private fun Velocity.toFloat(orientation: Orientation) = if (orientation == Orientation.Vertical) this.y else this.x internal fun Velocity.toValidVelocity() = Velocity(if (this.x.isNaN()) 0f else this.x, if (this.y.isNaN()) 0f else this.y) private val NoOpOnDragStarted: suspend CoroutineScope.(startedPosition: Offset) -> Unit = {} private val NoOpOnDragStopped: suspend CoroutineScope.(velocity: Float) -> Unit = {} private sealed class DragDetectionState { /** * Starter state for any drag gesture cycle. At this state we're waiting for a Down event to * indicate that a drag gesture may start. Since drag gesture start at the initial pass we have * the option to indicate if we consumed the event during the initial pass using * [consumedOnInitial]. We also save the [awaitTouchSlop] between passes so we don't call the * [DragGestureNode.startDragImmediately] as often. */ class AwaitDown( var awaitTouchSlop: AwaitTouchSlop = AwaitTouchSlop.NotInitialized, var consumedOnInitial: Boolean = false, ) : DragDetectionState() { enum class AwaitTouchSlop { Yes, No, NotInitialized, } } /** * If drag should wait for touch slop, after the initial down recognition we move to this state. * Here we will collect drag events until touch slop is crossed. */ class AwaitTouchSlop( var initialDown: PointerInputChange? = null, var pointerId: PointerId = PointerId(Long.MAX_VALUE), var verifyConsumptionInFinalPass: Boolean = false, ) : DragDetectionState() /** * Alternative state that implements the gesture pick up feature. If a draggable loses an event * because someone else consumed it, it can still pick it up later if the consumer "gives up" on * that gesture. Once a gesture is lost the draggable will pass on to this state until all * fingers are up. */ class AwaitGesturePickup( var initialDown: PointerInputChange? = null, var pointerId: PointerId = PointerId(Long.MAX_VALUE), var touchSlopDetector: TouchSlopDetector? = null, ) : DragDetectionState() /** State where dragging is happening. */ class Dragging(var pointerId: PointerId = PointerId(Long.MAX_VALUE)) : DragDetectionState() } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Scrollable.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation.gestures import androidx.compose.animation.core.AnimationState import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.animate import androidx.compose.animation.core.animateDecay import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ComposeFoundationFlags.isDelayPressesUsingGestureConsumptionEnabled import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollFactory import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.gestures.Orientation.Horizontal import androidx.compose.foundation.gestures.Orientation.Vertical import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.internal.PlatformOptimizedCancellationException import androidx.compose.foundation.relocation.BringIntoViewResponderNode import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.foundation.rememberPlatformOverscrollEffect import androidx.compose.foundation.scrollableArea import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.MotionDurationScale import androidx.compose.ui.focus.FocusTargetModifierNode import androidx.compose.ui.focus.Focusability import androidx.compose.ui.focus.getFocusedRect import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.KeyInputModifierNode import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.SideEffect import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput import androidx.compose.ui.input.nestedscroll.nestedScrollModifierNode import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.SemanticsModifierNode import androidx.compose.ui.node.TraversableNode import androidx.compose.ui.node.dispatchOnScrollChanged import androidx.compose.ui.node.invalidateSemantics import androidx.compose.ui.node.requireDensity import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.scrollBy import androidx.compose.ui.semantics.scrollByOffset import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.Velocity import androidx.compose.ui.util.fastAny import kotlin.math.PI import kotlin.math.abs import kotlin.math.absoluteValue import kotlin.math.atan2 import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch import kotlinx.coroutines.withContext /** * Configure touch scrolling and flinging for the UI element in a single [Orientation]. * * Users should update their state themselves using default [ScrollableState] and its * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect * their own state in UI when using this component. * * `scrollable` is a low level modifier that handles low level scrolling input gestures, without * other behaviors commonly used for scrollable containers. For building scrollable containers, see * [androidx.compose.foundation.scrollableArea]. `scrollableArea` clips its content to its bounds, * renders overscroll, and adjusts the direction of scroll gestures to ensure that the content moves * with the user's gestures. See also [androidx.compose.foundation.verticalScroll] and * [androidx.compose.foundation.horizontalScroll] for high level scrollable containers that handle * layout and move the content as the user scrolls. * * If you don't need to have fling or nested scroll support, but want to make component simply * draggable, consider using [draggable]. * * @sample androidx.compose.foundation.samples.ScrollableSample * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be * interpreted by the user land logic and contains useful information about on-going events. * @param orientation orientation of the scrolling * @param enabled whether or not scrolling in enabled * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave * like bottom to top and left to right will behave like right to left. * @param flingBehavior logic describing fling behavior when drag has finished with velocity. If * `null`, default from [ScrollableDefaults.flingBehavior] will be used. * @param interactionSource [MutableInteractionSource] that will be used to emit drag events when * this scrollable is being dragged. */ @Stable fun Modifier.scrollable( state: ScrollableState, orientation: Orientation, enabled: Boolean = true, reverseDirection: Boolean = false, flingBehavior: FlingBehavior? = null, interactionSource: MutableInteractionSource? = null, ): Modifier = scrollable( state = state, orientation = orientation, enabled = enabled, reverseDirection = reverseDirection, flingBehavior = flingBehavior, interactionSource = interactionSource, overscrollEffect = null, ) /** * Configure touch scrolling and flinging for the UI element in a single [Orientation]. * * Users should update their state themselves using default [ScrollableState] and its * `consumeScrollDelta` callback or by implementing [ScrollableState] interface manually and reflect * their own state in UI when using this component. * * `scrollable` is a low level modifier that handles low level scrolling input gestures, without * other behaviors commonly used for scrollable containers. For building scrollable containers, see * [androidx.compose.foundation.scrollableArea]. `scrollableArea` clips its content to its bounds, * renders overscroll, and adjusts the direction of scroll gestures to ensure that the content moves * with the user's gestures. See also [androidx.compose.foundation.verticalScroll] and * [androidx.compose.foundation.horizontalScroll] for high level scrollable containers that handle * layout and move the content as the user scrolls. * * If you don't need to have fling or nested scroll support, but want to make component simply * draggable, consider using [draggable]. * * This overload provides the access to [OverscrollEffect] that defines the behaviour of the over * scrolling logic. Use [androidx.compose.foundation.rememberOverscrollEffect] to create an instance * of the current provided overscroll implementation. Note: compared to other APIs that accept * [overscrollEffect] such as [scrollableArea] and [verticalScroll], `scrollable` does not render * the overscroll, it only provides events. Manually add [androidx.compose.foundation.overscroll] to * render the overscroll or use other APIs. * * @sample androidx.compose.foundation.samples.ScrollableSample * @param state [ScrollableState] state of the scrollable. Defines how scroll events will be * interpreted by the user land logic and contains useful information about on-going events. * @param orientation orientation of the scrolling * @param overscrollEffect effect to which the deltas will be fed when the scrollable have some * scrolling delta left. Pass `null` for no overscroll. If you pass an effect you should also * apply [androidx.compose.foundation.overscroll] modifier. * @param enabled whether or not scrolling in enabled * @param reverseDirection reverse the direction of the scroll, so top to bottom scroll will behave * like bottom to top and left to right will behave like right to left. * @param flingBehavior logic describing fling behavior when drag has finished with velocity. If * `null`, default from [ScrollableDefaults.flingBehavior] will be used. * @param interactionSource [MutableInteractionSource] that will be used to emit drag events when * this scrollable is being dragged. * @param bringIntoViewSpec The configuration that this scrollable should use to perform scrolling * when scroll requests are received from the focus system. If null is provided the system will * use the behavior provided by [LocalBringIntoViewSpec] which by default has a platform dependent * implementation. */ @Stable fun Modifier.scrollable( state: ScrollableState, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean = true, reverseDirection: Boolean = false, flingBehavior: FlingBehavior? = null, interactionSource: MutableInteractionSource? = null, bringIntoViewSpec: BringIntoViewSpec? = null, ) = this then ScrollableElement( state, orientation, overscrollEffect, enabled, reverseDirection, flingBehavior, interactionSource, bringIntoViewSpec, ) private class ScrollableElement( val state: ScrollableState, val orientation: Orientation, val overscrollEffect: OverscrollEffect?, val enabled: Boolean, val reverseDirection: Boolean, val flingBehavior: FlingBehavior?, val interactionSource: MutableInteractionSource?, val bringIntoViewSpec: BringIntoViewSpec?, ) : ModifierNodeElement() { override fun create(): ScrollableNode { return ScrollableNode( state, overscrollEffect, flingBehavior, orientation, enabled, reverseDirection, interactionSource, bringIntoViewSpec, ) } override fun update(node: ScrollableNode) { node.update( state, orientation, overscrollEffect, enabled, reverseDirection, flingBehavior, interactionSource, bringIntoViewSpec, ) } override fun hashCode(): Int { var result = state.hashCode() result = 31 * result + orientation.hashCode() result = 31 * result + overscrollEffect.hashCode() result = 31 * result + enabled.hashCode() result = 31 * result + reverseDirection.hashCode() result = 31 * result + flingBehavior.hashCode() result = 31 * result + interactionSource.hashCode() result = 31 * result + bringIntoViewSpec.hashCode() return result } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ScrollableElement) return false if (state != other.state) return false if (orientation != other.orientation) return false if (overscrollEffect != other.overscrollEffect) return false if (enabled != other.enabled) return false if (reverseDirection != other.reverseDirection) return false if (flingBehavior != other.flingBehavior) return false if (interactionSource != other.interactionSource) return false if (bringIntoViewSpec != other.bringIntoViewSpec) return false return true } override fun InspectorInfo.inspectableProperties() { name = "scrollable" properties["orientation"] = orientation properties["state"] = state properties["overscrollEffect"] = overscrollEffect properties["enabled"] = enabled properties["reverseDirection"] = reverseDirection properties["flingBehavior"] = flingBehavior properties["interactionSource"] = interactionSource properties["bringIntoViewSpec"] = bringIntoViewSpec } } @OptIn(ExperimentalFoundationApi::class) internal class ScrollableNode( state: ScrollableState, private var overscrollEffect: OverscrollEffect?, private var flingBehavior: FlingBehavior?, orientation: Orientation, enabled: Boolean, reverseDirection: Boolean, interactionSource: MutableInteractionSource?, bringIntoViewSpec: BringIntoViewSpec?, ) : DragGestureNode( canDrag = CanDragCalculation, enabled = enabled, interactionSource = interactionSource, orientationLock = orientation, ), KeyInputModifierNode, SemanticsModifierNode, OnScrollChangedDispatcher { override val shouldAutoInvalidate: Boolean = false private val nestedScrollDispatcher = NestedScrollDispatcher() // Place holder fling behavior, we'll initialize it when the density is available. private val defaultFlingBehavior = platformScrollableDefaultFlingBehavior() private val scrollingLogic = ScrollingLogic( scrollableState = state, orientation = orientation, overscrollEffect = overscrollEffect, reverseDirection = reverseDirection, flingBehavior = flingBehavior ?: defaultFlingBehavior, nestedScrollDispatcher = nestedScrollDispatcher, onScrollChangedDispatcher = this, isScrollableNodeAttached = { isAttached }, ) private val nestedScrollConnection = ScrollableNestedScrollConnection(enabled = enabled, scrollingLogic = scrollingLogic) private val focusTargetModifierNode = delegate(FocusTargetModifierNode(focusability = Focusability.Never)) private val contentInViewNode = delegate( ContentInViewNode( orientation = orientation, scrollingLogic = scrollingLogic, reverseDirection = reverseDirection, bringIntoViewSpec = bringIntoViewSpec, getFocusedRect = { focusTargetModifierNode.getFocusedRect() }, ) ) private var scrollByAction: ((x: Float, y: Float) -> Boolean)? = null private var scrollByOffsetAction: (suspend (Offset) -> Offset)? = null private var mouseWheelScrollingLogic: MouseWheelScrollingLogic? = null private var trackpadScrollingLogic: TrackpadScrollingLogic? = null private var scrollableContainerNode: ScrollableContainerNode? = null init { /** Nested scrolling */ delegate(nestedScrollModifierNode(nestedScrollConnection, nestedScrollDispatcher)) /** Focus scrolling */ delegate(BringIntoViewResponderNode(contentInViewNode)) if (!isDelayPressesUsingGestureConsumptionEnabled) { scrollableContainerNode = delegate(ScrollableContainerNode(enabled)) } } override fun dispatchScrollDeltaInfo(delta: Offset) { if (!isAttached) return dispatchOnScrollChanged(delta) } override suspend fun drag( forEachDelta: suspend ((dragDelta: DragEvent.DragDelta) -> Unit) -> Unit ) { with(scrollingLogic) { scroll(scrollPriority = MutatePriority.UserInput) { forEachDelta { // Indirect pointer Events should be reverted to account for the reverse we // do in Scrollable. Regular touchscreen events are inverted in scrollable, but // that shouldn't happen for indirect pointer events, so we cancel the reverse // here. val invertIndirectPointer = if (it.isIndirectPointerEvent) -1f else 1f scrollByWithOverscroll( it.delta.singleAxisOffset() * invertIndirectPointer, source = UserInput, ) } } } } override fun onDragStarted(startedPosition: Offset) {} override fun onDragStopped(event: DragEvent.DragStopped) { nestedScrollDispatcher.coroutineScope.launch { // Indirect pointer Events should be reverted to account for the reverse we // do in Scrollable. Regular touchscreen events are inverted in scrollable, but // that shouldn't happen for indirect pointer events, so we cancel the reverse // here. val invertIndirectPointer = if (event.isIndirectPointerEvent) -1f else 1f scrollingLogic.onScrollStopped( event.velocity * invertIndirectPointer, isMouseWheel = false, ) } } private fun onWheelScrollStopped(velocity: Velocity) { nestedScrollDispatcher.coroutineScope.launch { scrollingLogic.onScrollStopped(velocity, isMouseWheel = true) } } private fun onTrackpadScrollStopped(velocity: Velocity) { nestedScrollDispatcher.coroutineScope.launch { scrollingLogic.onScrollStopped(velocity, isMouseWheel = false) } } override fun startDragImmediately(): Boolean { return scrollingLogic.shouldScrollImmediately() } private fun ensureMouseWheelScrollingLogicInitialized() { if (mouseWheelScrollingLogic == null) { mouseWheelScrollingLogic = MouseWheelScrollingLogic( scrollingLogic = scrollingLogic, mouseWheelScrollConfig = platformScrollConfig(), onScrollStopped = ::onWheelScrollStopped, density = requireDensity(), ) } mouseWheelScrollingLogic?.startReceivingEvents(coroutineScope) } private fun ensureTrackpadScrollingLogicInitialized() { if (trackpadScrollingLogic == null) { trackpadScrollingLogic = TrackpadScrollingLogic( scrollingLogic = scrollingLogic, onScrollStopped = ::onTrackpadScrollStopped, density = requireDensity(), ) } trackpadScrollingLogic?.startReceivingEvents(coroutineScope) } fun update( state: ScrollableState, orientation: Orientation, overscrollEffect: OverscrollEffect?, enabled: Boolean, reverseDirection: Boolean, flingBehavior: FlingBehavior?, interactionSource: MutableInteractionSource?, bringIntoViewSpec: BringIntoViewSpec?, ) { var shouldInvalidateSemantics = false if (this.enabled != enabled) { // enabled changed nestedScrollConnection.enabled = enabled scrollableContainerNode?.update(enabled) shouldInvalidateSemantics = true } // a new fling behavior was set, change the resolved one. val resolvedFlingBehavior = flingBehavior ?: defaultFlingBehavior val resetPointerInputHandling = scrollingLogic.update( scrollableState = state, orientation = orientation, overscrollEffect = overscrollEffect, reverseDirection = reverseDirection, flingBehavior = resolvedFlingBehavior, nestedScrollDispatcher = nestedScrollDispatcher, ) contentInViewNode.update(orientation, reverseDirection, bringIntoViewSpec) this.overscrollEffect = overscrollEffect this.flingBehavior = flingBehavior // update DragGestureNode update( canDrag = CanDragCalculation, enabled = enabled, interactionSource = interactionSource, orientationLock = if (scrollingLogic.isVertical()) Vertical else Horizontal, shouldResetPointerInputHandling = resetPointerInputHandling, ) if (shouldInvalidateSemantics) { clearScrollSemanticsActions() invalidateSemantics() } } override fun onAttach() { updateDefaultFlingBehavior() mouseWheelScrollingLogic?.updateDensity(requireDensity()) trackpadScrollingLogic?.updateDensity(requireDensity()) } private fun updateDefaultFlingBehavior() { if (!isAttached) return val density = requireDensity() defaultFlingBehavior.updateDensity(density) } override fun onDensityChange() { onCancelPointerInput() updateDefaultFlingBehavior() mouseWheelScrollingLogic?.updateDensity(requireDensity()) trackpadScrollingLogic?.updateDensity(requireDensity()) } // Key handler for Page up/down scrolling behavior. override fun onKeyEvent(event: KeyEvent): Boolean { return if ( enabled && (event.key == Key.PageDown || event.key == Key.PageUp) && (event.type == KeyEventType.KeyDown) && (!event.isCtrlPressed) ) { val scrollAmount: Offset = if (scrollingLogic.isVertical()) { val viewportHeight = contentInViewNode.viewportSizeOrZero.height val yAmount = if (event.key == Key.PageUp) { viewportHeight.toFloat() } else { -viewportHeight.toFloat() } Offset(0f, yAmount) } else { val viewportWidth = contentInViewNode.viewportSizeOrZero.width val xAmount = if (event.key == Key.PageUp) { viewportWidth.toFloat() } else { -viewportWidth.toFloat() } Offset(xAmount, 0f) } // A coroutine is launched for every individual scroll event in the // larger scroll gesture. If we see degradation in the future (that is, // a fast scroll gesture on a slow device causes UI jank [not seen up to // this point), we can switch to a more efficient solution where we // lazily launch one coroutine (with the first event) and use a Channel // to communicate the scroll amount to the UI thread. coroutineScope.launch { scrollingLogic.scroll(scrollPriority = MutatePriority.UserInput) { scrollBy(offset = scrollAmount, source = UserInput) } } true } else { false } } override fun onPreKeyEvent(event: KeyEvent) = false // Forward all PointerInputModifierNode method calls to `mmouseWheelScrollNode.pointerInputNode` // See explanation in `MouseWheelScrollNode.pointerInputNode` override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { if (pointerEvent.changes.fastAny { canDrag.invoke(it.type) }) { super.onPointerEvent(pointerEvent, pass, bounds) } initializeGestureCoordination() if (enabled) { if (pass == PointerEventPass.Initial && pointerEvent.type == PointerEventType.Scroll) { ensureMouseWheelScrollingLogicInitialized() } mouseWheelScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) if ( pass == PointerEventPass.Initial && (pointerEvent.type == PointerEventType.PanStart || pointerEvent.type == PointerEventType.PanMove || pointerEvent.type == PointerEventType.PanEnd) ) { ensureTrackpadScrollingLogicInitialized() } trackpadScrollingLogic?.onPointerEvent(pointerEvent, pass, bounds) } } override fun SemanticsPropertyReceiver.applySemantics() { if (enabled && (scrollByAction == null || scrollByOffsetAction == null)) { setScrollSemanticsActions() } scrollByAction?.let { scrollBy(action = it) } scrollByOffsetAction?.let { scrollByOffset(action = it) } } private fun setScrollSemanticsActions() { scrollByAction = { x, y -> coroutineScope.launch { scrollingLogic.semanticsScrollBy(Offset(x, y)) } true } scrollByOffsetAction = { offset -> scrollingLogic.semanticsScrollBy(offset) } } private fun clearScrollSemanticsActions() { scrollByAction = null scrollByOffsetAction = null } } /** Contains the default values used by [scrollable] */ object ScrollableDefaults { /** Create and remember default [FlingBehavior] that will represent natural fling curve. */ @Composable fun flingBehavior(): FlingBehavior = rememberPlatformDefaultFlingBehavior() /** * Returns a remembered [OverscrollEffect] created from the current value of * [LocalOverscrollFactory]. * * This API has been deprecated, and replaced with [rememberOverscrollEffect] */ @Deprecated( "This API has been replaced with rememberOverscrollEffect, which queries theme provided OverscrollFactory values instead of the 'platform default' without customization.", replaceWith = ReplaceWith( "rememberOverscrollEffect()", "androidx.compose.foundation.rememberOverscrollEffect", ), ) @Composable fun overscrollEffect(): OverscrollEffect { return rememberPlatformOverscrollEffect() ?: NoOpOverscrollEffect } private object NoOpOverscrollEffect : OverscrollEffect { override fun applyToScroll( delta: Offset, source: NestedScrollSource, performScroll: (Offset) -> Offset, ): Offset = performScroll(delta) override suspend fun applyToFling( velocity: Velocity, performFling: suspend (Velocity) -> Velocity, ) { performFling(velocity) } override val isInProgress: Boolean get() = false override val node: DelegatableNode get() = object : Modifier.Node() {} } /** * Calculates the final `reverseDirection` value for a scrollable component. * * This is a helper function used by [androidx.compose.foundation.scrollableArea] to determine * whether to reverse the direction of scroll input. The goal is to provide a "natural" * scrolling experience where content moves with the user's gesture, while also accounting for * the [layoutDirection]. * * The logic is as follows: * 1. To achieve "natural" scrolling (content moves with the gesture), scroll deltas are * inverted. This function returns `true` by default when `reverseScrolling` is `false`. * 2. In a Right-to-Left (`Rtl`) context with a `Horizontal` orientation, the direction is * flipped an additional time to maintain the natural feel, as the content is laid out from * right to left. * * @param layoutDirection current layout direction (e.g. from [LocalLayoutDirection]) * @param orientation orientation of scroll * @param reverseScrolling whether scrolling direction should be reversed * @return `true` if scroll direction should be reversed, `false` otherwise. */ fun reverseDirection( layoutDirection: LayoutDirection, orientation: Orientation, reverseScrolling: Boolean, ): Boolean { // A finger moves with the content, not with the viewport. Therefore, // always reverse once to have "natural" gesture that goes reversed to layout var reverseDirection = !reverseScrolling // But if rtl and horizontal, things move the other way around val isRtl = layoutDirection == LayoutDirection.Rtl if (isRtl && orientation != Orientation.Vertical) { reverseDirection = !reverseDirection } return reverseDirection } } internal interface ScrollConfig { /** Enables animated transition of scroll on mouse wheel events. */ val isSmoothScrollingEnabled: Boolean get() = true fun isPreciseWheelScroll(event: PointerEvent): Boolean = false fun Density.calculateMouseWheelScroll(event: PointerEvent, bounds: IntSize): Offset } internal expect fun CompositionLocalConsumerModifierNode.platformScrollConfig(): ScrollConfig // TODO: provide public way to drag by mouse (especially requested for Pager) internal val CanDragCalculation: (PointerType) -> Boolean = { type -> type != PointerType.Mouse } /** * Holds all scrolling related logic: controls nested scrolling, flinging, overscroll and delta * dispatching. */ internal class ScrollingLogic( var scrollableState: ScrollableState, private var overscrollEffect: OverscrollEffect?, private var flingBehavior: FlingBehavior, private var orientation: Orientation, private var reverseDirection: Boolean, private var nestedScrollDispatcher: NestedScrollDispatcher, private var onScrollChangedDispatcher: OnScrollChangedDispatcher, private val isScrollableNodeAttached: () -> Boolean, ) : ScrollLogic { // specifies if this scrollable node is currently flinging override var isFlinging = false private set fun Float.toOffset(): Offset = when { this == 0f -> Offset.Zero orientation == Horizontal -> Offset(this, 0f) else -> Offset(0f, this) } fun Offset.singleAxisOffset(): Offset = if (orientation == Horizontal) copy(y = 0f) else copy(x = 0f) fun Offset.toFloat(): Float = if (orientation == Horizontal) this.x else this.y /** * Converts this offset to a single axis delta based on the derived angle from the x and y * deltas. * * @return Returns a single axis delta based on the angle. If the angle is mostly horizontal, * and we are in a horizontal scrollable, this will return the x component. If the angle is * mostly vertical, and we are in a vertical scrollable, this will return the y component. * Otherwise, this will return 0. Mostly horizontal means angles smaller than * [VerticalAxisThresholdAngle]. */ fun Offset.toSingleAxisDeltaFromAngle(): Float { val angle = atan2(this.y.absoluteValue, this.x.absoluteValue) return if (angle >= VerticalAxisThresholdAngle) { if (orientation == Vertical) this.y else 0f } else { if (orientation == Horizontal) this.x else 0f } } fun Float.toVelocity(): Velocity = when { this == 0f -> Velocity.Zero orientation == Horizontal -> Velocity(this, 0f) else -> Velocity(0f, this) } private fun Velocity.toFloat(): Float = if (orientation == Horizontal) this.x else this.y private fun Velocity.singleAxisVelocity(): Velocity = if (orientation == Horizontal) copy(y = 0f) else copy(x = 0f) private fun Velocity.update(newValue: Float): Velocity = if (orientation == Horizontal) copy(x = newValue) else copy(y = newValue) fun Float.reverseIfNeeded(): Float = if (reverseDirection) this * -1 else this fun Offset.reverseIfNeeded(): Offset = if (reverseDirection) this * -1f else this private var latestScrollSource = UserInput private var outerStateScope = NoOpScrollScope private val nestedScrollScope = object : NestedScrollScope { override fun scrollBy(offset: Offset, source: NestedScrollSource): Offset { return with(outerStateScope) { performScroll(offset, source) } } override fun scrollByWithOverscroll( offset: Offset, source: NestedScrollSource, ): Offset { latestScrollSource = source val overscroll = overscrollEffect return if (overscroll != null && shouldDispatchOverscroll) { overscroll.applyToScroll(offset, latestScrollSource, performScrollForOverscroll) } else { with(outerStateScope) { performScroll(offset, source) } } } } private val performScrollForOverscroll: (Offset) -> Offset = { delta -> with(outerStateScope) { performScroll(delta, latestScrollSource) } } @OptIn(ExperimentalFoundationApi::class) private fun ScrollScope.performScroll(delta: Offset, source: NestedScrollSource): Offset { val consumedByPreScroll = nestedScrollDispatcher.dispatchPreScroll(delta, source) val scrollAvailableAfterPreScroll = delta - consumedByPreScroll val singleAxisDeltaForSelfScroll = scrollAvailableAfterPreScroll.singleAxisOffset().reverseIfNeeded().toFloat() // Consume on a single axis. val consumedBySelfScroll = scrollBy(singleAxisDeltaForSelfScroll).toOffset().reverseIfNeeded() // Trigger on scroll changed callback onScrollChangedDispatcher.dispatchScrollDeltaInfo(consumedBySelfScroll) val deltaAvailableAfterScroll = scrollAvailableAfterPreScroll - consumedBySelfScroll val consumedByPostScroll = nestedScrollDispatcher.dispatchPostScroll( consumedBySelfScroll, deltaAvailableAfterScroll, source, ) return consumedByPreScroll + consumedBySelfScroll + consumedByPostScroll } private val shouldDispatchOverscroll get() = scrollableState.canScrollForward || scrollableState.canScrollBackward override fun performRawScroll(scroll: Offset): Offset { return if (scrollableState.isScrollInProgress) { Offset.Zero } else { dispatchRawDelta(scroll) } } private fun dispatchRawDelta(scroll: Offset): Offset { return scrollableState .dispatchRawDelta(scroll.toFloat().reverseIfNeeded()) .reverseIfNeeded() .toOffset() } suspend fun onScrollStopped(initialVelocity: Velocity, isMouseWheel: Boolean) { if (isMouseWheel && !flingBehavior.shouldBeTriggeredByMouseWheel) { return } val availableVelocity = initialVelocity.singleAxisVelocity() val performFling: suspend (Velocity) -> Velocity = { velocity -> val preConsumedByParent = nestedScrollDispatcher.dispatchPreFling(velocity) val available = velocity - preConsumedByParent val velocityLeft = doFlingAnimation(available) val consumedPost = nestedScrollDispatcher.dispatchPostFling((available - velocityLeft), velocityLeft) val totalLeft = velocityLeft - consumedPost velocity - totalLeft } val overscroll = overscrollEffect if (overscroll != null && shouldDispatchOverscroll) { overscroll.applyToFling(availableVelocity, performFling) } else { performFling(availableVelocity) } } @OptIn(ExperimentalFoundationApi::class) override suspend fun doFlingAnimation(available: Velocity): Velocity { var result: Velocity = available isFlinging = true try { scroll(scrollPriority = MutatePriority.Default) { val nestedScrollScope = this val reverseScope = object : ScrollScope { override fun scrollBy(pixels: Float): Float { // Fling has hit the bounds or node left composition, // cancel it to allow continuation. This will conclude this node's // fling, // allowing the onPostFling signal to be called // with the leftover velocity from the fling animation. Any nested // scroll // node above will be able to pick up the left over velocity and // continue // the fling. if ( pixels.absoluteValue != 0.0f && !isScrollableNodeAttached.invoke() ) { throw FlingCancellationException() } return nestedScrollScope .scrollByWithOverscroll( offset = pixels.toOffset().reverseIfNeeded(), source = SideEffect, ) .toFloat() .reverseIfNeeded() } } with(reverseScope) { with(flingBehavior) { result = result.update( performFling(available.toFloat().reverseIfNeeded()) .reverseIfNeeded() ) } } } } finally { isFlinging = false } return result } fun shouldScrollImmediately(): Boolean { return scrollableState.isScrollInProgress || overscrollEffect?.isInProgress ?: false } /** Opens a scrolling session with nested scrolling and overscroll support. */ suspend fun scroll( scrollPriority: MutatePriority = MutatePriority.Default, block: suspend NestedScrollScope.() -> Unit, ) { scrollableState.scroll(scrollPriority) { outerStateScope = this block.invoke(nestedScrollScope) } } /** @return true if the pointer input should be reset */ fun update( scrollableState: ScrollableState, orientation: Orientation, overscrollEffect: OverscrollEffect?, reverseDirection: Boolean, flingBehavior: FlingBehavior, nestedScrollDispatcher: NestedScrollDispatcher, ): Boolean { var resetPointerInputHandling = false if (this.scrollableState != scrollableState) { this.scrollableState = scrollableState resetPointerInputHandling = true } this.overscrollEffect = overscrollEffect if (this.orientation != orientation) { this.orientation = orientation resetPointerInputHandling = true } if (this.reverseDirection != reverseDirection) { this.reverseDirection = reverseDirection resetPointerInputHandling = true } this.flingBehavior = flingBehavior this.nestedScrollDispatcher = nestedScrollDispatcher return resetPointerInputHandling } fun isVertical(): Boolean = orientation == Vertical } private val NoOpScrollScope: ScrollScope = object : ScrollScope { override fun scrollBy(pixels: Float): Float = pixels } internal class ScrollableNestedScrollConnection( val scrollingLogic: ScrollLogic, var enabled: Boolean, ) : NestedScrollConnection { override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource, ): Offset = if (enabled) { scrollingLogic.performRawScroll(available) } else { Offset.Zero } @OptIn(ExperimentalFoundationApi::class) override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { return if (enabled) { val velocityLeft = if (scrollingLogic.isFlinging) { Velocity.Zero } else { scrollingLogic.doFlingAnimation(available) } available - velocityLeft } else { Velocity.Zero } } } /** Interface to allow re-use across Scrollable and Scrollable2D. */ internal interface ScrollLogic { val isFlinging: Boolean fun performRawScroll(scroll: Offset): Offset suspend fun doFlingAnimation(available: Velocity): Velocity } /** Compatibility interface for default fling behaviors that depends on [Density]. */ internal interface ScrollableDefaultFlingBehavior : FlingBehavior { /** * Update the internal parameters of FlingBehavior in accordance with the new * [androidx.compose.ui.unit.Density] value. * * @param density new density value. */ fun updateDensity(density: Density) = Unit } /** * TODO: Move it to public interface Currently, default [FlingBehavior] is not triggered at all to * avoid unexpected effects during regular scrolling. However, custom one must be triggered * because it's used not only for "inertia", but also for snapping in * [androidx.compose.foundation.pager.Pager] or * [androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior]. */ private val FlingBehavior.shouldBeTriggeredByMouseWheel get() = this !is ScrollableDefaultFlingBehavior /** * This method returns [ScrollableDefaultFlingBehavior] whose density will be managed by the * [ScrollableElement] because it's not created inside [Composable] context. This is different from * [rememberPlatformDefaultFlingBehavior] which creates [FlingBehavior] whose density depends on * [LocalDensity] and is automatically resolved. */ internal expect fun platformScrollableDefaultFlingBehavior(): ScrollableDefaultFlingBehavior /** * Create and remember default [FlingBehavior] that will represent natural platform fling decay * behavior. */ @Composable internal expect fun rememberPlatformDefaultFlingBehavior(): FlingBehavior internal class DefaultFlingBehavior( private var flingDecay: DecayAnimationSpec, private val motionDurationScale: MotionDurationScale = DefaultScrollMotionDurationScale, ) : ScrollableDefaultFlingBehavior { // For Testing var lastAnimationCycleCount = 0 override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { lastAnimationCycleCount = 0 // come up with the better threshold, but we need it since spline curve gives us NaNs return withContext(motionDurationScale) { if (abs(initialVelocity) > 1f) { var velocityLeft = initialVelocity var lastValue = 0f val animationState = AnimationState(initialValue = 0f, initialVelocity = initialVelocity) try { animationState.animateDecay(flingDecay) { val delta = value - lastValue val consumed = scrollBy(delta) lastValue = value velocityLeft = this.velocity // avoid rounding errors and stop if anything is unconsumed if (abs(delta - consumed) > 0.5f) this.cancelAnimation() lastAnimationCycleCount++ } } catch (exception: CancellationException) { velocityLeft = animationState.velocity } velocityLeft } else { initialVelocity } } } override fun updateDensity(density: Density) { flingDecay = splineBasedDecay(density) } } private const val DefaultScrollMotionDurationScaleFactor = 1f internal val DefaultScrollMotionDurationScale = object : MotionDurationScale { override val scaleFactor: Float get() = DefaultScrollMotionDurationScaleFactor } /** * (b/311181532): This could not be flattened so we moved it to TraversableNode, but ideally * ScrollabeNode should be the one to be travesable. */ internal class ScrollableContainerNode(enabled: Boolean) : Modifier.Node(), TraversableNode { override val traverseKey: Any = TraverseKey var enabled: Boolean = enabled private set companion object TraverseKey fun update(enabled: Boolean) { this.enabled = enabled } } internal val UnityDensity = object : Density { override val density: Float get() = 1f override val fontScale: Float get() = 1f } /** A scroll scope for nested scrolling and overscroll support. */ internal interface NestedScrollScope { fun scrollBy(offset: Offset, source: NestedScrollSource): Offset fun scrollByWithOverscroll(offset: Offset, source: NestedScrollSource): Offset } /** * Scroll deltas originating from the semantics system. Should be dispatched as an animation driven * event. */ private suspend fun ScrollingLogic.semanticsScrollBy(offset: Offset): Offset { var previousValue = 0f scroll(scrollPriority = MutatePriority.Default) { animate(0f, offset.toFloat()) { currentValue, _ -> val delta = currentValue - previousValue val consumed = scrollBy(offset = delta.reverseIfNeeded().toOffset(), source = UserInput) .toFloat() .reverseIfNeeded() previousValue += consumed } } return previousValue.toOffset() } internal class FlingCancellationException : PlatformOptimizedCancellationException("The fling animation was cancelled") internal interface OnScrollChangedDispatcher { fun dispatchScrollDeltaInfo(delta: Offset) } private const val VerticalAxisThresholdAngle = PI / 4 ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/gestures/Transformable.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation.gestures import androidx.compose.foundation.ComposeFoundationFlags import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.gestures.TransformEvent.TransformDelta import androidx.compose.foundation.gestures.TransformEvent.TransformStarted import androidx.compose.foundation.gestures.TransformEvent.TransformStopped import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerEventType import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.input.pointer.SuspendingPointerInputModifierNode import androidx.compose.ui.input.pointer.isCtrlPressed import androidx.compose.ui.input.pointer.positionChanged import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.PointerInputModifierNode import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.IntSize import androidx.compose.ui.util.fastAny import androidx.compose.ui.util.fastFold import androidx.compose.ui.util.fastForEach import kotlin.math.PI import kotlin.math.abs import kotlin.math.pow import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.isActive import kotlinx.coroutines.launch /** * Enable transformation gestures of the modified UI element. * * Users should update their state themselves using default [TransformableState] and its * `onTransformation` callback or by implementing [TransformableState] interface manually and * reflect their own state in UI when using this component. * * @sample androidx.compose.foundation.samples.TransformableSample * @param state [TransformableState] of the transformable. Defines how transformation events will be * interpreted by the user land logic, contains useful information about on-going events and * provides animation capabilities. * @param lockRotationOnZoomPan If `true`, rotation is allowed only if touch slop is detected for * rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but * rotation gestures will not be. If `false`, once touch slop is reached, all three gestures are * detected. * @param enabled whether zooming by gestures is enabled or not */ fun Modifier.transformable( state: TransformableState, lockRotationOnZoomPan: Boolean = false, enabled: Boolean = true, ) = transformable(state, { true }, lockRotationOnZoomPan, enabled) /** * Enable transformation gestures of the modified UI element. * * Users should update their state themselves using default [TransformableState] and its * `onTransformation` callback or by implementing [TransformableState] interface manually and * reflect their own state in UI when using this component. * * This overload of transformable modifier provides [canPan] parameter, which allows the caller to * control when the pan can start. making pan gesture to not to start when the scale is 1f makes * transformable modifiers to work well within the scrollable container. See example: * * @sample androidx.compose.foundation.samples.TransformableSampleInsideScroll * @param state [TransformableState] of the transformable. Defines how transformation events will be * interpreted by the user land logic, contains useful information about on-going events and * provides animation capabilities. * @param canPan whether the pan gesture can be performed or not given the pan offset * @param lockRotationOnZoomPan If `true`, rotation is allowed only if touch slop is detected for * rotation before pan or zoom motions. If not, pan and zoom gestures will be detected, but * rotation gestures will not be. If `false`, once touch slop is reached, all three gestures are * detected. * @param enabled whether zooming by gestures is enabled or not */ fun Modifier.transformable( state: TransformableState, canPan: (Offset) -> Boolean, lockRotationOnZoomPan: Boolean = false, enabled: Boolean = true, ) = this then TransformableElement(state, canPan, lockRotationOnZoomPan, enabled) private class TransformableElement( private val state: TransformableState, private val canPan: (Offset) -> Boolean, private val lockRotationOnZoomPan: Boolean, private val enabled: Boolean, ) : ModifierNodeElement() { override fun create(): TransformableNode = TransformableNode(state, canPan, lockRotationOnZoomPan, enabled) override fun update(node: TransformableNode) { node.update(state, canPan, lockRotationOnZoomPan, enabled) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other === null) return false if (this::class != other::class) return false other as TransformableElement if (state != other.state) return false if (canPan !== other.canPan) return false if (lockRotationOnZoomPan != other.lockRotationOnZoomPan) return false if (enabled != other.enabled) return false return true } override fun hashCode(): Int { var result = state.hashCode() result = 31 * result + canPan.hashCode() result = 31 * result + lockRotationOnZoomPan.hashCode() result = 31 * result + enabled.hashCode() return result } override fun InspectorInfo.inspectableProperties() { name = "transformable" properties["state"] = state properties["canPan"] = canPan properties["enabled"] = enabled properties["lockRotationOnZoomPan"] = lockRotationOnZoomPan } } private class TransformableNode( private var state: TransformableState, private var canPan: (Offset) -> Boolean, private var lockRotationOnZoomPan: Boolean, private var enabled: Boolean, ) : DelegatingNode(), PointerInputModifierNode, CompositionLocalConsumerModifierNode { private val updatedCanPan: (Offset) -> Boolean = { canPan.invoke(it) } private val channel = Channel(capacity = Channel.UNLIMITED) private var scrollConfig: ScrollConfig? = null override fun onAttach() { super.onAttach() scrollConfig = platformScrollConfig() } private val pointerInputNode = delegate( SuspendingPointerInputModifierNode { if (!enabled) return@SuspendingPointerInputModifierNode coroutineScope { launch(start = CoroutineStart.UNDISPATCHED) { while (isActive) { var event = channel.receive() if (event !is TransformStarted) continue try { state.transform(MutatePriority.UserInput) { while (event !is TransformStopped) { (event as? TransformDelta)?.let { transformByWithCentroid( centroid = it.centroid, zoomChange = it.zoomChange, panChange = it.panChange, rotationChange = it.rotationChange, ) } event = channel.receive() } } } catch (_: CancellationException) { // ignore the cancellation and start over again. } } } awaitEachGesture { try { detectZoom(lockRotationOnZoomPan, channel, updatedCanPan) } catch (exception: CancellationException) { if (!isActive) throw exception } finally { channel.trySend(TransformStopped) } } } } ) private var pointerInputModifierMouse: PointerInputModifierNode? = null fun update( state: TransformableState, canPan: (Offset) -> Boolean, lockRotationOnZoomPan: Boolean, enabled: Boolean, ) { this.canPan = canPan val needsReset = this.state != state || this.enabled != enabled || this.lockRotationOnZoomPan != lockRotationOnZoomPan if (needsReset) { this.state = state this.enabled = enabled this.lockRotationOnZoomPan = lockRotationOnZoomPan pointerInputNode.resetPointerInputHandler() } } override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { val scrollConfig = scrollConfig if ( enabled && pointerEvent.changes.fastAny { it.type == PointerType.Mouse } && scrollConfig != null && pointerInputModifierMouse == null ) { pointerInputModifierMouse = delegate( SuspendingPointerInputModifierNode { detectNonTouchGestures(channel, scrollConfig) } ) } pointerInputNode.onPointerEvent(pointerEvent, pass, bounds) pointerInputModifierMouse?.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { pointerInputNode.onCancelPointerInput() pointerInputModifierMouse?.onCancelPointerInput() } } // The factor used to covert the mouse scroll to zoom. // Every 545 pixels of scroll is converted into 2 times zoom. This value is calculated from // curve fitting the ChromeOS's zoom factors. internal const val SCROLL_FACTOR = 545f /** * Convert non touch events into the appropriate transform events. There are 3 cases, where order of * determination matters: * - If Ctrl is pressed, and we get a scroll, either from a mouse wheel or a trackpad pan, we * convert that scroll into an equivalent zoom * - If we get a trackpad pan, we convert that into a pan * - If we get a trackpad scale, we convert that into a zoom */ private suspend fun PointerInputScope.detectNonTouchGestures( channel: Channel, scrollConfig: ScrollConfig, ) { val currentContext = currentCoroutineContext() awaitPointerEventScope { while (currentContext.isActive) { try { var zoomOffset: Offset? var panOffset: Offset? var scale: Float? var pointer: PointerEvent do { pointer = awaitPointerEvent() // Convert non touch events into the appropriate transform events. // There are 3 cases, where order of determination matters: // - If Ctrl is pressed, and we get a scroll, either from a mouse wheel or a // trackpad pan, we convert that scroll into an equivalent zoom // - If we get a trackpad pan, we convert that into a pan // - If we get a trackpad scale, we convert that into a zoom zoomOffset = consumePointerEventAsCtrlScrollOrNull(pointer, scrollConfig) panOffset = consumePointerEventAsPanOrNull(pointer) scale = consumePointerEventAsScaleOrNull(pointer) } while (zoomOffset == null && panOffset == null && scale == null) if (zoomOffset != null) { var scrollDelta: Offset = zoomOffset channel.trySend(TransformStarted) while (true) { // This formula is curve fitting form Chrome OS's ctrl + scroll // implementation. val zoomChange = 2f.pow(scrollDelta.y / SCROLL_FACTOR) channel.trySend( TransformDelta( centroid = pointer.calculateCentroid { true }, zoomChange = zoomChange, panChange = Offset.Zero, rotationChange = 0f, ) ) pointer = awaitPointerEvent() scrollDelta = consumePointerEventAsCtrlScrollOrNull(pointer, scrollConfig) ?: break } } else if (panOffset != null) { var panDelta: Offset = panOffset channel.trySend(TransformStarted) while (true) { channel.trySend( TransformDelta( centroid = pointer.calculateCentroid { true }, zoomChange = 1f, panChange = panDelta, rotationChange = 0f, ) ) pointer = awaitPointerEvent() panDelta = consumePointerEventAsPanOrNull(pointer) ?: break } } else { var scaleDelta: Float = checkNotNull(scale) { "One of zoomOffset, panOffset and scaleDelta must be non-null" } channel.trySend(TransformStarted) while (true) { channel.trySend( TransformDelta( centroid = pointer.calculateCentroid { true }, zoomChange = scaleDelta, panChange = Offset.Zero, rotationChange = 0f, ) ) pointer = awaitPointerEvent() scaleDelta = consumePointerEventAsScaleOrNull(pointer) ?: break } } } finally { channel.trySend(TransformStopped) } } } } /** * If the PointerEvent is a mouse scroll event that has non zero scrollDelta and the ctrl key is * pressed, its scrollDelta is returned. Otherwise, null is returned. The event is consumed when it * detects ctrl + mouse scroll. */ private fun AwaitPointerEventScope.consumePointerEventAsCtrlScrollOrNull( pointer: PointerEvent, scrollConfig: ScrollConfig, ): Offset? { if ( !pointer.keyboardModifiers.isCtrlPressed || (pointer.type != PointerEventType.Scroll && pointer.type != PointerEventType.PanStart && pointer.type != PointerEventType.PanMove && pointer.type != PointerEventType.PanEnd) ) { return null } @OptIn(ExperimentalFoundationApi::class) val scrollDelta = with(scrollConfig) { calculateMouseWheelScroll(pointer, size) } + if (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled) { (pointer.changes.firstOrNull()?.let { -it.panOffset + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> acc - historicalChange.panOffset } } ?: Offset.Zero) } else { Offset.Zero } if (scrollDelta == Offset.Zero) { return null } pointer.changes.fastForEach { it.consume() } return scrollDelta } private fun AwaitPointerEventScope.consumePointerEventAsPanOrNull(pointer: PointerEvent): Offset? { @OptIn(ExperimentalFoundationApi::class) if ( !ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || (pointer.type != PointerEventType.PanStart && pointer.type != PointerEventType.PanMove && pointer.type != PointerEventType.PanEnd) ) { return null } val scrollDelta = pointer.changes.firstOrNull()?.let { -it.panOffset + it.historical.fastFold(Offset.Zero) { acc, historicalChange -> acc - historicalChange.panOffset } } ?: Offset.Zero if (scrollDelta == Offset.Zero) { return null } pointer.changes.fastForEach { it.consume() } return scrollDelta } private fun AwaitPointerEventScope.consumePointerEventAsScaleOrNull(pointer: PointerEvent): Float? { @OptIn(ExperimentalFoundationApi::class) if ( !ComposeFoundationFlags.isTrackpadGestureHandlingEnabled || (pointer.type != PointerEventType.ScaleStart && pointer.type != PointerEventType.ScaleChange && pointer.type != PointerEventType.ScaleEnd) ) { return null } var scaleDelta = 1f pointer.changes.fastForEach { scaleDelta *= it.scaleFactor it.historical.fastForEach { scaleDelta *= it.scaleFactor } } if (scaleDelta == 1f) { return null } pointer.changes.fastForEach { it.consume() } return scaleDelta } private suspend fun AwaitPointerEventScope.detectZoom( panZoomLock: Boolean, channel: Channel, canPan: (Offset) -> Boolean, ) { var rotation = 0f var zoom = 1f var pan = Offset.Zero var pastTouchSlop = false val touchSlop = viewConfiguration.touchSlop var lockedToPanZoom = false awaitFirstDown(requireUnconsumed = false) do { val event = awaitPointerEvent() @OptIn(ExperimentalFoundationApi::class) val canceled = event.changes.fastAny { it.isConsumed } || (ComposeFoundationFlags.isTrackpadGestureHandlingEnabled && (event.type == PointerEventType.PanStart || event.type == PointerEventType.PanMove || event.type == PointerEventType.PanEnd || event.type == PointerEventType.ScaleStart || event.type == PointerEventType.ScaleChange || event.type == PointerEventType.ScaleEnd)) if (!canceled) { val zoomChange = event.calculateZoom() val rotationChange = event.calculateRotation() val panChange = event.calculatePan() if (!pastTouchSlop) { zoom *= zoomChange rotation += rotationChange pan += panChange val centroidSize = event.calculateCentroidSize(useCurrent = false) val zoomMotion = abs(1 - zoom) * centroidSize val rotationMotion = abs(rotation * PI.toFloat() * centroidSize / 180f) val panMotion = pan.getDistance() if ( zoomMotion > touchSlop || rotationMotion > touchSlop || (panMotion > touchSlop && canPan.invoke(panChange)) ) { pastTouchSlop = true lockedToPanZoom = panZoomLock && rotationMotion < touchSlop channel.trySend(TransformStarted) } } if (pastTouchSlop) { val centroid = event.calculateCentroid(useCurrent = false) val effectiveRotation = if (lockedToPanZoom) 0f else rotationChange if ( effectiveRotation != 0f || zoomChange != 1f || (panChange != Offset.Zero && canPan.invoke(panChange)) ) { channel.trySend( TransformDelta(centroid, zoomChange, panChange, effectiveRotation) ) } event.changes.fastForEach { if (it.positionChanged()) { it.consume() } } } } else { channel.trySend(TransformStopped) } val finalEvent = awaitPointerEvent(pass = PointerEventPass.Final) // someone consumed while we were waiting for touch slop val finallyCanceled = finalEvent.changes.fastAny { it.isConsumed } && !pastTouchSlop } while (!canceled && !finallyCanceled && event.changes.fastAny { it.pressed }) } private sealed class TransformEvent { object TransformStarted : TransformEvent() object TransformStopped : TransformEvent() class TransformDelta( val centroid: Offset, val zoomChange: Float, val panChange: Offset, val rotationChange: Float, ) : TransformEvent() } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Background.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.foundation import androidx.annotation.FloatRange import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.SemanticsModifierNode import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.node.invalidateSemantics import androidx.compose.ui.node.observeReads import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.shape import androidx.compose.ui.unit.LayoutDirection /** * Draws [shape] with a solid [color] behind the content. * * @sample androidx.compose.foundation.samples.DrawBackgroundColor * @param color color to paint background with * @param shape desired shape of the background */ @Stable fun Modifier.background(color: Color, shape: Shape = RectangleShape): Modifier { val alpha = 1.0f // for solid colors return this.then( BackgroundElement( color = color, shape = shape, alpha = alpha, inspectorInfo = debugInspectorInfo { name = "background" value = color properties["color"] = color properties["shape"] = shape }, ) ) } /** * Draws [shape] with [brush] behind the content. * * @sample androidx.compose.foundation.samples.DrawBackgroundShapedBrush * @param brush brush to paint background with * @param shape desired shape of the background * @param alpha Opacity to be applied to the [brush], with `0` being completely transparent and `1` * being completely opaque. The value must be between `0` and `1`. */ @Stable fun Modifier.background( brush: Brush, shape: Shape = RectangleShape, @FloatRange(from = 0.0, to = 1.0) alpha: Float = 1.0f, ) = this.then( BackgroundElement( brush = brush, alpha = alpha, shape = shape, inspectorInfo = debugInspectorInfo { name = "background" properties["alpha"] = alpha properties["brush"] = brush properties["shape"] = shape }, ) ) private class BackgroundElement( private val color: Color = Color.Unspecified, private val brush: Brush? = null, private val alpha: Float, private val shape: Shape, private val inspectorInfo: InspectorInfo.() -> Unit, ) : ModifierNodeElement() { override fun create(): BackgroundNode { return BackgroundNode(color, brush, alpha, shape) } override fun update(node: BackgroundNode) { node.color = color node.brush = brush node.alpha = alpha if (node.shape != shape) { node.shape = shape node.invalidateSemantics() } node.invalidateDraw() } override fun InspectorInfo.inspectableProperties() { inspectorInfo() } override fun hashCode(): Int { var result = color.hashCode() result = 31 * result + (brush?.hashCode() ?: 0) result = 31 * result + alpha.hashCode() result = 31 * result + shape.hashCode() return result } override fun equals(other: Any?): Boolean { val otherModifier = other as? BackgroundElement ?: return false return color == otherModifier.color && brush == otherModifier.brush && alpha == otherModifier.alpha && shape == otherModifier.shape } } private class BackgroundNode( var color: Color, var brush: Brush?, var alpha: Float, var shape: Shape, ) : DrawModifierNode, Modifier.Node(), ObserverModifierNode, SemanticsModifierNode { override val shouldAutoInvalidate = false override val isImportantForBounds = false // Naively cache outline calculation if input parameters are the same, we manually observe // reads inside shape#createOutline separately private var lastSize: Size = Size.Unspecified private var lastLayoutDirection: LayoutDirection? = null private var lastOutline: Outline? = null private var lastShape: Shape? = null private var tmpOutline: Outline? = null override fun ContentDrawScope.draw() { if (shape === RectangleShape) { // shortcut to avoid Outline calculation and allocation drawRect() } else { drawOutline() } drawContent() } override fun onObservedReadsChanged() { // Reset cached properties lastSize = Size.Unspecified lastLayoutDirection = null lastOutline = null lastShape = null // Invalidate draw so we build the cache again - this is needed because observeReads within // the draw scope obscures the state reads from the draw scope's observer invalidateDraw() } private fun ContentDrawScope.drawRect() { if (color != Color.Unspecified) drawRect(color = color) brush?.let { drawRect(brush = it, alpha = alpha) } } private fun ContentDrawScope.drawOutline() { val outline = getOutline() if (color != Color.Unspecified) drawOutline(outline, color = color) brush?.let { drawOutline(outline, brush = it, alpha = alpha) } } private fun ContentDrawScope.getOutline(): Outline { val outline: Outline? if (size == lastSize && layoutDirection == lastLayoutDirection && lastShape == shape) { outline = lastOutline!! } else { // Manually observe reads so we can directly invalidate the outline when it changes // Use tmpOutline to avoid creating an object reference to local var outline observeReads { tmpOutline = shape.createOutline(size, layoutDirection, this) } outline = tmpOutline tmpOutline = null } lastOutline = outline lastSize = size lastLayoutDirection = layoutDirection lastShape = shape return outline!! } override fun SemanticsPropertyReceiver.applySemantics() { this.shape = this@BackgroundNode.shape } } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/Border.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.CacheDrawModifierNode import androidx.compose.ui.draw.CacheDrawScope import androidx.compose.ui.draw.DrawResult import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.isSimple import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmapConfig import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathOperation import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.drawscope.CanvasDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.drawscope.scale import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.SemanticsModifierNode import androidx.compose.ui.node.invalidateSemantics import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.semantics.SemanticsPropertyReceiver import androidx.compose.ui.semantics.shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.toSize import kotlin.math.ceil import kotlin.math.max import kotlin.math.min /** * Modify element to add border with appearance specified with a [border] and a [shape] and clip it. * * @sample androidx.compose.foundation.samples.BorderSample * @param border [BorderStroke] class that specifies border appearance, such as size and color * @param shape shape of the border */ @Stable fun Modifier.border(border: BorderStroke, shape: Shape = RectangleShape) = border(width = border.width, brush = border.brush, shape = shape) /** * Modify element to add border with appearance specified with a [width], a [color] and a [shape] * and clip it. * * @sample androidx.compose.foundation.samples.BorderSampleWithDataClass * @param width width of the border. Use [Dp.Hairline] for a hairline border. * @param color color to paint the border with * @param shape shape of the border */ @Stable fun Modifier.border(width: Dp, color: Color, shape: Shape = RectangleShape) = border(width, SolidColor(color), shape) /** * Modify element to add border with appearance specified with a [width], a [brush] and a [shape] * and clip it. * * @sample androidx.compose.foundation.samples.BorderSampleWithBrush * @sample androidx.compose.foundation.samples.BorderSampleWithDynamicData * @param width width of the border. Use [Dp.Hairline] for a hairline border. * @param brush brush to paint the border with * @param shape shape of the border */ @Stable fun Modifier.border(width: Dp, brush: Brush, shape: Shape) = this then BorderModifierNodeElement(width, brush, shape) internal data class BorderModifierNodeElement(val width: Dp, val brush: Brush, val shape: Shape) : ModifierNodeElement() { override fun create() = BorderModifierNode(width, brush, shape) override fun update(node: BorderModifierNode) { node.width = width node.brush = brush node.shape = shape } override fun InspectorInfo.inspectableProperties() { name = "border" properties["width"] = width if (brush is SolidColor) { properties["color"] = brush.value value = brush.value } else { properties["brush"] = brush } properties["shape"] = shape } } internal class BorderModifierNode( widthParameter: Dp, brushParameter: Brush, shapeParameter: Shape, ) : DelegatingNode(), SemanticsModifierNode { override val shouldAutoInvalidate: Boolean = false override val isImportantForBounds = false // BorderCache object that is lazily allocated depending on the type of shape // This object is only used for generic shapes and rounded rectangles with different corner // radius sizes. // Note: Extension functions that use BorderCache are part of this class. private var borderCache: BorderCache? = null var width = widthParameter set(value) { if (field != value) { field = value drawWithCacheModifierNode.invalidateDrawCache() } } var brush = brushParameter set(value) { if (field != value) { field = value drawWithCacheModifierNode.invalidateDrawCache() } } var shape = shapeParameter set(value) { if (field != value) { field = value drawWithCacheModifierNode.invalidateDrawCache() invalidateSemantics() } } private val drawWithCacheModifierNode = delegate( CacheDrawModifierNode { val hasValidBorderParams = width.toPx() >= 0f && size.minDimension > 0f if (!hasValidBorderParams) { drawContentWithoutBorder() } else { val strokeWidthPx = min( if (width == Dp.Hairline) 1f else ceil(width.toPx()), ceil(size.minDimension / 2), ) val halfStroke = strokeWidthPx / 2 val topLeft = Offset(halfStroke, halfStroke) val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) // The stroke is larger than the drawing area so just draw a full shape instead val fillArea = (strokeWidthPx * 2) > size.minDimension when (val outline = shape.createOutline(size, layoutDirection, this)) { is Outline.Generic -> drawGenericBorder(brush, outline, fillArea, strokeWidthPx) is Outline.Rounded -> drawRoundRectBorder( brush, outline, topLeft, borderSize, fillArea, strokeWidthPx, ) is Outline.Rectangle -> drawRectBorder(brush, topLeft, borderSize, fillArea, strokeWidthPx) } } } ) /** * Border implementation for generic paths. Note it is possible to be given paths that do not * make sense in the context of a border (ex. a figure 8 path or a non-enclosed shape) We do not * handle that here as we expect developers to give us enclosed, non-overlapping paths. */ private fun CacheDrawScope.drawGenericBorder( brush: Brush, outline: Outline.Generic, fillArea: Boolean, strokeWidth: Float, ): DrawResult = if (fillArea) { onDrawWithContent { drawContent() drawPath(outline.path, brush = brush) } } else { // Optimization, if we are only drawing a solid color border, we only need an alpha8 // mask as we can draw the mask with a tint. // Otherwise we need to allocate a full ImageBitmap and draw it normally val config: ImageBitmapConfig val colorFilter: ColorFilter? if (brush is SolidColor) { config = ImageBitmapConfig.Alpha8 // The brush is drawn into the mask with the corresponding color including the // alpha channel so when we tint we should not apply the alpha as it would end up // modulating it twice colorFilter = ColorFilter.tint(brush.value.copy(alpha = 1f)) } else { config = ImageBitmapConfig.Argb8888 colorFilter = null } val pathBounds = outline.path.getBounds() // Create a mask path that includes a rectangle with the original path cut out of it. // Note: borderCache is part of the class that defines this extension function. if (borderCache == null) { borderCache = BorderCache() } val maskPath = borderCache!!.obtainPath().apply { reset() addRect(pathBounds) op(this, outline.path, PathOperation.Difference) } val cacheImageBitmap: ImageBitmap val pathBoundsSize = IntSize(ceil(pathBounds.width).toInt(), ceil(pathBounds.height).toInt()) with(borderCache!!) { // Draw into offscreen bitmap with the size of the path // We need to draw into this intermediate bitmap to act as a layer // and make sure that the clearing logic does not generate underdraw // into the target we are rendering into cacheImageBitmap = drawBorderCache(pathBoundsSize, config) { // Paths can have offsets, so translate to keep the drawn path // within the bounds of the mask bitmap translate(-pathBounds.left, -pathBounds.top) { // Draw the path with a stroke width twice the provided value. // Because strokes are centered, this will draw both and inner and // outer stroke with the desired stroke width drawPath( path = outline.path, brush = brush, style = Stroke(strokeWidth * 2), ) // Scale the canvas slightly to cover the background that may be visible // after clearing the outer stroke scale((size.width + 1) / size.width, (size.height + 1) / size.height) { // Remove the outer stroke by clearing the inverted mask path drawPath( path = maskPath, brush = brush, blendMode = BlendMode.Clear, ) } } } } onDrawWithContent { drawContent() translate(pathBounds.left, pathBounds.top) { drawImage(cacheImageBitmap, srcSize = pathBoundsSize, colorFilter = colorFilter) } } } /** Border implementation for simple rounded rects and those with different corner radii */ private fun CacheDrawScope.drawRoundRectBorder( brush: Brush, outline: Outline.Rounded, topLeft: Offset, borderSize: Size, fillArea: Boolean, strokeWidth: Float, ): DrawResult { return if (outline.roundRect.isSimple) { val cornerRadius = outline.roundRect.topLeftCornerRadius val halfStroke = strokeWidth / 2 val borderStroke = Stroke(strokeWidth) onDrawWithContent { drawContent() when { fillArea -> { // If the drawing area is smaller than the stroke being drawn // drawn all around it just draw a filled in rounded rect drawRoundRect(brush, cornerRadius = cornerRadius) } cornerRadius.x < halfStroke -> { // If the corner radius is smaller than half of the stroke width // then the interior curvature of the stroke will be a sharp edge // In this case just draw a normal filled in rounded rect with the // desired corner radius but clipping out the interior rectangle clipRect( strokeWidth, strokeWidth, size.width - strokeWidth, size.height - strokeWidth, clipOp = ClipOp.Difference, ) { drawRoundRect(brush, cornerRadius = cornerRadius) } } else -> { // Otherwise draw a stroked rounded rect with the corner radius // shrunk by half of the stroke width. This will ensure that the // outer curvature of the rounded rectangle will have the desired // corner radius. drawRoundRect( brush = brush, topLeft = topLeft, size = borderSize, cornerRadius = cornerRadius.shrink(halfStroke), style = borderStroke, ) } } } } else { // Note: borderCache is part of the class that defines this extension function. if (borderCache == null) { borderCache = BorderCache() } val path = borderCache!!.obtainPath() val roundedRectPath = createRoundRectPath(path, outline.roundRect, strokeWidth, fillArea) onDrawWithContent { drawContent() drawPath(roundedRectPath, brush = brush) } } } override fun SemanticsPropertyReceiver.applySemantics() { shape = this@BorderModifierNode.shape } } /** * Helper object that handles lazily allocating and re-using objects to render the border into an * offscreen ImageBitmap */ private data class BorderCache( private var imageBitmap: ImageBitmap? = null, private var canvas: androidx.compose.ui.graphics.Canvas? = null, private var canvasDrawScope: CanvasDrawScope? = null, private var borderPath: Path? = null, ) { inline fun CacheDrawScope.drawBorderCache( borderSize: IntSize, config: ImageBitmapConfig, block: DrawScope.() -> Unit, ): ImageBitmap { var targetImageBitmap = imageBitmap var targetCanvas = canvas // If we previously had allocated a full Argb888 ImageBitmap but are only requiring // an alpha mask, just re-use the same ImageBitmap instead of allocating a new one val compatibleConfig = targetImageBitmap?.config == ImageBitmapConfig.Argb8888 || config == targetImageBitmap?.config if ( targetImageBitmap == null || targetCanvas == null || size.width > targetImageBitmap.width || size.height > targetImageBitmap.height || !compatibleConfig ) { targetImageBitmap = ImageBitmap(borderSize.width, borderSize.height, config = config).also { imageBitmap = it } targetCanvas = androidx.compose.ui.graphics.Canvas(targetImageBitmap).also { canvas = it } } val targetDrawScope = canvasDrawScope ?: CanvasDrawScope().also { canvasDrawScope = it } val drawSize = borderSize.toSize() targetDrawScope.draw(this, layoutDirection, targetCanvas, drawSize) { // Clear the previously rendered portion within this ImageBitmap as we could // be re-using it drawRect(color = Color.Black, size = drawSize, blendMode = BlendMode.Clear) block() } targetImageBitmap.prepareToDraw() return targetImageBitmap } fun obtainPath(): Path = borderPath ?: Path().also { borderPath = it } } /** * Border implementation for invalid parameters that just draws the content as the given border * parameters are infeasible (ex. negative border width) */ private fun CacheDrawScope.drawContentWithoutBorder(): DrawResult = onDrawWithContent { drawContent() } /** Border implementation for rectangular borders */ private fun CacheDrawScope.drawRectBorder( brush: Brush, topLeft: Offset, borderSize: Size, fillArea: Boolean, strokeWidthPx: Float, ): DrawResult { // If we are drawing a rectangular stroke, just offset it by half the stroke // width as strokes are always drawn centered on their geometry. // If the border is larger than the drawing area, just fill the area with a // solid rectangle val rectTopLeft = if (fillArea) Offset.Zero else topLeft val size = if (fillArea) size else borderSize val style = if (fillArea) Fill else Stroke(strokeWidthPx) return onDrawWithContent { drawContent() drawRect(brush = brush, topLeft = rectTopLeft, size = size, style = style) } } /** * Helper method that creates a round rect with the inner region removed by the given stroke width */ private fun createRoundRectPath( targetPath: Path, roundedRect: RoundRect, strokeWidth: Float, fillArea: Boolean, ): Path = targetPath.apply { reset() addRoundRect(roundedRect) if (!fillArea) { val insetPath = Path().apply { addRoundRect(createInsetRoundedRect(strokeWidth, roundedRect)) } op(this, insetPath, PathOperation.Difference) } } private fun createInsetRoundedRect(widthPx: Float, roundedRect: RoundRect) = RoundRect( left = widthPx, top = widthPx, right = roundedRect.width - widthPx, bottom = roundedRect.height - widthPx, topLeftCornerRadius = roundedRect.topLeftCornerRadius.shrink(widthPx), topRightCornerRadius = roundedRect.topRightCornerRadius.shrink(widthPx), bottomLeftCornerRadius = roundedRect.bottomLeftCornerRadius.shrink(widthPx), bottomRightCornerRadius = roundedRect.bottomRightCornerRadius.shrink(widthPx), ) /** * Helper method to shrink the corner radius by the given value, clamping to 0 if the resultant * corner radius would be negative */ private fun CornerRadius.shrink(value: Float): CornerRadius = CornerRadius(max(0f, this.x - value), max(0f, this.y - value)) ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/shape/RoundedCornerShape.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.foundation.shape import androidx.annotation.IntRange import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.RoundRect import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.toRect import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection.Ltr import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp /** * A shape describing the rectangle with rounded corners. * * This shape will automatically mirror the corner sizes in [LayoutDirection.Rtl], use * [AbsoluteRoundedCornerShape] for the layout direction unaware version of this shape. * * @param topStart a size of the top start corner * @param topEnd a size of the top end corner * @param bottomEnd a size of the bottom end corner * @param bottomStart a size of the bottom start corner */ class RoundedCornerShape( topStart: CornerSize, topEnd: CornerSize, bottomEnd: CornerSize, bottomStart: CornerSize, ) : CornerBasedShape( topStart = topStart, topEnd = topEnd, bottomEnd = bottomEnd, bottomStart = bottomStart, ) { override fun createOutline( size: Size, topStart: Float, topEnd: Float, bottomEnd: Float, bottomStart: Float, layoutDirection: LayoutDirection, ): Outline { return if (topStart + topEnd + bottomEnd + bottomStart == 0.0f) { Outline.Rectangle(size.toRect()) } else { Outline.Rounded( RoundRect( rect = size.toRect(), topLeft = CornerRadius(if (layoutDirection == Ltr) topStart else topEnd), topRight = CornerRadius(if (layoutDirection == Ltr) topEnd else topStart), bottomRight = CornerRadius(if (layoutDirection == Ltr) bottomEnd else bottomStart), bottomLeft = CornerRadius(if (layoutDirection == Ltr) bottomStart else bottomEnd), ) ) } } override fun copy( topStart: CornerSize, topEnd: CornerSize, bottomEnd: CornerSize, bottomStart: CornerSize, ) = RoundedCornerShape( topStart = topStart, topEnd = topEnd, bottomEnd = bottomEnd, bottomStart = bottomStart, ) override fun toString(): String { return "RoundedCornerShape(topStart = $topStart, topEnd = $topEnd, bottomEnd = " + "$bottomEnd, bottomStart = $bottomStart)" } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is RoundedCornerShape) return false if (topStart != other.topStart) return false if (topEnd != other.topEnd) return false if (bottomEnd != other.bottomEnd) return false if (bottomStart != other.bottomStart) return false return true } override fun hashCode(): Int { var result = topStart.hashCode() result = 31 * result + topEnd.hashCode() result = 31 * result + bottomEnd.hashCode() result = 31 * result + bottomStart.hashCode() return result } override fun lerp(other: Any?, t: Float): Any? { var other: Any? = other if (other == RectangleShape || other == null) { other = RoundedCornerShape(0f) } if (other is RoundedCornerShape) { return lerp(this, other, t) } return null } } internal fun lerp(a: RoundedCornerShape, b: RoundedCornerShape, t: Float): RoundedCornerShape { return RoundedCornerShape( topStart = lerp(a.topStart, b.topStart, t), topEnd = lerp(a.topEnd, b.topEnd, t), bottomEnd = lerp(a.bottomEnd, b.bottomEnd, t), bottomStart = lerp(a.bottomStart, b.bottomStart, t), ) } internal fun lerp(a: CornerSize, b: CornerSize, t: Float): CornerSize { return object : CornerSize { override fun toPx(shapeSize: Size, density: Density): Float { return lerp(a.toPx(shapeSize, density), b.toPx(shapeSize, density), t) } } } /** Circular [Shape] with all the corners sized as the 50 percent of the shape size. */ val CircleShape = RoundedCornerShape(50) /** * Creates [RoundedCornerShape] with the same size applied for all four corners. * * @param corner [CornerSize] to apply. */ fun RoundedCornerShape(corner: CornerSize) = RoundedCornerShape(corner, corner, corner, corner) /** * Creates [RoundedCornerShape] with the same size applied for all four corners. * * @param size Size in [Dp] to apply. */ fun RoundedCornerShape(size: Dp) = RoundedCornerShape(CornerSize(size)) /** * Creates [RoundedCornerShape] with the same size applied for all four corners. * * @param size Size in pixels to apply. */ fun RoundedCornerShape(size: Float) = RoundedCornerShape(CornerSize(size)) /** * Creates [RoundedCornerShape] with the same size applied for all four corners. * * @param percent Size in percents to apply. */ fun RoundedCornerShape(percent: Int) = RoundedCornerShape(CornerSize(percent)) /** Creates [RoundedCornerShape] with sizes defined in [Dp]. */ fun RoundedCornerShape( topStart: Dp = 0.dp, topEnd: Dp = 0.dp, bottomEnd: Dp = 0.dp, bottomStart: Dp = 0.dp, ) = RoundedCornerShape( topStart = CornerSize(topStart), topEnd = CornerSize(topEnd), bottomEnd = CornerSize(bottomEnd), bottomStart = CornerSize(bottomStart), ) /** Creates [RoundedCornerShape] with sizes defined in pixels. */ fun RoundedCornerShape( topStart: Float = 0.0f, topEnd: Float = 0.0f, bottomEnd: Float = 0.0f, bottomStart: Float = 0.0f, ) = RoundedCornerShape( topStart = CornerSize(topStart), topEnd = CornerSize(topEnd), bottomEnd = CornerSize(bottomEnd), bottomStart = CornerSize(bottomStart), ) /** * Creates [RoundedCornerShape] with sizes defined in percents of the shape's smaller side. * * @param topStartPercent The top start corner radius as a percentage of the smaller side, with a * range of 0 - 100. * @param topEndPercent The top end corner radius as a percentage of the smaller side, with a range * of 0 - 100. * @param bottomEndPercent The bottom end corner radius as a percentage of the smaller side, with a * range of 0 - 100. * @param bottomStartPercent The bottom start corner radius as a percentage of the smaller side, * with a range of 0 - 100. */ fun RoundedCornerShape( @IntRange(from = 0, to = 100) topStartPercent: Int = 0, @IntRange(from = 0, to = 100) topEndPercent: Int = 0, @IntRange(from = 0, to = 100) bottomEndPercent: Int = 0, @IntRange(from = 0, to = 100) bottomStartPercent: Int = 0, ) = RoundedCornerShape( topStart = CornerSize(topStartPercent), topEnd = CornerSize(topEndPercent), bottomEnd = CornerSize(bottomEndPercent), bottomStart = CornerSize(bottomStartPercent), ) ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/Pager.kt ```kotlin /* * Copyright 2023 The Android Open Source Project * * 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 androidx.compose.foundation.pager import androidx.annotation.FloatRange import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.DecayAnimationSpec import androidx.compose.animation.core.Spring import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.spring import androidx.compose.animation.rememberSplineBasedDecay import androidx.compose.foundation.OverscrollEffect import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.TargetedFlingBehavior import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.snapping.calculateFinalSnappingBound import androidx.compose.foundation.gestures.snapping.snapFlingBehavior import androidx.compose.foundation.internal.requirePrecondition import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.rememberOverscrollEffect import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.pageDown import androidx.compose.ui.semantics.pageLeft import androidx.compose.ui.semantics.pageRight import androidx.compose.ui.semantics.pageUp import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import kotlin.math.abs import kotlin.math.absoluteValue import kotlin.math.roundToInt import kotlin.math.sign import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** * A Pager that scrolls horizontally. Pages are lazily placed in accordance to the available * viewport size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and * use a snap animation (provided by [flingBehavior] to scroll pages into a specific position). You * can use [beyondViewportPageCount] to place more pages before and after the visible pages. * * If you need snapping with pages of different size, you can use a [snapFlingBehavior] with a * [SnapLayoutInfoProvider] adapted to a LazyList. * * @param state The state to control this pager * @param modifier A modifier instance to be applied to this Pager outer layout * @param contentPadding a padding around the whole content. This will add padding for the content * after it has been clipped, which is not possible via [modifier] param. You can use it to add a * padding before the first page or after the last one. Use [pageSpacing] to add spacing between * the pages. * @param pageSize Use this to change how the pages will look like inside this pager. * @param beyondViewportPageCount Pages to compose and layout before and after the list of visible * pages. Note: Be aware that using a large value for [beyondViewportPageCount] will cause a lot * of pages to be composed, measured and placed which will defeat the purpose of using lazy * loading. This should be used as an optimization to pre-load a couple of pages before and after * the visible ones. This does not include the pages automatically composed and laid out by the * pre-fetcher in the direction of the scroll during scroll events. * @param pageSpacing The amount of space to be used to separate the pages in this Pager * @param verticalAlignment How pages are aligned vertically in this Pager. * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures. * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is * allowed. You can still scroll programmatically using [PagerState.scroll] even when it is * disabled. * @param reverseLayout reverse the direction of scrolling and layout. * @param key a stable and unique key representing the item. When you specify the key the scroll * position will be maintained based on the key, which means if you add/remove items before the * current visible item the item with the given key will be kept as the first visible one. If null * is passed the position in the list will represent the key. * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager] * behaves with nested lists. The default behavior will see [Pager] to consume all nested deltas. * @param snapPosition The calculation of how this Pager will perform snapping of pages. Use this to * provide different settling to different positions in the layout. This is used by [Pager] as a * way to calculate [PagerState.currentPage], currentPage is the page closest to the snap position * in the layout (e.g. if the snap position is the start of the layout, then currentPage will be * the page closest to that). * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this * Pager. Note that the [OverscrollEffect.node] will be applied internally as well - you do not * need to use Modifier.overscroll separately. * @param pageContent This Pager's page Composable. * @sample androidx.compose.foundation.samples.SimpleHorizontalPagerSample * @sample androidx.compose.foundation.samples.HorizontalPagerWithScrollableContent * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation * of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState]. * * Please refer to the samples to learn how to use this API. */ @Composable fun HorizontalPager( state: PagerState, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), pageSize: PageSize = PageSize.Fill, beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, pageSpacing: Dp = 0.dp, verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state), userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, key: ((index: Int) -> Any)? = null, pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(state, Orientation.Horizontal), snapPosition: SnapPosition = SnapPosition.Start, overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), pageContent: @Composable PagerScope.(page: Int) -> Unit, ) { Pager( state = state, modifier = modifier, contentPadding = contentPadding, pageSize = pageSize, beyondViewportPageCount = beyondViewportPageCount, pageSpacing = pageSpacing, orientation = Orientation.Horizontal, verticalAlignment = verticalAlignment, horizontalAlignment = Alignment.CenterHorizontally, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, reverseLayout = reverseLayout, key = key, pageNestedScrollConnection = pageNestedScrollConnection, snapPosition = snapPosition, overscrollEffect = overscrollEffect, pageContent = pageContent, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun HorizontalPager( state: PagerState, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), pageSize: PageSize = PageSize.Fill, beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, pageSpacing: Dp = 0.dp, verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state), userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, key: ((index: Int) -> Any)? = null, pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(state, Orientation.Horizontal), snapPosition: SnapPosition = SnapPosition.Start, pageContent: @Composable PagerScope.(page: Int) -> Unit, ) { HorizontalPager( state = state, modifier = modifier, contentPadding = contentPadding, pageSize = pageSize, beyondViewportPageCount = beyondViewportPageCount, pageSpacing = pageSpacing, verticalAlignment = verticalAlignment, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, reverseLayout = reverseLayout, key = key, pageNestedScrollConnection = pageNestedScrollConnection, snapPosition = snapPosition, overscrollEffect = rememberOverscrollEffect(), pageContent = pageContent, ) } /** * A Pager that scrolls vertically. Pages are lazily placed in accordance to the available viewport * size. By definition, pages in a [Pager] have the same size, defined by [pageSize] and use a snap * animation (provided by [flingBehavior] to scroll pages into a specific position). You can use * [beyondViewportPageCount] to place more pages before and after the visible pages. * * If you need snapping with pages of different size, you can use a [snapFlingBehavior] with a * [SnapLayoutInfoProvider] adapted to a LazyList. * * @param state The state to control this pager * @param modifier A modifier instance to be apply to this Pager outer layout * @param contentPadding a padding around the whole content. This will add padding for the content * after it has been clipped, which is not possible via [modifier] param. You can use it to add a * padding before the first page or after the last one. Use [pageSpacing] to add spacing between * the pages. * @param pageSize Use this to change how the pages will look like inside this pager. * @param beyondViewportPageCount Pages to compose and layout before and after the list of visible * pages. Note: Be aware that using a large value for [beyondViewportPageCount] will cause a lot * of pages to be composed, measured and placed which will defeat the purpose of using lazy * loading. This should be used as an optimization to pre-load a couple of pages before and after * the visible ones. This does not include the pages automatically composed and laid out by the * pre-fetcher in * * the direction of the scroll during scroll events. * * @param pageSpacing The amount of space to be used to separate the pages in this Pager * @param horizontalAlignment How pages are aligned horizontally in this Pager. * @param flingBehavior The [TargetedFlingBehavior] to be used for post scroll gestures. * @param userScrollEnabled whether the scrolling via the user gestures or accessibility actions is * allowed. You can still scroll programmatically using [PagerState.scroll] even when it is * disabled. * @param reverseLayout reverse the direction of scrolling and layout. * @param key a stable and unique key representing the item. When you specify the key the scroll * position will be maintained based on the key, which means if you add/remove items before the * current visible item the item with the given key will be kept as the first visible one. If null * is passed the position in the list will represent the key. * @param pageNestedScrollConnection A [NestedScrollConnection] that dictates how this [Pager] * behaves with nested lists. The default behavior will see [Pager] to consume all nested deltas. * @param snapPosition The calculation of how this Pager will perform snapping of Pages. Use this to * provide different settling to different positions in the layout. This is used by [Pager] as a * way to calculate [PagerState.currentPage], currentPage is the page closest to the snap position * in the layout (e.g. if the snap position is the start of the layout, then currentPage will be * the page closest to that). * @param overscrollEffect the [OverscrollEffect] that will be used to render overscroll for this * Pager. Note that the [OverscrollEffect.node] will be applied internally as well - you do not * need to use Modifier.overscroll separately. * @param pageContent This Pager's page Composable. * @sample androidx.compose.foundation.samples.SimpleVerticalPagerSample * @see androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider for the implementation * of a [SnapLayoutInfoProvider] that uses [androidx.compose.foundation.lazy.LazyListState]. * * Please refer to the sample to learn how to use this API. */ @Composable fun VerticalPager( state: PagerState, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), pageSize: PageSize = PageSize.Fill, beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, pageSpacing: Dp = 0.dp, horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state), userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, key: ((index: Int) -> Any)? = null, pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(state, Orientation.Vertical), snapPosition: SnapPosition = SnapPosition.Start, overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(), pageContent: @Composable PagerScope.(page: Int) -> Unit, ) { Pager( state = state, modifier = modifier, contentPadding = contentPadding, pageSize = pageSize, beyondViewportPageCount = beyondViewportPageCount, pageSpacing = pageSpacing, orientation = Orientation.Vertical, verticalAlignment = Alignment.CenterVertically, horizontalAlignment = horizontalAlignment, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, reverseLayout = reverseLayout, key = key, pageNestedScrollConnection = pageNestedScrollConnection, snapPosition = snapPosition, overscrollEffect = overscrollEffect, pageContent = pageContent, ) } @Deprecated("Use the non deprecated overload", level = DeprecationLevel.HIDDEN) @Composable fun VerticalPager( state: PagerState, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), pageSize: PageSize = PageSize.Fill, beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, pageSpacing: Dp = 0.dp, horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state), userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, key: ((index: Int) -> Any)? = null, pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection(state, Orientation.Vertical), snapPosition: SnapPosition = SnapPosition.Start, pageContent: @Composable PagerScope.(page: Int) -> Unit, ) { VerticalPager( state = state, modifier = modifier, contentPadding = contentPadding, pageSize = pageSize, beyondViewportPageCount = beyondViewportPageCount, pageSpacing = pageSpacing, horizontalAlignment = horizontalAlignment, flingBehavior = flingBehavior, userScrollEnabled = userScrollEnabled, reverseLayout = reverseLayout, key = key, pageNestedScrollConnection = pageNestedScrollConnection, snapPosition = snapPosition, overscrollEffect = rememberOverscrollEffect(), pageContent = pageContent, ) } /** Contains the default values used by [Pager]. */ object PagerDefaults { /** * A [snapFlingBehavior] that will snap pages to the start of the layout. One can use the given * parameters to control how the snapping animation will happen. * * @param state The [PagerState] that controls the which to which this FlingBehavior will be * applied to. * @param pagerSnapDistance A way to control the snapping destination for this [Pager]. The * default behavior will result in any fling going to the next page in the direction of the * fling (if the fling has enough velocity, otherwise the Pager will bounce back). Use * [PagerSnapDistance.atMost] to define a maximum number of pages this [Pager] is allowed to * fling after scrolling is finished and fling has started. * @param decayAnimationSpec The animation spec used to approach the target offset. When the * fling velocity is large enough. Large enough means large enough to naturally decay. For * single page snapping this usually never happens since there won't be enough space to run a * decay animation. * @param snapAnimationSpec The animation spec used to finally snap to the position. This * animation will be often used in 2 cases: 1) There was enough space to an approach * animation, the Pager will use [snapAnimationSpec] in the last step of the animation to * settle the page into position. 2) There was not enough space to run the approach animation. * @param snapPositionalThreshold If the fling has a low velocity (e.g. slow scroll), this fling * behavior will use this snap threshold in order to determine if the pager should snap back * or move forward. Use a number between 0 and 1 as a fraction of the page size that needs to * be scrolled before the Pager considers it should move to the next page. For instance, if * snapPositionalThreshold = 0.35, it means if this pager is scrolled with a slow velocity and * the Pager scrolls more than 35% of the page size, then will jump to the next page, if not * it scrolls back. Note that any fling that has high enough velocity will *always* move to * the next page in the direction of the fling. * @return An instance of [FlingBehavior] that will perform Snapping to the next page by * default. The animation will be governed by the post scroll velocity and the Pager will use * either [snapAnimationSpec] or [decayAnimationSpec] to approach the snapped position If a * velocity is not high enough the pager will use [snapAnimationSpec] to reach the snapped * position. If the velocity is high enough, the Pager will use the logic described in * [decayAnimationSpec] and [snapAnimationSpec]. * @see androidx.compose.foundation.gestures.snapping.snapFlingBehavior for more information on * what which parameter controls in the overall snapping animation. * * The animation specs used by the fling behavior will depend on 2 factors: * 1) The gesture velocity. * 2) The target page proposed by [pagerSnapDistance]. * * If you're using single page snapping (the most common use case for [Pager]), there won't be * enough space to actually run a decay animation to approach the target page, so the Pager will * always use the snapping animation from [snapAnimationSpec]. If you're using multi-page * snapping (this means you're abs(targetPage - currentPage) > 1) the Pager may use * [decayAnimationSpec] or [snapAnimationSpec] to approach the targetPage, it will depend on the * velocity generated by the triggering gesture. If the gesture has a high enough velocity to * approach the target page, the Pager will use [decayAnimationSpec] followed by * [snapAnimationSpec] for the final step of the animation. If the gesture doesn't have enough * velocity, the Pager will use [snapAnimationSpec] + [snapAnimationSpec] in a similar fashion. */ @Composable fun flingBehavior( state: PagerState, pagerSnapDistance: PagerSnapDistance = PagerSnapDistance.atMost(1), decayAnimationSpec: DecayAnimationSpec = rememberSplineBasedDecay(), snapAnimationSpec: AnimationSpec = spring( stiffness = Spring.StiffnessMediumLow, visibilityThreshold = Int.VisibilityThreshold.toFloat(), ), @FloatRange(from = 0.0, to = 1.0) snapPositionalThreshold: Float = 0.5f, ): TargetedFlingBehavior { requirePrecondition(snapPositionalThreshold in 0f..1f) { "snapPositionalThreshold should be a number between 0 and 1. " + "You've specified $snapPositionalThreshold" } val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current return remember( state, decayAnimationSpec, snapAnimationSpec, pagerSnapDistance, density, layoutDirection, ) { val snapLayoutInfoProvider = SnapLayoutInfoProvider(state, pagerSnapDistance) { flingVelocity, lowerBound, upperBound -> calculateFinalSnappingBound( pagerState = state, layoutDirection = layoutDirection, snapPositionalThreshold = snapPositionalThreshold, flingVelocity = flingVelocity, lowerBoundOffset = lowerBound, upperBoundOffset = upperBound, ) } snapFlingBehavior( snapLayoutInfoProvider = snapLayoutInfoProvider, decayAnimationSpec = decayAnimationSpec, snapAnimationSpec = snapAnimationSpec, ) } } /** * The default implementation of Pager's pageNestedScrollConnection. * * @param state state of the pager * @param orientation The orientation of the pager. This will be used to determine which * direction the nested scroll connection will operate and react on. */ @Composable fun pageNestedScrollConnection( state: PagerState, orientation: Orientation, ): NestedScrollConnection { return remember(state, orientation) { DefaultPagerNestedScrollConnection(state, orientation) } } /** * The default value of beyondViewportPageCount used to specify the number of pages to compose * and layout before and after the visible pages. It does not include the pages automatically * composed and laid out by the pre-fetcher in the direction of the scroll during scroll events. */ const val BeyondViewportPageCount = 0 } internal fun SnapPosition.currentPageOffset( layoutSize: Int, pageSize: Int, spaceBetweenPages: Int, beforeContentPadding: Int, afterContentPadding: Int, currentPage: Int, currentPageOffsetFraction: Float, pageCount: Int, ): Int { val snapOffset = position( layoutSize, pageSize, beforeContentPadding, afterContentPadding, currentPage, pageCount, ) return (snapOffset - currentPageOffsetFraction * (pageSize + spaceBetweenPages)).roundToInt() } private class DefaultPagerNestedScrollConnection( val state: PagerState, val orientation: Orientation, ) : NestedScrollConnection { fun Velocity.consumeOnOrientation(orientation: Orientation): Velocity { return if (orientation == Orientation.Vertical) { copy(x = 0f) } else { copy(y = 0f) } } override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { return if ( // rounding error and drag only source == NestedScrollSource.UserInput && abs(state.currentPageOffsetFraction) > 1e-6 && // only need to treat deltas on this Pager's orientation available.toFloat().absoluteValue > 0f ) { // find the current and next page (in the direction of dragging) val currentPageOffset = state.currentPageOffsetFraction * state.pageSize val pageAvailableSpace = state.layoutInfo.pageSize + state.layoutInfo.pageSpacing val nextClosestPageOffset = currentPageOffset + pageAvailableSpace * -sign(state.currentPageOffsetFraction) val minBound: Float val maxBound: Float // build min and max bounds in absolute coordinates for nested scroll if (state.currentPageOffsetFraction > 0f) { minBound = nextClosestPageOffset maxBound = currentPageOffset } else { minBound = currentPageOffset maxBound = nextClosestPageOffset } val delta = available.toFloat() val coerced = delta.coerceIn(minBound, maxBound) // dispatch and return reversed as usual val consumed = -state.dispatchRawDelta(-coerced) available.copy( x = if (orientation == Orientation.Horizontal) consumed else available.x, y = if (orientation == Orientation.Vertical) consumed else available.y, ) } else { Offset.Zero } } private fun Offset.toFloat() = if (orientation == Orientation.Horizontal) x else y override fun onPostScroll( consumed: Offset, available: Offset, source: NestedScrollSource, ): Offset { if (source == NestedScrollSource.SideEffect && available.mainAxis() != 0f) { throw CancellationException("Scroll cancelled") } return Offset.Zero } override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { return available.consumeOnOrientation(orientation) } private fun Offset.mainAxis(): Float = if (orientation == Orientation.Horizontal) this.x else this.y } internal fun Modifier.pagerSemantics( state: PagerState, isVertical: Boolean, scope: CoroutineScope, userScrollEnabled: Boolean, ): Modifier { fun performForwardPaging(): Boolean { return if (state.canScrollForward) { scope.launch { state.animateToNextPage() } true } else { false } } fun performBackwardPaging(): Boolean { return if (state.canScrollBackward) { scope.launch { state.animateToPreviousPage() } true } else { false } } return if (userScrollEnabled) { this.then( Modifier.semantics { if (isVertical) { pageUp { performBackwardPaging() } pageDown { performForwardPaging() } } else { pageLeft { performBackwardPaging() } pageRight { performForwardPaging() } } } ) } else { this then Modifier } } private inline fun debugLog(generateMsg: () -> String) { if (PagerDebugConfig.MainPagerComposable) { println("Pager: ${generateMsg()}") } } internal object PagerDebugConfig { const val MainPagerComposable = false const val PagerState = false const val MeasureLogic = false const val ScrollPosition = false const val PagerSnapDistance = false const val PagerSnapLayoutInfoProvider = false } ``` ## File: compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/pager/PagerState.kt ```kotlin /* * Copyright 2023 The Android Open Source Project * * 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. */ @file:Suppress("DEPRECATION") package androidx.compose.foundation.pager import androidx.annotation.FloatRange import androidx.annotation.IntRange as AndroidXIntRange import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animate import androidx.compose.animation.core.spring import androidx.compose.foundation.ComposeFoundationFlags.isCacheWindowForPagerEnabled import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.MutatePriority import androidx.compose.foundation.ScrollIndicatorState import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.gestures.snapping.SnapPosition import androidx.compose.foundation.gestures.stopScroll import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.internal.requirePrecondition import androidx.compose.foundation.lazy.layout.AwaitFirstLayoutModifier import androidx.compose.foundation.lazy.layout.LazyLayoutBeyondBoundsInfo import androidx.compose.foundation.lazy.layout.LazyLayoutCacheWindow import androidx.compose.foundation.lazy.layout.LazyLayoutPinnedItemList import androidx.compose.foundation.lazy.layout.LazyLayoutPrefetchState import androidx.compose.foundation.lazy.layout.LazyLayoutScrollScope import androidx.compose.foundation.lazy.layout.ObservableScopeInvalidator import androidx.compose.foundation.lazy.layout.PrefetchScheduler import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.annotation.FrequentlyChangingValue import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.structuralEqualityPolicy import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.AlignmentLine import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.layout.RemeasurementModifier import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceAtMost import kotlin.coroutines.EmptyCoroutineContext import kotlin.math.abs import kotlin.math.absoluteValue import kotlin.math.roundToLong import kotlin.math.sign import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** * Creates and remember a [PagerState] to be used with a [Pager] * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.PagerWithStateSample * @param initialPage The pager that should be shown first. * @param initialPageOffsetFraction The offset of the initial page as a fraction of the page size. * This should vary between -0.5 and 0.5 and indicates how to offset the initial page from the * snapped position. * @param pageCount The amount of pages this Pager will have. */ @Composable fun rememberPagerState( initialPage: Int = 0, @FloatRange(from = -0.5, to = 0.5) initialPageOffsetFraction: Float = 0f, pageCount: () -> Int, ): PagerState { return rememberSaveable(saver = DefaultPagerState.Saver) { DefaultPagerState(initialPage, initialPageOffsetFraction, pageCount) } .apply { pageCountState.value = pageCount } } /** * Creates a default [PagerState] to be used with a [Pager] * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.PagerWithStateSample * @param currentPage The pager that should be shown first. * @param currentPageOffsetFraction The offset of the initial page as a fraction of the page size. * This should vary between -0.5 and 0.5 and indicates how to offset the initial page from the * snapped position. * @param pageCount The amount of pages this Pager will have. */ fun PagerState( currentPage: Int = 0, @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f, pageCount: () -> Int, ): PagerState = DefaultPagerState(currentPage, currentPageOffsetFraction, pageCount) private class DefaultPagerState( currentPage: Int, currentPageOffsetFraction: Float, updatedPageCount: () -> Int, ) : PagerState(currentPage, currentPageOffsetFraction) { var pageCountState = mutableStateOf(updatedPageCount) override val pageCount: Int get() = pageCountState.value.invoke() companion object { /** To keep current page and current page offset saved */ val Saver: Saver = listSaver( save = { listOf( it.currentPage, (it.currentPageOffsetFraction).coerceIn(MinPageOffset, MaxPageOffset), it.pageCount, ) }, restore = { DefaultPagerState( currentPage = it[0] as Int, currentPageOffsetFraction = it[1] as Float, updatedPageCount = { it[2] as Int }, ) }, ) } } /** The state that can be used to control [VerticalPager] and [HorizontalPager] */ @OptIn(ExperimentalFoundationApi::class) @Stable abstract class PagerState internal constructor( currentPage: Int = 0, @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f, prefetchScheduler: PrefetchScheduler? = null, ) : ScrollableState { /** * @param currentPage The initial page to be displayed * @param currentPageOffsetFraction The offset of the initial page with respect to the start of * the layout. */ constructor( currentPage: Int = 0, @FloatRange(from = -0.5, to = 0.5) currentPageOffsetFraction: Float = 0f, ) : this(currentPage, currentPageOffsetFraction, null) internal var hasLookaheadOccurred: Boolean = false private set internal var approachLayoutInfo: PagerMeasureResult? = null private set /** * The total amount of pages present in this pager. The source of this data should be * observable. */ abstract val pageCount: Int init { requirePrecondition(currentPageOffsetFraction in -0.5..0.5) { "currentPageOffsetFraction $currentPageOffsetFraction is " + "not within the range -0.5 to 0.5" } } /** Difference between the last up and last down events of a scroll event. */ internal var upDownDifference: Offset by mutableStateOf(Offset.Zero) private val scrollPosition = PagerScrollPosition(currentPage, currentPageOffsetFraction, this) internal var firstVisiblePage = currentPage private set internal var firstVisiblePageOffset = 0 private set internal var maxScrollOffset: Long = Long.MAX_VALUE internal var minScrollOffset: Long = 0L private var accumulator: Float = 0.0f /** * The prefetch will act after the measure pass has finished and it needs to know the magnitude * and direction of the scroll that triggered the measure pass */ private var previousPassDelta = 0f /** * The ScrollableController instance. We keep it as we need to call stopAnimation on it once we * reached the end of the list. */ private val scrollableState = ScrollableState { performScroll(it) } /** * Within the scrolling context we can use absolute positions to determine scroll deltas and max * min scrolling. */ private fun performScroll(delta: Float): Float { val currentScrollPosition = currentAbsoluteScrollOffset() debugLog { "\nDelta=$delta " + "\ncurrentScrollPosition=$currentScrollPosition " + "\naccumulator=$accumulator" + "\nmaxScrollOffset=$maxScrollOffset" } val decimalAccumulation = (delta + accumulator) val decimalAccumulationInt = decimalAccumulation.roundToLong() accumulator = decimalAccumulation - decimalAccumulationInt // nothing to scroll if (delta.absoluteValue < 1e-4f) return delta /** * The updated scroll position is the current position with the integer part of the delta * and accumulator applied. */ val updatedScrollPosition = (currentScrollPosition + decimalAccumulationInt) /** Check if the scroll position may be larger than the maximum possible scroll. */ val coercedScrollPosition = updatedScrollPosition.coerceIn(minScrollOffset, maxScrollOffset) /** Check if we actually coerced. */ val changed = updatedScrollPosition != coercedScrollPosition /** Calculated the actual scroll delta to be applied */ val scrollDelta = coercedScrollPosition - currentScrollPosition previousPassDelta = scrollDelta.toFloat() if (scrollDelta.absoluteValue != 0L) { isLastScrollForwardState.value = scrollDelta > 0.0f isLastScrollBackwardState.value = scrollDelta < 0.0f } /** Apply the scroll delta */ var scrolledLayoutInfo = pagerLayoutInfoState.value.copyWithScrollDeltaWithoutRemeasure( delta = -scrollDelta.toInt() ) if (scrolledLayoutInfo != null && this.approachLayoutInfo != null) { // if we were able to scroll the lookahead layout info without remeasure, lets // try to do the same for post lookahead layout info (sometimes they diverge). val scrolledApproachLayoutInfo = approachLayoutInfo?.copyWithScrollDeltaWithoutRemeasure( delta = -scrollDelta.toInt() ) if (scrolledApproachLayoutInfo != null) { // we can apply scroll delta for both phases without remeasure approachLayoutInfo = scrolledApproachLayoutInfo } else { // we can't apply scroll delta for post lookahead, so we have to remeasure scrolledLayoutInfo = null } } if (scrolledLayoutInfo != null) { debugLog { "Will Apply Without Remeasure" } applyMeasureResult( result = scrolledLayoutInfo, isLookingAhead = hasLookaheadOccurred, visibleItemsStayedTheSame = true, ) // we don't need to remeasure, so we only trigger re-placement: placementScopeInvalidator.invalidateScope() layoutWithoutMeasurement++ } else { debugLog { "Will Apply With Remeasure" } scrollPosition.applyScrollDelta(scrollDelta.toInt()) remeasurement?.forceRemeasure() layoutWithMeasurement++ } // Return the consumed value. return (if (changed) scrollDelta else delta).toFloat() } /** Only used for testing to confirm that we're not making too many measure passes */ internal val numMeasurePasses: Int get() = layoutWithMeasurement + layoutWithoutMeasurement internal var layoutWithMeasurement: Int = 0 private set private var layoutWithoutMeasurement: Int = 0 /** Only used for testing to disable prefetching when needed to test the main logic. */ internal var prefetchingEnabled: Boolean = true /** * The index scheduled to be prefetched (or the last prefetched index if the prefetch is done). */ private var indexToPrefetch = -1 /** The handle associated with the current index from [indexToPrefetch]. */ private var currentPrefetchHandle: LazyLayoutPrefetchState.PrefetchHandle? = null /** * Keeps the scrolling direction during the previous calculation in order to be able to detect * the scrolling direction change. */ private var wasPrefetchingForward = false /** Backing state for PagerLayoutInfo */ private var pagerLayoutInfoState = mutableStateOf(EmptyLayoutInfo, neverEqualPolicy()) /** * A [PagerLayoutInfo] that contains useful information about the Pager's last layout pass. For * instance, you can query which pages are currently visible in the layout. * * This property is observable and is updated after every scroll or remeasure. If you use it in * the composable function it will be recomposed on every change causing potential performance * issues including infinity recomposition loop. Therefore, avoid using it in the composition. * * If you want to run some side effects like sending an analytics event or updating a state * based on this value consider using "snapshotFlow": * * @sample androidx.compose.foundation.samples.UsingPagerLayoutInfoForSideEffectSample */ val layoutInfo: PagerLayoutInfo get() = pagerLayoutInfoState.value internal val pageSpacing: Int get() = pagerLayoutInfoState.value.pageSpacing internal val pageSize: Int get() = pagerLayoutInfoState.value.pageSize internal var density: Density = UnitDensity internal val pageSizeWithSpacing: Int get() = pageSize + pageSpacing // non state backed version internal var latestPageSizeWithSpacing: Int = 0 /** * How far the current page needs to scroll so the target page is considered to be the next * page. */ internal val positionThresholdFraction: Float get() = with(density) { val minThreshold = minOf(DefaultPositionThreshold.toPx(), pageSize / 2f) minThreshold / pageSize.toFloat() } internal val internalInteractionSource: MutableInteractionSource = MutableInteractionSource() /** * [InteractionSource] that will be used to dispatch drag events when this list is being * dragged. If you want to know whether the fling (or animated scroll) is in progress, use * [isScrollInProgress]. */ val interactionSource: InteractionSource get() = internalInteractionSource /** * The page that sits closest to the snapped position. This is an observable value and will * change as the pager scrolls either by gesture or animation. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample */ val currentPage: Int get() = scrollPosition.currentPage private var programmaticScrollTargetPage by mutableIntStateOf(-1) private var settledPageState by mutableIntStateOf(currentPage) /** * The page that is currently "settled". This is an animation/gesture unaware page in the sense * that it will not be updated while the pages are being scrolled, but rather when the * animation/scroll settles. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample */ val settledPage by derivedStateOf(structuralEqualityPolicy()) { if (isScrollInProgress) { settledPageState } else { this.currentPage } } /** * The page this [Pager] intends to settle to. During fling or animated scroll (from * [animateScrollToPage] this will represent the page this pager intends to settle to. When no * scroll is ongoing, this will be equal to [currentPage]. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample */ val targetPage: Int by derivedStateOf(structuralEqualityPolicy()) { val finalPage = if (!isScrollInProgress) { this.currentPage } else if (programmaticScrollTargetPage != -1) { programmaticScrollTargetPage } else { // act on scroll only if (abs(this.currentPageOffsetFraction) >= abs(positionThresholdFraction)) { if (lastScrolledForward) { firstVisiblePage + 1 } else { firstVisiblePage } } else { this.currentPage } } finalPage.coerceInPageRange() } /** * Indicates how far the current page is to the snapped position, this will vary from -0.5 (page * is offset towards the start of the layout) to 0.5 (page is offset towards the end of the * layout). This is 0.0 if the [currentPage] is in the snapped position. The value will flip * once the current page changes. * * This property is observable and shouldn't be used as is in a composable function due to * potential performance issues. To use it in the composition, please consider using a derived * state (e.g [derivedStateOf]) to only have recompositions when the derived value changes. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.ObservingStateChangesInPagerStateSample */ val currentPageOffsetFraction: Float @FrequentlyChangingValue get() = scrollPosition.currentPageOffsetFraction internal val prefetchState = LazyLayoutPrefetchState(prefetchScheduler) { Snapshot.withoutReadObservation { schedulePrecomposition(firstVisiblePage) } } /** * Cache window in Pager Initial Layout prefetching happens after the initial measure pass and * latestPageSizeWithSpacing is updated before the prefetching happens. * * For scroll backed prefetching we will use the last known latestPageSizeWithSpacing. */ private val pagerCacheWindow = object : LazyLayoutCacheWindow { override fun Density.calculateAheadWindow(viewport: Int): Int = latestPageSizeWithSpacing override fun Density.calculateBehindWindow(viewport: Int): Int = 0 } private val _scrollIndicatorState = object : ScrollIndicatorState { override val scrollOffset: Int get() = calculateScrollOffset() override val contentSize: Int get() = layoutInfo.calculateContentSize(pageCount) override val viewportSize: Int get() = layoutInfo.mainAxisViewportSize } private fun calculateScrollOffset(): Int { val totalScrollOffset = (pageSizeWithSpacing * firstVisiblePage.toLong()) + firstVisiblePageOffset return totalScrollOffset.fastCoerceAtMost(Int.MAX_VALUE.toLong()).toInt() } internal val cacheWindowLogic = PagerCacheWindowLogic(pagerCacheWindow, prefetchState) { pageCount } internal val beyondBoundsInfo = LazyLayoutBeyondBoundsInfo() /** * Provides a modifier which allows to delay some interactions (e.g. scroll) until layout is * ready. */ internal val awaitLayoutModifier = AwaitFirstLayoutModifier() /** * The [Remeasurement] object associated with our layout. It allows us to remeasure * synchronously during scroll. */ internal var remeasurement: Remeasurement? by mutableStateOf(null) private set /** The modifier which provides [remeasurement]. */ internal val remeasurementModifier = object : RemeasurementModifier { override fun onRemeasurementAvailable(remeasurement: Remeasurement) { this@PagerState.remeasurement = remeasurement } } /** Constraints passed to the prefetcher for premeasuring the prefetched items. */ internal var premeasureConstraints = Constraints() /** Stores currently pinned pages which are always composed, used by for beyond bound pages. */ internal val pinnedPages = LazyLayoutPinnedItemList() internal val nearestRange: IntRange by scrollPosition.nearestRangeState internal val placementScopeInvalidator = ObservableScopeInvalidator() /** * Scroll (jump immediately) to a given [page]. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.ScrollToPageSample * @param page The destination page to scroll to * @param pageOffsetFraction A fraction of the page size that indicates the offset the * destination page will be offset from its snapped position. */ suspend fun scrollToPage( page: Int, @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0f, ) = scroll { debugLog { "Scroll from page=$currentPage to page=$page" } awaitScrollDependencies() requirePrecondition(pageOffsetFraction in -0.5..0.5) { "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5" } val targetPage = page.coerceInPageRange() snapToItem(targetPage, pageOffsetFraction, forceRemeasure = true) } /** * Jump immediately to a given [page] with a given [pageOffsetFraction] inside a [ScrollScope]. * Use this method to create custom animated scrolling experiences. This will update the value * of [currentPage] and [currentPageOffsetFraction] immediately, but can only be used inside a * [ScrollScope], use [scroll] to gain access to a [ScrollScope]. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage * @param page The destination page to scroll to * @param pageOffsetFraction A fraction of the page size that indicates the offset the * destination page will be offset from its snapped position. */ fun ScrollScope.updateCurrentPage( page: Int, @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0.0f, ) { snapToItem(page, pageOffsetFraction, forceRemeasure = true) } /** * Used to update [targetPage] during a programmatic scroll operation. This can only be called * inside a [ScrollScope] and should be called anytime a custom scroll (through [scroll]) is * executed in order to correctly update [targetPage]. This will not move the pages and it's * still the responsibility of the caller to call [ScrollScope.scrollBy] in order to actually * get to [targetPage]. By the end of the [scroll] block, when the [Pager] is no longer * scrolling [targetPage] will assume the value of [currentPage]. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.PagerCustomAnimateScrollToPage */ fun ScrollScope.updateTargetPage(targetPage: Int) { programmaticScrollTargetPage = targetPage.coerceInPageRange() } internal fun snapToItem(page: Int, offsetFraction: Float, forceRemeasure: Boolean) { val positionChanged = scrollPosition.currentPage != page || scrollPosition.currentPageOffsetFraction != offsetFraction if (positionChanged) { // we changed positions, cancel existing requests and wait for the next scroll to // refill the window cacheWindowLogic.resetStrategy() } scrollPosition.requestPositionAndForgetLastKnownKey(page, offsetFraction) if (forceRemeasure) { remeasurement?.forceRemeasure() } else { measurementScopeInvalidator.invalidateScope() } } internal val measurementScopeInvalidator = ObservableScopeInvalidator() /** * Requests the [page] to be at the snapped position during the next remeasure, offset by * [pageOffsetFraction], and schedules a remeasure. * * The scroll position will be updated to the requested position rather than maintain the index * based on the current page key (when a data set change will also be applied during the next * remeasure), but *only* for the next remeasure. * * Any scroll in progress will be cancelled. * * @param page the index to which to scroll. Must be non-negative. * @param pageOffsetFraction the offset fraction that the page should end up after the scroll. */ fun requestScrollToPage( @AndroidXIntRange(from = 0) page: Int, @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0.0f, ) { // Cancel any scroll in progress. if (isScrollInProgress) { pagerLayoutInfoState.value.coroutineScope.launch { stopScroll() } } snapToItem(page, pageOffsetFraction, forceRemeasure = false) } /** * Scroll animate to a given [page]'s closest snap position. If the [page] is too far away from * [currentPage] we will not compose all pages in the way. We will pre-jump to a nearer page, * compose and animate the rest of the pages until [page]. * * Please refer to the sample to learn how to use this API. * * @sample androidx.compose.foundation.samples.AnimateScrollPageSample * @param page The destination page to scroll to * @param pageOffsetFraction A fraction of the page size that indicates the offset the * destination page will be offset from its snapped position. * @param animationSpec An [AnimationSpec] to move between pages. We'll use a [spring] as the * default animation. */ suspend fun animateScrollToPage( page: Int, @FloatRange(from = -0.5, to = 0.5) pageOffsetFraction: Float = 0f, animationSpec: AnimationSpec = spring(), ) { if ( page == currentPage && currentPageOffsetFraction == pageOffsetFraction || pageCount == 0 ) return awaitScrollDependencies() requirePrecondition(pageOffsetFraction in -0.5..0.5) { "pageOffsetFraction $pageOffsetFraction is not within the range -0.5 to 0.5" } val targetPage = page.coerceInPageRange() val targetPageOffsetToSnappedPosition = (pageOffsetFraction * pageSizeWithSpacing) scroll { LazyLayoutScrollScope(this@PagerState, this) .animateScrollToPage( targetPage, targetPageOffsetToSnappedPosition, animationSpec, updateTargetPage = { updateTargetPage(it) }, ) } } private suspend fun awaitScrollDependencies() { if (pagerLayoutInfoState.value === EmptyLayoutInfo) { awaitLayoutModifier.waitForFirstLayout() } } override suspend fun scroll( scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit, ) { awaitScrollDependencies() // will scroll and it's not scrolling already update settled page if (!isScrollInProgress) { settledPageState = currentPage } scrollableState.scroll(scrollPriority, block) programmaticScrollTargetPage = -1 // reset animated scroll target page indicator } override fun dispatchRawDelta(delta: Float): Float { return scrollableState.dispatchRawDelta(delta) } override val isScrollInProgress: Boolean get() = scrollableState.isScrollInProgress final override var canScrollForward: Boolean by mutableStateOf(false) private set final override var canScrollBackward: Boolean by mutableStateOf(false) private set private val isLastScrollForwardState = mutableStateOf(false) private val isLastScrollBackwardState = mutableStateOf(false) @get:Suppress("GetterSetterNames") override val lastScrolledForward: Boolean get() = isLastScrollForwardState.value @get:Suppress("GetterSetterNames") override val lastScrolledBackward: Boolean get() = isLastScrollBackwardState.value override val scrollIndicatorState: ScrollIndicatorState? get() = _scrollIndicatorState /** Updates the state with the new calculated scroll position and consumed scroll. */ internal fun applyMeasureResult( result: PagerMeasureResult, isLookingAhead: Boolean, visibleItemsStayedTheSame: Boolean = false, ) { // update the prefetch state with the number of nested prefetch items this layout // should use. prefetchState.idealNestedPrefetchCount = result.visiblePagesInfo.size // Update non state backed page size info latestPageSizeWithSpacing = result.pageSize + result.pageSpacing if (!isLookingAhead && hasLookaheadOccurred) { debugLog { "Applying Approach Measure Result" } // If there was already a lookahead pass, record this result as Approach result approachLayoutInfo = result } else { debugLog { "Applying Measure Result" } if (isLookingAhead) { hasLookaheadOccurred = true } if (visibleItemsStayedTheSame) { scrollPosition.updateCurrentPageOffsetFraction(result.currentPageOffsetFraction) } else { scrollPosition.updateFromMeasureResult(result) if (isCacheWindowForPagerEnabled) { if (prefetchingEnabled) { cacheWindowLogic.onVisibleItemsChanged(result) } } else { cancelPrefetchIfVisibleItemsChanged(result) } } pagerLayoutInfoState.value = result canScrollForward = result.canScrollForward canScrollBackward = result.canScrollBackward result.firstVisiblePage?.let { firstVisiblePage = it.index } firstVisiblePageOffset = result.firstVisiblePageScrollOffset tryRunPrefetch(result) maxScrollOffset = result.calculateNewMaxScrollOffset(pageCount) minScrollOffset = result.calculateNewMinScrollOffset(pageCount).coerceAtMost(maxScrollOffset) debugLog { "Finished Applying Measure Result\nNew maxScrollOffset=$maxScrollOffset" } } } private fun tryRunPrefetch(result: PagerMeasureResult) = Snapshot.withoutReadObservation { if (!prefetchingEnabled) return if (result.beyondViewportPageCount >= pageCount) return if (abs(previousPassDelta) <= 0.5f) return if (!isGestureActionMatchesScroll(previousPassDelta)) return if (isCacheWindowForPagerEnabled) { cacheWindowLogic.onScroll(previousPassDelta, result) } else { notifyPrefetch(previousPassDelta, result) } } private fun Int.coerceInPageRange() = if (pageCount > 0) { coerceIn(0, pageCount - 1) } else { 0 } // check if the scrolling will be a result of a fling operation. That is, if the scrolling // direction is in the opposite direction of the gesture movement. Also, return true if there // is no applied gesture that causes the scrolling private fun isGestureActionMatchesScroll(scrollDelta: Float): Boolean = if (layoutInfo.orientation == Orientation.Vertical) { sign(scrollDelta) == sign(-upDownDifference.y) } else { sign(scrollDelta) == sign(-upDownDifference.x) } || isNotGestureAction() internal fun isNotGestureAction(): Boolean = upDownDifference.x.toInt() == 0 && upDownDifference.y.toInt() == 0 private fun notifyPrefetch(delta: Float, info: PagerLayoutInfo) { if (!prefetchingEnabled) { return } if (info.visiblePagesInfo.isNotEmpty()) { val isPrefetchingForward = delta > 0 val indexToPrefetch = calculatePrefetchIndex(isPrefetchingForward, info) if (indexToPrefetch in 0 until pageCount) { if (indexToPrefetch != this.indexToPrefetch) { if (wasPrefetchingForward != isPrefetchingForward) { // the scrolling direction has been changed which means the last prefetched // is not going to be reached anytime soon so it is safer to dispose it. // if this item is already visible it is safe to call the method anyway // as it will be no-op currentPrefetchHandle?.cancel() } this.wasPrefetchingForward = isPrefetchingForward this.indexToPrefetch = indexToPrefetch currentPrefetchHandle = prefetchState.schedulePrecompositionAndPremeasure( indexToPrefetch, premeasureConstraints, ) } if (isPrefetchingForward) { val lastItem = info.visiblePagesInfo.last() val pageSize = info.pageSize + info.pageSpacing val distanceToReachNextItem = lastItem.offset + pageSize - info.viewportEndOffset // if in the next frame we will get the same delta will we reach the item? if (distanceToReachNextItem < delta) { currentPrefetchHandle?.markAsUrgent() } } else { val firstItem = info.visiblePagesInfo.first() val distanceToReachNextItem = info.viewportStartOffset - firstItem.offset // if in the next frame we will get the same delta will we reach the item? if (distanceToReachNextItem < -delta) { currentPrefetchHandle?.markAsUrgent() } } } } } private fun cancelPrefetchIfVisibleItemsChanged(info: PagerLayoutInfo) { if (indexToPrefetch != -1 && info.visiblePagesInfo.isNotEmpty()) { val expectedPrefetchIndex = calculatePrefetchIndex(wasPrefetchingForward, info) if (indexToPrefetch != expectedPrefetchIndex) { indexToPrefetch = -1 currentPrefetchHandle?.cancel() currentPrefetchHandle = null } } } /** Calculate the farthest page index that should be prefetched when scrolling. */ private fun calculatePrefetchIndex(forward: Boolean, info: PagerLayoutInfo): Int { return if (forward) { val offset = info.beyondViewportPageCount + PagesToPrefetch if (offset < 0) { // Detect overflow from large beyondViewportPageCount Int.MAX_VALUE } else { info.visiblePagesInfo.last().index + offset } } else { info.visiblePagesInfo.first().index - info.beyondViewportPageCount - PagesToPrefetch } } /** * An utility function to help to calculate a given page's offset. This is an offset that * represents how far [page] is from the settled position (represented by [currentPage] offset). * The difference here is that [currentPageOffsetFraction] is a value between -0.5 and 0.5 and * the value calculated by this function can be larger than these numbers if [page] is different * than [currentPage]. * * For instance, if currentPage=0 and we call [getOffsetDistanceInPages] for page 3, the result * will be 3, meaning the given page is 3 pages away from the current page (the sign represent * the direction of the offset, positive is forward, negative is backwards). Another example is * if currentPage=3 and we call [getOffsetDistanceInPages] for page 1, the result would be -2, * meaning we're 2 pages away (moving backwards) to the current page. * * This offset also works in conjunction with [currentPageOffsetFraction], so if [currentPage] * is out of its snapped position (i.e. currentPageOffsetFraction!=0) then the calculated value * will still represent the offset in number of pages (in this case, not whole pages). For * instance, if currentPage=1 and we're slightly offset, currentPageOffsetFraction=0.2, if we * call this to page 2, the result would be 0.8, that is 0.8 page away from current page (moving * forward). * * @param page The page to calculate the offset from. This should be between 0 and [pageCount]. * @return The offset of [page] with respect to [currentPage]. */ fun getOffsetDistanceInPages(page: Int): Float { requirePrecondition(page in 0..pageCount) { "page $page is not within the range 0 to $pageCount" } return page - currentPage - currentPageOffsetFraction } /** * When the user provided custom keys for the pages we can try to detect when there were pages * added or removed before our current page and keep this page as the current one given that its * index has been changed. */ internal fun matchScrollPositionWithKey( itemProvider: PagerLazyLayoutItemProvider, currentPage: Int = Snapshot.withoutReadObservation { scrollPosition.currentPage }, ): Int = scrollPosition.matchPageWithKey(itemProvider, currentPage) } internal suspend fun PagerState.animateToNextPage() { if (currentPage + 1 < pageCount) animateScrollToPage(currentPage + 1) } internal suspend fun PagerState.animateToPreviousPage() { if (currentPage - 1 >= 0) animateScrollToPage(currentPage - 1) } internal val DefaultPositionThreshold = 56.dp private const val MaxPagesForAnimateScroll = 3 internal const val PagesToPrefetch = 1 private val UnitDensity = object : Density { override val density: Float = 1f override val fontScale: Float = 1f } internal val EmptyLayoutInfo = PagerMeasureResult( visiblePagesInfo = emptyList(), pageSize = 0, pageSpacing = 0, afterContentPadding = 0, orientation = Orientation.Horizontal, viewportStartOffset = 0, viewportEndOffset = 0, reverseLayout = false, beyondViewportPageCount = 0, firstVisiblePage = null, firstVisiblePageScrollOffset = 0, currentPage = null, currentPageOffsetFraction = 0.0f, canScrollForward = false, snapPosition = SnapPosition.Start, measureResult = object : MeasureResult { override val width: Int = 0 override val height: Int = 0 @Suppress("PrimitiveInCollection") override val alignmentLines: Map = mapOf() override fun placeChildren() {} }, remeasureNeeded = false, coroutineScope = CoroutineScope(EmptyCoroutineContext), density = UnitDensity, childConstraints = Constraints(), ) private inline fun debugLog(generateMsg: () -> String) { if (PagerDebugConfig.PagerState) { println("PagerState: ${generateMsg()}") } } internal fun PagerLayoutInfo.calculateNewMaxScrollOffset(pageCount: Int): Long { val pageSizeWithSpacing = pageSpacing + pageSize val maxScrollPossible = (pageCount.toLong()) * pageSizeWithSpacing + beforeContentPadding + afterContentPadding - pageSpacing val layoutSize = if (orientation == Orientation.Horizontal) viewportSize.width else viewportSize.height /** * We need to take into consideration the snap position for max scroll position. For instance, * if SnapPosition.Start, the max scroll position is pageCount * pageSize - viewport. Now if * SnapPosition.End, it should be pageCount * pageSize. Therefore, the snap position discount * varies between 0 and viewport. */ val snapPositionDiscount = layoutSize - (snapPosition.position( layoutSize = layoutSize, itemSize = pageSize, itemIndex = pageCount - 1, beforeContentPadding = beforeContentPadding, afterContentPadding = afterContentPadding, itemCount = pageCount, )) .coerceIn(0, layoutSize) debugLog { "maxScrollPossible=$maxScrollPossible" + "\nsnapPositionDiscount=$snapPositionDiscount" + "\nlayoutSize=$layoutSize" } return (maxScrollPossible - snapPositionDiscount).coerceAtLeast(0L) } private fun PagerMeasureResult.calculateNewMinScrollOffset(pageCount: Int): Long { val layoutSize = if (orientation == Orientation.Horizontal) viewportSize.width else viewportSize.height return snapPosition .position( layoutSize = layoutSize, itemSize = pageSize, itemIndex = 0, beforeContentPadding = beforeContentPadding, afterContentPadding = afterContentPadding, itemCount = pageCount, ) .coerceIn(0, layoutSize) .toLong() } private suspend fun LazyLayoutScrollScope.animateScrollToPage( targetPage: Int, targetPageOffsetToSnappedPosition: Float, animationSpec: AnimationSpec, updateTargetPage: ScrollScope.(Int) -> Unit, ) { updateTargetPage(targetPage) val forward = targetPage > firstVisibleItemIndex val visiblePages = lastVisibleItemIndex - firstVisibleItemIndex + 1 if ( ((forward && targetPage > lastVisibleItemIndex) || (!forward && targetPage < firstVisibleItemIndex)) && abs(targetPage - firstVisibleItemIndex) >= MaxPagesForAnimateScroll ) { val preJumpPosition = if (forward) { (targetPage - visiblePages).coerceAtLeast(firstVisibleItemIndex) } else { (targetPage + visiblePages).coerceAtMost(firstVisibleItemIndex) } debugLog { "animateScrollToPage with pre-jump to position=$preJumpPosition" } // Pre-jump to 1 viewport away from destination page, if possible snapToItem(preJumpPosition, 0) } // The final delta displacement will be the difference between the pages offsets // discounting whatever offset the original page had scrolled plus the offset // fraction requested by the user. val displacement = calculateDistanceTo(targetPage) + targetPageOffsetToSnappedPosition debugLog { "animateScrollToPage $displacement pixels" } var previousValue = 0f animate(0f, displacement, animationSpec = animationSpec) { currentValue, _ -> val delta = currentValue - previousValue val consumed = scrollBy(delta) debugLog { "Dispatched Delta=$delta Consumed=$consumed" } previousValue += consumed } } ``` ================================================ FILE: .claude/skills/compose-expert/references/source-code/material3-source.md ================================================ # Compose Material3 Source Reference ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/MaterialTheme.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.MotionScheme.Companion.standard import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocal import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.compositionLocalWithComputedDefaultOf import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf /** * Material Theming refers to the customization of your Material Design app to better reflect your * product’s brand. * * Material components such as [Button] and [Checkbox] use values provided here when retrieving * default values. * * All values may be set by providing this component with the [colorScheme][ColorScheme], * [typography][Typography] and [shapes][Shapes] attributes. Use this to configure the overall theme * of elements within this MaterialTheme. * * Any values that are not set will inherit the current value from the theme, falling back to the * defaults if there is no parent MaterialTheme. This allows using a MaterialTheme at the top of * your application, and then separate MaterialTheme(s) for different screens / parts of your UI, * overriding only the parts of the theme definition that need to change. * * @sample androidx.compose.material3.samples.MaterialThemeSample * @param colorScheme A complete definition of the Material Color theme for this hierarchy * @param shapes A set of corner shapes to be used as this hierarchy's shape system * @param typography A set of text styles to be used as this hierarchy's typography system * @param content The content inheriting this theme */ @Composable fun MaterialTheme( colorScheme: ColorScheme = MaterialTheme.colorScheme, shapes: Shapes = MaterialTheme.shapes, typography: Typography = MaterialTheme.typography, content: @Composable () -> Unit, ) = MaterialTheme( colorScheme = colorScheme, motionScheme = MaterialTheme.motionScheme, shapes = shapes, typography = typography, content = content, ) /** * Material Theming refers to the customization of your Material Design app to better reflect your * product’s brand. * * Material components such as [Button] and [Checkbox] use values provided here when retrieving * default values. * * All values may be set by providing this component with the [colorScheme][ColorScheme], * [typography][Typography] attributes. Use this to configure the overall theme of elements within * this MaterialTheme. * * Any values that are not set will inherit the current value from the theme, falling back to the * defaults if there is no parent MaterialTheme. This allows using a MaterialTheme at the top of * your application, and then separate MaterialTheme(s) for different screens / parts of your UI, * overriding only the parts of the theme definition that need to change. * * @param colorScheme A complete definition of the Material Color theme for this hierarchy * @param motionScheme A complete definition of the Material Motion scheme for this hierarchy * @param shapes A set of corner shapes to be used as this hierarchy's shape system * @param typography A set of text styles to be used as this hierarchy's typography system */ @Composable fun MaterialTheme( colorScheme: ColorScheme = MaterialTheme.colorScheme, motionScheme: MotionScheme = MaterialTheme.motionScheme, shapes: Shapes = MaterialTheme.shapes, typography: Typography = MaterialTheme.typography, content: @Composable () -> Unit, ) { val theme = MaterialTheme.Values( colorScheme = colorScheme, motionScheme = motionScheme, shapes = shapes, typography = typography, ) val rippleIndication = ripple() val selectionColors = rememberTextSelectionColors(colorScheme) CompositionLocalProvider( _localMaterialTheme provides theme, LocalIndication provides rippleIndication, LocalTextSelectionColors provides selectionColors, ) { EnsurePrecisionPointerListenersRegistered { ProvideTextStyle(value = typography.bodyLarge, content = content) } } } /** * Contains functions to access the current theme values provided at the call site's position in the * hierarchy. */ object MaterialTheme { /** * Retrieves the current [ColorScheme] at the call site's position in the hierarchy. * * @sample androidx.compose.material3.samples.ThemeColorSample */ val colorScheme: ColorScheme @Composable @ReadOnlyComposable get() = LocalMaterialTheme.current.colorScheme /** * Retrieves the current [Typography] at the call site's position in the hierarchy. * * @sample androidx.compose.material3.samples.ThemeTextStyleSample */ val typography: Typography @Composable @ReadOnlyComposable get() = LocalMaterialTheme.current.typography /** * Retrieves the current [Shapes] at the call site's position in the hierarchy. * * @sample androidx.compose.material3.samples.ThemeShapeSample */ val shapes: Shapes @Composable @ReadOnlyComposable get() = LocalMaterialTheme.current.shapes /** Retrieves the current [MotionScheme] at the call site's position in the hierarchy. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) val motionScheme: MotionScheme @Composable @ReadOnlyComposable get() = LocalMaterialTheme.current.motionScheme /** * [CompositionLocal] providing [MaterialThemeSubsystems] throughout the hierarchy. You can use * properties in the companion object to access specific subsystems, for example [colorScheme]. * To provide a new value for this, use [MaterialTheme]. This API is exposed to allow retrieving * values from inside CompositionLocalConsumerModifierNode implementations - in most cases you * should use [colorScheme] and other properties directly. */ val LocalMaterialTheme: CompositionLocal get() = _localMaterialTheme /** * A read-only `CompositionLocal` that provides the current [MotionScheme] to Material 3 * components. * * The motion scheme is typically supplied by [MaterialTheme.motionScheme] and can be overridden * for specific UI subtrees by wrapping it with another [MaterialTheme]. * * This API is exposed to allow retrieving motion values from inside * `CompositionLocalConsumerModifierNode` implementations, but in most cases it's recommended to * read the motion values from [MaterialTheme.motionScheme]. */ @Suppress("ExperimentalPropertyAnnotation") @ExperimentalMaterial3ExpressiveApi @Deprecated( level = DeprecationLevel.WARNING, message = "Use [LocalMaterialTheme.current.motionScheme] instead", ) val LocalMotionScheme: CompositionLocal get() = compositionLocalWithComputedDefaultOf { LocalMaterialTheme.currentValue.motionScheme } /** * Material 3 contains different theme subsystems to allow visual customization across a UI * hierarchy. * * Components use properties provided here when retrieving default values. * * @property colorScheme [ColorScheme] used by material components * @property typography [Typography] used by material components * @property shapes [Shapes] used by material components * @property motionScheme [MotionScheme] used by material components */ @Immutable class Values( val colorScheme: ColorScheme = lightColorScheme(), val typography: Typography = Typography(), val shapes: Shapes = Shapes(), val motionScheme: MotionScheme = standard(), ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || this::class != other::class) return false other as Values if (colorScheme != other.colorScheme) return false if (typography != other.typography) return false if (shapes != other.shapes) return false if (motionScheme != other.motionScheme) return false return true } override fun hashCode(): Int { var result = colorScheme.hashCode() result = 31 * result + typography.hashCode() result = 31 * result + shapes.hashCode() result = 31 * result + motionScheme.hashCode() return result } override fun toString(): String { return "Values(colorScheme=$colorScheme, " + "typography=$typography, shapes=$shapes, motionScheme=$motionScheme)" } } } /** * Material Expressive Theming refers to the customization of your Material Design app to better * reflect your product’s brand. * * Material components such as [Button] and [Checkbox] use values provided here when retrieving * default values. * * All values may be set by providing this component with the [colorScheme][ColorScheme], * [typography][Typography], [shapes][Shapes] attributes. Use this to configure the overall theme of * elements within this MaterialTheme. * * Any values that are not set will fall back to the defaults. To inherit the current value from the * theme, pass them into subsequent calls and override only the parts of the theme definition that * need to change. * * Alternatively, only call this function at the top of your application, and then call * [MaterialTheme] to specify separate MaterialTheme(s) for different screens / parts of your UI, * overriding only the parts of the theme definition that need to change. * * @sample androidx.compose.material3.samples.MaterialExpressiveThemeSample * @param colorScheme A complete definition of the Material Color theme for this hierarchy * @param motionScheme A complete definition of the Material motion theme for this hierarchy * @param shapes A set of corner shapes to be used as this hierarchy's shape system * @param typography A set of text styles to be used as this hierarchy's typography system * @param content The content inheriting this theme */ @ExperimentalMaterial3ExpressiveApi @Composable fun MaterialExpressiveTheme( colorScheme: ColorScheme? = null, motionScheme: MotionScheme? = null, shapes: Shapes? = null, typography: Typography? = null, content: @Composable () -> Unit, ) { if (LocalUsingExpressiveTheme.current) { MaterialTheme( colorScheme = colorScheme ?: MaterialTheme.colorScheme, motionScheme = motionScheme ?: MaterialTheme.motionScheme, typography = typography ?: MaterialTheme.typography, shapes = shapes ?: MaterialTheme.shapes, content = content, ) } else { CompositionLocalProvider(LocalUsingExpressiveTheme provides true) { MaterialTheme( colorScheme = colorScheme ?: expressiveLightColorScheme(), motionScheme = motionScheme ?: MotionScheme.expressive(), shapes = shapes ?: Shapes(), // TODO: replace with calls to Expressive typography default typography = typography ?: Typography(), content = content, ) } } } internal val LocalUsingExpressiveTheme = staticCompositionLocalOf { false } @Composable /*@VisibleForTesting*/ internal fun rememberTextSelectionColors(colorScheme: ColorScheme): TextSelectionColors { val primaryColor = colorScheme.primary return remember(primaryColor) { TextSelectionColors( handleColor = primaryColor, backgroundColor = primaryColor.copy(alpha = TextSelectionBackgroundOpacity), ) } } /*@VisibleForTesting*/ internal const val TextSelectionBackgroundOpacity = 0.4f /** Use [MaterialTheme.LocalMaterialTheme] to access this publicly. */ @Suppress("CompositionLocalNaming") private val _localMaterialTheme: ProvidableCompositionLocal = staticCompositionLocalOf { MaterialTheme.Values() } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ColorScheme.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.material3.tokens.ColorDarkTokens import androidx.compose.material3.tokens.ColorLightTokens import androidx.compose.material3.tokens.ColorSchemeKeyTokens import androidx.compose.material3.tokens.PaletteTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.Stable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlin.math.ln /** * A color scheme holds all the named color parameters for a [MaterialTheme]. * * Color schemes are designed to be harmonious, ensure accessible text, and distinguish UI elements * and surfaces from one another. There are two built-in baseline schemes, [lightColorScheme] and a * [darkColorScheme], that can be used as-is or customized. * * The Material color system and custom schemes provide default values for color as a starting point * for customization. * * To learn more about colors, see * [Material Design colors](https://m3.material.io/styles/color/system/overview). * * @property primary The primary color is the color displayed most frequently across your app’s * screens and components. * @property onPrimary Color used for text and icons displayed on top of the primary color. * @property primaryContainer The preferred tonal color of containers. * @property onPrimaryContainer The color (and state variants) that should be used for content on * top of [primaryContainer]. * @property inversePrimary Color to be used as a "primary" color in places where the inverse color * scheme is needed, such as the button on a SnackBar. * @property secondary The secondary color provides more ways to accent and distinguish your * product. Secondary colors are best for: * - Floating action buttons * - Selection controls, like checkboxes and radio buttons * - Highlighting selected text * - Links and headlines * * @property onSecondary Color used for text and icons displayed on top of the secondary color. * @property secondaryContainer A tonal color to be used in containers. * @property onSecondaryContainer The color (and state variants) that should be used for content on * top of [secondaryContainer]. * @property tertiary The tertiary color that can be used to balance primary and secondary colors, * or bring heightened attention to an element such as an input field. * @property onTertiary Color used for text and icons displayed on top of the tertiary color. * @property tertiaryContainer A tonal color to be used in containers. * @property onTertiaryContainer The color (and state variants) that should be used for content on * top of [tertiaryContainer]. * @property background The background color that appears behind scrollable content. * @property onBackground Color used for text and icons displayed on top of the background color. * @property surface The surface color that affect surfaces of components, such as cards, sheets, * and menus. * @property onSurface Color used for text and icons displayed on top of the surface color. * @property surfaceVariant Another option for a color with similar uses of [surface]. * @property onSurfaceVariant The color (and state variants) that can be used for content on top of * [surface]. * @property surfaceTint This color will be used by components that apply tonal elevation and is * applied on top of [surface]. The higher the elevation the more this color is used. * @property inverseSurface A color that contrasts sharply with [surface]. Useful for surfaces that * sit on top of other surfaces with [surface] color. * @property inverseOnSurface A color that contrasts well with [inverseSurface]. Useful for content * that sits on top of containers that are [inverseSurface]. * @property error The error color is used to indicate errors in components, such as invalid text in * a text field. * @property onError Color used for text and icons displayed on top of the error color. * @property errorContainer The preferred tonal color of error containers. * @property onErrorContainer The color (and state variants) that should be used for content on top * of [errorContainer]. * @property outline Subtle color used for boundaries. Outline color role adds contrast for * accessibility purposes. * @property outlineVariant Utility color used for boundaries for decorative elements when strong * contrast is not required. * @property scrim Color of a scrim that obscures content. * @property surfaceBright A [surface] variant that is always brighter than [surface], whether in * light or dark mode. * @property surfaceDim A [surface] variant that is always dimmer than [surface], whether in light * or dark mode. * @property surfaceContainer A [surface] variant that affects containers of components, such as * cards, sheets, and menus. * @property surfaceContainerHigh A [surface] variant for containers with higher emphasis than * [surfaceContainer]. Use this role for content which requires more emphasis than * [surfaceContainer]. * @property surfaceContainerHighest A [surface] variant for containers with higher emphasis than * [surfaceContainerHigh]. Use this role for content which requires more emphasis than * [surfaceContainerHigh]. * @property surfaceContainerLow A [surface] variant for containers with lower emphasis than * [surfaceContainer]. Use this role for content which requires less emphasis than * [surfaceContainer]. * @property surfaceContainerLowest A [surface] variant for containers with lower emphasis than * [surfaceContainerLow]. Use this role for content which requires less emphasis than * [surfaceContainerLow]. * @property primaryFixed A [primary] variant that maintains the same tone in light and dark themes. * The fixed color role may be used instead of the equivalent container role in situations where * such fixed behavior is desired. * @property primaryFixedDim A [primary] variant that maintains the same tone in light and dark * themes. Dim roles provide a stronger, more emphasized tone relative to the equivalent fixed * color. * @property onPrimaryFixed Color used for text and icons displayed on top of [primaryFixed] or * [primaryFixedDim]. Maintains the same tone in light and dark themes. * @property onPrimaryFixedVariant An [onPrimaryFixed] variant which provides less emphasis. Useful * when a strong contrast is not required. * @property secondaryFixed A [secondary] variant that maintains the same tone in light and dark * themes. The fixed color role may be used instead of the equivalent container role in situations * where such fixed behavior is desired. * @property secondaryFixedDim A [secondary] variant that maintains the same tone in light and dark * themes. Dim roles provide a stronger, more emphasized tone relative to the equivalent fixed * color. * @property onSecondaryFixed Color used for text and icons displayed on top of [secondaryFixed] or * [secondaryFixedDim]. Maintains the same tone in light and dark themes. * @property onSecondaryFixedVariant An [onSecondaryFixed] variant which provides less emphasis. * Useful when a strong contrast is not required. * @property tertiaryFixed A [tertiary] variant that maintains the same tone in light and dark * themes. The fixed color role may be used instead of the equivalent container role in situations * where such fixed behavior is desired. * @property tertiaryFixedDim A [tertiary] variant that maintains the same tone in light and dark * themes. Dim roles provide a stronger, more emphasized tone relative to the equivalent fixed * color. * @property onTertiaryFixed Color used for text and icons displayed on top of [tertiaryFixed] or * [tertiaryFixedDim]. Maintains the same tone in light and dark themes. * @property onTertiaryFixedVariant An [onTertiaryFixed] variant which provides less emphasis. * Useful when a strong contrast is not required. */ @Immutable class ColorScheme( val primary: Color, val onPrimary: Color, val primaryContainer: Color, val onPrimaryContainer: Color, val inversePrimary: Color, val secondary: Color, val onSecondary: Color, val secondaryContainer: Color, val onSecondaryContainer: Color, val tertiary: Color, val onTertiary: Color, val tertiaryContainer: Color, val onTertiaryContainer: Color, val background: Color, val onBackground: Color, val surface: Color, val onSurface: Color, val surfaceVariant: Color, val onSurfaceVariant: Color, val surfaceTint: Color, val inverseSurface: Color, val inverseOnSurface: Color, val error: Color, val onError: Color, val errorContainer: Color, val onErrorContainer: Color, val outline: Color, val outlineVariant: Color, val scrim: Color, val surfaceBright: Color, val surfaceDim: Color, val surfaceContainer: Color, val surfaceContainerHigh: Color, val surfaceContainerHighest: Color, val surfaceContainerLow: Color, val surfaceContainerLowest: Color, val primaryFixed: Color, val primaryFixedDim: Color, val onPrimaryFixed: Color, val onPrimaryFixedVariant: Color, val secondaryFixed: Color, val secondaryFixedDim: Color, val onSecondaryFixed: Color, val onSecondaryFixedVariant: Color, val tertiaryFixed: Color, val tertiaryFixedDim: Color, val onTertiaryFixed: Color, val onTertiaryFixedVariant: Color, ) { @Deprecated( level = DeprecationLevel.WARNING, message = "Use constructor with additional 'fixed' container roles.", replaceWith = ReplaceWith( "ColorScheme(primary,\n" + "onPrimary,\n" + "primaryContainer,\n" + "onPrimaryContainer,\n" + "inversePrimary,\n" + "secondary,\n" + "onSecondary,\n" + "secondaryContainer,\n" + "onSecondaryContainer,\n" + "tertiary,\n" + "onTertiary,\n" + "tertiaryContainer,\n" + "onTertiaryContainer,\n" + "background,\n" + "onBackground,\n" + "surface,\n" + "onSurface,\n" + "surfaceVariant,\n" + "onSurfaceVariant,\n" + "surfaceTint,\n" + "inverseSurface,\n" + "inverseOnSurface,\n" + "error,\n" + "onError,\n" + "errorContainer,\n" + "onErrorContainer,\n" + "outline,\n" + "outlineVariant,\n" + "scrim,\n" + "surfaceBright,\n" + "surfaceDim,\n" + "surfaceContainer,\n" + "surfaceContainerHigh,\n" + "surfaceContainerHighest,\n" + "surfaceContainerLow,\n" + "surfaceContainerLowest,)" ), ) constructor( primary: Color, onPrimary: Color, primaryContainer: Color, onPrimaryContainer: Color, inversePrimary: Color, secondary: Color, onSecondary: Color, secondaryContainer: Color, onSecondaryContainer: Color, tertiary: Color, onTertiary: Color, tertiaryContainer: Color, onTertiaryContainer: Color, background: Color, onBackground: Color, surface: Color, onSurface: Color, surfaceVariant: Color, onSurfaceVariant: Color, surfaceTint: Color, inverseSurface: Color, inverseOnSurface: Color, error: Color, onError: Color, errorContainer: Color, onErrorContainer: Color, outline: Color, outlineVariant: Color, scrim: Color, surfaceBright: Color, surfaceDim: Color, surfaceContainer: Color, surfaceContainerHigh: Color, surfaceContainerHighest: Color, surfaceContainerLow: Color, surfaceContainerLowest: Color, ) : this( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceDim = surfaceDim, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, primaryFixed = Color.Unspecified, primaryFixedDim = Color.Unspecified, onPrimaryFixed = Color.Unspecified, onPrimaryFixedVariant = Color.Unspecified, secondaryFixed = Color.Unspecified, secondaryFixedDim = Color.Unspecified, onSecondaryFixed = Color.Unspecified, onSecondaryFixedVariant = Color.Unspecified, tertiaryFixed = Color.Unspecified, tertiaryFixedDim = Color.Unspecified, onTertiaryFixed = Color.Unspecified, onTertiaryFixedVariant = Color.Unspecified, ) /** Returns a copy of this ColorScheme, optionally overriding some of the values. */ fun copy( primary: Color = this.primary, onPrimary: Color = this.onPrimary, primaryContainer: Color = this.primaryContainer, onPrimaryContainer: Color = this.onPrimaryContainer, inversePrimary: Color = this.inversePrimary, secondary: Color = this.secondary, onSecondary: Color = this.onSecondary, secondaryContainer: Color = this.secondaryContainer, onSecondaryContainer: Color = this.onSecondaryContainer, tertiary: Color = this.tertiary, onTertiary: Color = this.onTertiary, tertiaryContainer: Color = this.tertiaryContainer, onTertiaryContainer: Color = this.onTertiaryContainer, background: Color = this.background, onBackground: Color = this.onBackground, surface: Color = this.surface, onSurface: Color = this.onSurface, surfaceVariant: Color = this.surfaceVariant, onSurfaceVariant: Color = this.onSurfaceVariant, surfaceTint: Color = this.surfaceTint, inverseSurface: Color = this.inverseSurface, inverseOnSurface: Color = this.inverseOnSurface, error: Color = this.error, onError: Color = this.onError, errorContainer: Color = this.errorContainer, onErrorContainer: Color = this.onErrorContainer, outline: Color = this.outline, outlineVariant: Color = this.outlineVariant, scrim: Color = this.scrim, surfaceBright: Color = this.surfaceBright, surfaceDim: Color = this.surfaceDim, surfaceContainer: Color = this.surfaceContainer, surfaceContainerHigh: Color = this.surfaceContainerHigh, surfaceContainerHighest: Color = this.surfaceContainerHighest, surfaceContainerLow: Color = this.surfaceContainerLow, surfaceContainerLowest: Color = this.surfaceContainerLowest, primaryFixed: Color = this.primaryFixed, primaryFixedDim: Color = this.primaryFixedDim, onPrimaryFixed: Color = this.onPrimaryFixed, onPrimaryFixedVariant: Color = this.onPrimaryFixedVariant, secondaryFixed: Color = this.secondaryFixed, secondaryFixedDim: Color = this.secondaryFixedDim, onSecondaryFixed: Color = this.onSecondaryFixed, onSecondaryFixedVariant: Color = this.onSecondaryFixedVariant, tertiaryFixed: Color = this.tertiaryFixed, tertiaryFixedDim: Color = this.tertiaryFixedDim, onTertiaryFixed: Color = this.onTertiaryFixed, onTertiaryFixedVariant: Color = this.onTertiaryFixedVariant, ): ColorScheme = ColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceDim = surfaceDim, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, primaryFixed = primaryFixed, primaryFixedDim = primaryFixedDim, onPrimaryFixed = onPrimaryFixed, onPrimaryFixedVariant = onPrimaryFixedVariant, secondaryFixed = secondaryFixed, secondaryFixedDim = secondaryFixedDim, onSecondaryFixed = onSecondaryFixed, onSecondaryFixedVariant = onSecondaryFixedVariant, tertiaryFixed = tertiaryFixed, tertiaryFixedDim = tertiaryFixedDim, onTertiaryFixed = onTertiaryFixed, onTertiaryFixedVariant = onTertiaryFixedVariant, ) @Deprecated( message = "Maintained for binary compatibility. Use overload with additional fixed roles " + "instead", level = DeprecationLevel.HIDDEN, ) fun copy( primary: Color = this.primary, onPrimary: Color = this.onPrimary, primaryContainer: Color = this.primaryContainer, onPrimaryContainer: Color = this.onPrimaryContainer, inversePrimary: Color = this.inversePrimary, secondary: Color = this.secondary, onSecondary: Color = this.onSecondary, secondaryContainer: Color = this.secondaryContainer, onSecondaryContainer: Color = this.onSecondaryContainer, tertiary: Color = this.tertiary, onTertiary: Color = this.onTertiary, tertiaryContainer: Color = this.tertiaryContainer, onTertiaryContainer: Color = this.onTertiaryContainer, background: Color = this.background, onBackground: Color = this.onBackground, surface: Color = this.surface, onSurface: Color = this.onSurface, surfaceVariant: Color = this.surfaceVariant, onSurfaceVariant: Color = this.onSurfaceVariant, surfaceTint: Color = this.surfaceTint, inverseSurface: Color = this.inverseSurface, inverseOnSurface: Color = this.inverseOnSurface, error: Color = this.error, onError: Color = this.onError, errorContainer: Color = this.errorContainer, onErrorContainer: Color = this.onErrorContainer, outline: Color = this.outline, outlineVariant: Color = this.outlineVariant, scrim: Color = this.scrim, ): ColorScheme = copy( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, ) @Deprecated( message = "Maintained for binary compatibility. Use overload with additional fixed roles " + "instead", level = DeprecationLevel.HIDDEN, ) fun copy( primary: Color = this.primary, onPrimary: Color = this.onPrimary, primaryContainer: Color = this.primaryContainer, onPrimaryContainer: Color = this.onPrimaryContainer, inversePrimary: Color = this.inversePrimary, secondary: Color = this.secondary, onSecondary: Color = this.onSecondary, secondaryContainer: Color = this.secondaryContainer, onSecondaryContainer: Color = this.onSecondaryContainer, tertiary: Color = this.tertiary, onTertiary: Color = this.onTertiary, tertiaryContainer: Color = this.tertiaryContainer, onTertiaryContainer: Color = this.onTertiaryContainer, background: Color = this.background, onBackground: Color = this.onBackground, surface: Color = this.surface, onSurface: Color = this.onSurface, surfaceVariant: Color = this.surfaceVariant, onSurfaceVariant: Color = this.onSurfaceVariant, surfaceTint: Color = this.surfaceTint, inverseSurface: Color = this.inverseSurface, inverseOnSurface: Color = this.inverseOnSurface, error: Color = this.error, onError: Color = this.onError, errorContainer: Color = this.errorContainer, onErrorContainer: Color = this.onErrorContainer, outline: Color = this.outline, outlineVariant: Color = this.outlineVariant, scrim: Color = this.scrim, surfaceBright: Color = this.surfaceBright, surfaceDim: Color = this.surfaceDim, surfaceContainer: Color = this.surfaceContainer, surfaceContainerHigh: Color = this.surfaceContainerHigh, surfaceContainerHighest: Color = this.surfaceContainerHighest, surfaceContainerLow: Color = this.surfaceContainerLow, surfaceContainerLowest: Color = this.surfaceContainerLowest, ): ColorScheme = copy( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceDim = surfaceDim, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, ) override fun toString(): String { return "ColorScheme(" + "primary=$primary" + "onPrimary=$onPrimary" + "primaryContainer=$primaryContainer" + "onPrimaryContainer=$onPrimaryContainer" + "inversePrimary=$inversePrimary" + "secondary=$secondary" + "onSecondary=$onSecondary" + "secondaryContainer=$secondaryContainer" + "onSecondaryContainer=$onSecondaryContainer" + "tertiary=$tertiary" + "onTertiary=$onTertiary" + "tertiaryContainer=$tertiaryContainer" + "onTertiaryContainer=$onTertiaryContainer" + "background=$background" + "onBackground=$onBackground" + "surface=$surface" + "onSurface=$onSurface" + "surfaceVariant=$surfaceVariant" + "onSurfaceVariant=$onSurfaceVariant" + "surfaceTint=$surfaceTint" + "inverseSurface=$inverseSurface" + "inverseOnSurface=$inverseOnSurface" + "error=$error" + "onError=$onError" + "errorContainer=$errorContainer" + "onErrorContainer=$onErrorContainer" + "outline=$outline" + "outlineVariant=$outlineVariant" + "scrim=$scrim" + "surfaceBright=$surfaceBright" + "surfaceDim=$surfaceDim" + "surfaceContainer=$surfaceContainer" + "surfaceContainerHigh=$surfaceContainerHigh" + "surfaceContainerHighest=$surfaceContainerHighest" + "surfaceContainerLow=$surfaceContainerLow" + "surfaceContainerLowest=$surfaceContainerLowest" + "primaryFixed=$primaryFixed" + "primaryFixedDim=$primaryFixedDim" + "onPrimaryFixed=$onPrimaryContainer" + "onPrimaryFixedVariant=$onPrimaryFixedVariant" + "secondaryFixed=$secondaryFixed" + "secondaryFixedDim=$secondaryFixedDim" + "onSecondaryFixed=$onSecondaryFixed" + "onSecondaryFixedVariant=$onSecondaryFixedVariant" + "tertiaryFixed=$tertiaryFixed" + "tertiaryFixedDim=$tertiaryFixedDim" + "onTertiaryFixed=$onTertiaryFixed" + "onTertiaryFixedVariant=$onTertiaryFixedVariant" + ")" } internal var defaultButtonColorsCached: ButtonColors? = null internal var defaultElevatedButtonColorsCached: ButtonColors? = null internal var defaultFilledTonalButtonColorsCached: ButtonColors? = null internal var defaultOutlinedButtonColorsCached: ButtonColors? = null internal var defaultTextButtonColorsCached: ButtonColors? = null internal var defaultCardColorsCached: CardColors? = null internal var defaultElevatedCardColorsCached: CardColors? = null internal var defaultOutlinedCardColorsCached: CardColors? = null internal var defaultAssistChipColorsCached: ChipColors? = null internal var defaultElevatedAssistChipColorsCached: ChipColors? = null internal var defaultSuggestionChipColorsCached: ChipColors? = null internal var defaultElevatedSuggestionChipColorsCached: ChipColors? = null internal var defaultFilterChipColorsCached: SelectableChipColors? = null internal var defaultElevatedFilterChipColorsCached: SelectableChipColors? = null internal var defaultInputChipColorsCached: SelectableChipColors? = null internal var defaultVerticalDragHandleColorsCached: DragHandleColors? = null @OptIn(ExperimentalMaterial3Api::class) internal var defaultTopAppBarColorsCached: TopAppBarColors? = null internal var defaultCheckboxColorsCached: CheckboxColors? = null @OptIn(ExperimentalMaterial3Api::class) internal var defaultDatePickerColorsCached: DatePickerColors? = null internal var defaultIconButtonColorsCached: IconButtonColors? = null internal var defaultIconButtonVibrantColorsCached: IconButtonColors? = null internal var defaultIconToggleButtonColorsCached: IconToggleButtonColors? = null internal var defaultIconToggleButtonVibrantColorsCached: IconToggleButtonColors? = null internal var defaultFilledIconButtonColorsCached: IconButtonColors? = null internal var defaultFilledIconToggleButtonColorsCached: IconToggleButtonColors? = null internal var defaultFilledTonalIconButtonColorsCached: IconButtonColors? = null internal var defaultFilledTonalIconToggleButtonColorsCached: IconToggleButtonColors? = null internal var defaultOutlinedIconButtonColorsCached: IconButtonColors? = null internal var defaultOutlinedIconButtonVibrantColorsCached: IconButtonColors? = null internal var defaultOutlinedIconToggleButtonColorsCached: IconToggleButtonColors? = null internal var defaultOutlinedIconToggleButtonVibrantColorsCached: IconToggleButtonColors? = null internal var defaultToggleButtonColorsCached: ToggleButtonColors? = null internal var defaultElevatedToggleButtonColorsCached: ToggleButtonColors? = null internal var defaultTonalToggleButtonColorsCached: ToggleButtonColors? = null internal var defaultOutlinedToggleButtonColorsCached: ToggleButtonColors? = null internal var defaultListItemColorsCached: ListItemColors? = null internal var defaultSegmentedListItemColorsCached: ListItemColors? = null internal var defaultMenuItemColorsCached: MenuItemColors? = null internal var defaultMenuSelectableItemColorsCached: MenuItemColors? = null internal var defaultMenuSelectableItemVibrantColorsCached: MenuItemColors? = null internal var defaultNavigationBarItemColorsCached: NavigationBarItemColors? = null internal var defaultShortNavigationBarItemColorsCached: NavigationItemColors? = null internal var defaultNavigationRailItemColorsCached: NavigationRailItemColors? = null internal var defaultWideWideNavigationRailColorsCached: WideNavigationRailColors? = null internal var defaultWideNavigationRailItemColorsCached: NavigationItemColors? = null internal var defaultRadioButtonColorsCached: RadioButtonColors? = null internal var defaultSegmentedButtonColorsCached: SegmentedButtonColors? = null internal var defaultSliderColorsCached: SliderColors? = null internal var defaultSwitchColorsCached: SwitchColors? = null internal var defaultOutlinedTextFieldColorsCached: TextFieldColors? = null internal var defaultTextFieldColorsCached: TextFieldColors? = null @OptIn(ExperimentalMaterial3Api::class) internal var defaultTimePickerColorsCached: TimePickerColors? = null @OptIn(ExperimentalMaterial3Api::class) internal var defaultRichTooltipColorsCached: RichTooltipColors? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultFloatingToolbarStandardColorsCached: FloatingToolbarColors? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultFloatingToolbarVibrantColorsCached: FloatingToolbarColors? = null @Deprecated( level = DeprecationLevel.WARNING, message = "Use constructor with additional 'surfaceContainer' roles.", replaceWith = ReplaceWith( "ColorScheme(primary,\n" + "onPrimary,\n" + "primaryContainer,\n" + "onPrimaryContainer,\n" + "inversePrimary,\n" + "secondary,\n" + "onSecondary,\n" + "secondaryContainer,\n" + "onSecondaryContainer,\n" + "tertiary,\n" + "onTertiary,\n" + "tertiaryContainer,\n" + "onTertiaryContainer,\n" + "background,\n" + "onBackground,\n" + "surface,\n" + "onSurface,\n" + "surfaceVariant,\n" + "onSurfaceVariant,\n" + "surfaceTint,\n" + "inverseSurface,\n" + "inverseOnSurface,\n" + "error,\n" + "onError,\n" + "errorContainer,\n" + "onErrorContainer,\n" + "outline,\n" + "outlineVariant,\n" + "scrim,\n" + "surfaceBright,\n" + "surfaceDim,\n" + "surfaceContainer,\n" + "surfaceContainerHigh,\n" + "surfaceContainerHighest,\n" + "surfaceContainerLow,\n" + "surfaceContainerLowest,\n" + "primaryFixed,\n" + "primaryFixedDim,\n" + "onPrimaryFixed,\n" + "onPrimaryFixedVariant,\n" + "secondaryFixed,\n" + "secondaryFixedDim,\n" + "onSecondaryFixed,\n" + "onSecondaryFixedVariant,\n" + "tertiaryFixed,\n" + "tertiaryFixedDim,\n" + "onTertiaryFixed,\n" + "onTertiaryFixedVariant" + ")" ), ) constructor( primary: Color, onPrimary: Color, primaryContainer: Color, onPrimaryContainer: Color, inversePrimary: Color, secondary: Color, onSecondary: Color, secondaryContainer: Color, onSecondaryContainer: Color, tertiary: Color, onTertiary: Color, tertiaryContainer: Color, onTertiaryContainer: Color, background: Color, onBackground: Color, surface: Color, onSurface: Color, surfaceVariant: Color, onSurfaceVariant: Color, surfaceTint: Color, inverseSurface: Color, inverseOnSurface: Color, error: Color, onError: Color, errorContainer: Color, onErrorContainer: Color, outline: Color, outlineVariant: Color, scrim: Color, ) : this( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = Color.Unspecified, surfaceDim = Color.Unspecified, surfaceContainer = Color.Unspecified, surfaceContainerHigh = Color.Unspecified, surfaceContainerHighest = Color.Unspecified, surfaceContainerLow = Color.Unspecified, surfaceContainerLowest = Color.Unspecified, primaryFixed = Color.Unspecified, primaryFixedDim = Color.Unspecified, onPrimaryFixed = Color.Unspecified, onPrimaryFixedVariant = Color.Unspecified, secondaryFixed = Color.Unspecified, secondaryFixedDim = Color.Unspecified, onSecondaryFixed = Color.Unspecified, onSecondaryFixedVariant = Color.Unspecified, tertiaryFixed = Color.Unspecified, tertiaryFixedDim = Color.Unspecified, onTertiaryFixed = Color.Unspecified, onTertiaryFixedVariant = Color.Unspecified, ) } /** Returns a light Material color scheme. */ fun lightColorScheme( primary: Color = ColorLightTokens.Primary, onPrimary: Color = ColorLightTokens.OnPrimary, primaryContainer: Color = ColorLightTokens.PrimaryContainer, onPrimaryContainer: Color = ColorLightTokens.OnPrimaryContainer, inversePrimary: Color = ColorLightTokens.InversePrimary, secondary: Color = ColorLightTokens.Secondary, onSecondary: Color = ColorLightTokens.OnSecondary, secondaryContainer: Color = ColorLightTokens.SecondaryContainer, onSecondaryContainer: Color = ColorLightTokens.OnSecondaryContainer, tertiary: Color = ColorLightTokens.Tertiary, onTertiary: Color = ColorLightTokens.OnTertiary, tertiaryContainer: Color = ColorLightTokens.TertiaryContainer, onTertiaryContainer: Color = ColorLightTokens.OnTertiaryContainer, background: Color = ColorLightTokens.Background, onBackground: Color = ColorLightTokens.OnBackground, surface: Color = ColorLightTokens.Surface, onSurface: Color = ColorLightTokens.OnSurface, surfaceVariant: Color = ColorLightTokens.SurfaceVariant, onSurfaceVariant: Color = ColorLightTokens.OnSurfaceVariant, surfaceTint: Color = primary, inverseSurface: Color = ColorLightTokens.InverseSurface, inverseOnSurface: Color = ColorLightTokens.InverseOnSurface, error: Color = ColorLightTokens.Error, onError: Color = ColorLightTokens.OnError, errorContainer: Color = ColorLightTokens.ErrorContainer, onErrorContainer: Color = ColorLightTokens.OnErrorContainer, outline: Color = ColorLightTokens.Outline, outlineVariant: Color = ColorLightTokens.OutlineVariant, scrim: Color = ColorLightTokens.Scrim, surfaceBright: Color = ColorLightTokens.SurfaceBright, surfaceContainer: Color = ColorLightTokens.SurfaceContainer, surfaceContainerHigh: Color = ColorLightTokens.SurfaceContainerHigh, surfaceContainerHighest: Color = ColorLightTokens.SurfaceContainerHighest, surfaceContainerLow: Color = ColorLightTokens.SurfaceContainerLow, surfaceContainerLowest: Color = ColorLightTokens.SurfaceContainerLowest, surfaceDim: Color = ColorLightTokens.SurfaceDim, primaryFixed: Color = ColorLightTokens.PrimaryFixed, primaryFixedDim: Color = ColorLightTokens.PrimaryFixedDim, onPrimaryFixed: Color = ColorLightTokens.OnPrimaryFixed, onPrimaryFixedVariant: Color = ColorLightTokens.OnPrimaryFixedVariant, secondaryFixed: Color = ColorLightTokens.SecondaryFixed, secondaryFixedDim: Color = ColorLightTokens.SecondaryFixedDim, onSecondaryFixed: Color = ColorLightTokens.OnSecondaryFixed, onSecondaryFixedVariant: Color = ColorLightTokens.OnSecondaryFixedVariant, tertiaryFixed: Color = ColorLightTokens.TertiaryFixed, tertiaryFixedDim: Color = ColorLightTokens.TertiaryFixedDim, onTertiaryFixed: Color = ColorLightTokens.OnTertiaryFixed, onTertiaryFixedVariant: Color = ColorLightTokens.OnTertiaryFixedVariant, ): ColorScheme = ColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, surfaceDim = surfaceDim, primaryFixed = primaryFixed, primaryFixedDim = primaryFixedDim, onPrimaryFixed = onPrimaryFixed, onPrimaryFixedVariant = onPrimaryFixedVariant, secondaryFixed = secondaryFixed, secondaryFixedDim = secondaryFixedDim, onSecondaryFixed = onSecondaryFixed, onSecondaryFixedVariant = onSecondaryFixedVariant, tertiaryFixed = tertiaryFixed, tertiaryFixedDim = tertiaryFixedDim, onTertiaryFixed = onTertiaryFixed, onTertiaryFixedVariant = onTertiaryFixedVariant, ) /** Returns a dark Material color scheme. */ fun darkColorScheme( primary: Color = ColorDarkTokens.Primary, onPrimary: Color = ColorDarkTokens.OnPrimary, primaryContainer: Color = ColorDarkTokens.PrimaryContainer, onPrimaryContainer: Color = ColorDarkTokens.OnPrimaryContainer, inversePrimary: Color = ColorDarkTokens.InversePrimary, secondary: Color = ColorDarkTokens.Secondary, onSecondary: Color = ColorDarkTokens.OnSecondary, secondaryContainer: Color = ColorDarkTokens.SecondaryContainer, onSecondaryContainer: Color = ColorDarkTokens.OnSecondaryContainer, tertiary: Color = ColorDarkTokens.Tertiary, onTertiary: Color = ColorDarkTokens.OnTertiary, tertiaryContainer: Color = ColorDarkTokens.TertiaryContainer, onTertiaryContainer: Color = ColorDarkTokens.OnTertiaryContainer, background: Color = ColorDarkTokens.Background, onBackground: Color = ColorDarkTokens.OnBackground, surface: Color = ColorDarkTokens.Surface, onSurface: Color = ColorDarkTokens.OnSurface, surfaceVariant: Color = ColorDarkTokens.SurfaceVariant, onSurfaceVariant: Color = ColorDarkTokens.OnSurfaceVariant, surfaceTint: Color = primary, inverseSurface: Color = ColorDarkTokens.InverseSurface, inverseOnSurface: Color = ColorDarkTokens.InverseOnSurface, error: Color = ColorDarkTokens.Error, onError: Color = ColorDarkTokens.OnError, errorContainer: Color = ColorDarkTokens.ErrorContainer, onErrorContainer: Color = ColorDarkTokens.OnErrorContainer, outline: Color = ColorDarkTokens.Outline, outlineVariant: Color = ColorDarkTokens.OutlineVariant, scrim: Color = ColorDarkTokens.Scrim, surfaceBright: Color = ColorDarkTokens.SurfaceBright, surfaceContainer: Color = ColorDarkTokens.SurfaceContainer, surfaceContainerHigh: Color = ColorDarkTokens.SurfaceContainerHigh, surfaceContainerHighest: Color = ColorDarkTokens.SurfaceContainerHighest, surfaceContainerLow: Color = ColorDarkTokens.SurfaceContainerLow, surfaceContainerLowest: Color = ColorDarkTokens.SurfaceContainerLowest, surfaceDim: Color = ColorDarkTokens.SurfaceDim, primaryFixed: Color = ColorDarkTokens.PrimaryFixed, primaryFixedDim: Color = ColorDarkTokens.PrimaryFixedDim, onPrimaryFixed: Color = ColorDarkTokens.OnPrimaryFixed, onPrimaryFixedVariant: Color = ColorDarkTokens.OnPrimaryFixedVariant, secondaryFixed: Color = ColorDarkTokens.SecondaryFixed, secondaryFixedDim: Color = ColorDarkTokens.SecondaryFixedDim, onSecondaryFixed: Color = ColorDarkTokens.OnSecondaryFixed, onSecondaryFixedVariant: Color = ColorDarkTokens.OnSecondaryFixedVariant, tertiaryFixed: Color = ColorDarkTokens.TertiaryFixed, tertiaryFixedDim: Color = ColorDarkTokens.TertiaryFixedDim, onTertiaryFixed: Color = ColorDarkTokens.OnTertiaryFixed, onTertiaryFixedVariant: Color = ColorDarkTokens.OnTertiaryFixedVariant, ): ColorScheme = ColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, surfaceDim = surfaceDim, primaryFixed = primaryFixed, primaryFixedDim = primaryFixedDim, onPrimaryFixed = onPrimaryFixed, onPrimaryFixedVariant = onPrimaryFixedVariant, secondaryFixed = secondaryFixed, secondaryFixedDim = secondaryFixedDim, onSecondaryFixed = onSecondaryFixed, onSecondaryFixedVariant = onSecondaryFixedVariant, tertiaryFixed = tertiaryFixed, tertiaryFixedDim = tertiaryFixedDim, onTertiaryFixed = onTertiaryFixed, onTertiaryFixedVariant = onTertiaryFixedVariant, ) /** * The Material color system contains pairs of colors that are typically used for the background and * content color inside a component. For example, a [Button] typically uses `primary` for its * background, and `onPrimary` for the color of its content (usually text or iconography). * * This function tries to match the provided [backgroundColor] to a 'background' color in this * [ColorScheme], and then will return the corresponding color used for content. For example, when * [backgroundColor] is [ColorScheme.primary], this will return [ColorScheme.onPrimary]. * * If [backgroundColor] does not match a background color in the theme, this will return * [Color.Unspecified]. * * @return the matching content color for [backgroundColor]. If [backgroundColor] is not present in * the theme's [ColorScheme], then returns [Color.Unspecified]. * @see contentColorFor */ @Stable fun ColorScheme.contentColorFor(backgroundColor: Color): Color = when (backgroundColor) { primary -> onPrimary secondary -> onSecondary tertiary -> onTertiary background -> onBackground error -> onError primaryContainer -> onPrimaryContainer secondaryContainer -> onSecondaryContainer tertiaryContainer -> onTertiaryContainer errorContainer -> onErrorContainer inverseSurface -> inverseOnSurface surface -> onSurface surfaceVariant -> onSurfaceVariant surfaceBright -> onSurface surfaceContainer -> onSurface surfaceContainerHigh -> onSurface surfaceContainerHighest -> onSurface surfaceContainerLow -> onSurface surfaceContainerLowest -> onSurface surfaceDim -> onSurface primaryFixed -> onPrimaryFixed primaryFixedDim -> onPrimaryFixed secondaryFixed -> onSecondaryFixed secondaryFixedDim -> onSecondaryFixed tertiaryFixed -> onTertiaryFixed tertiaryFixedDim -> onTertiaryFixed else -> Color.Unspecified } /** * The Material color system contains pairs of colors that are typically used for the background and * content color inside a component. For example, a [Button] typically uses `primary` for its * background, and `onPrimary` for the color of its content (usually text or iconography). * * This function tries to match the provided [backgroundColor] to a 'background' color in this * [ColorScheme], and then will return the corresponding color used for content. For example, when * [backgroundColor] is [ColorScheme.primary], this will return [ColorScheme.onPrimary]. * * If [backgroundColor] does not match a background color in the theme, this will return the current * value of [LocalContentColor] as a best-effort color. * * @return the matching content color for [backgroundColor]. If [backgroundColor] is not present in * the theme's [ColorScheme], then returns the current value of [LocalContentColor]. * @see ColorScheme.contentColorFor */ @Composable @ReadOnlyComposable fun contentColorFor(backgroundColor: Color) = MaterialTheme.colorScheme.contentColorFor(backgroundColor).takeOrElse { LocalContentColor.current } /** * Computes the surface tonal color at different elevation levels e.g. surface1 through surface5. * * @param elevation Elevation value used to compute alpha of the color overlay layer. * @return the [ColorScheme.surface] color with an alpha of the [ColorScheme.surfaceTint] color * overlaid on top of it. */ @Stable fun ColorScheme.surfaceColorAtElevation(elevation: Dp): Color { if (elevation == 0.dp) return surface val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 100f return surfaceTint.copy(alpha = alpha).compositeOver(surface) } /** * Returns a light Material color scheme. * * The default color scheme for [MaterialExpressiveTheme]. For dark mode, use [darkColorScheme]. * * Example of MaterialExpressiveTheme toggling expressiveLightColorScheme and darkTheme. * * @sample androidx.compose.material3.samples.MaterialExpressiveThemeColorSchemeSample */ @ExperimentalMaterial3ExpressiveApi fun expressiveLightColorScheme() = lightColorScheme( // TODO: Replace palette references with color token references when available. onPrimaryContainer = PaletteTokens.Primary30, onSecondaryContainer = PaletteTokens.Secondary30, onTertiaryContainer = PaletteTokens.Tertiary30, onErrorContainer = PaletteTokens.Error30, ) @Deprecated( message = "Maintained for binary compatibility. Use overload with additional Fixed roles instead", level = DeprecationLevel.HIDDEN, ) /** Returns a light Material color scheme. */ fun lightColorScheme( primary: Color = ColorLightTokens.Primary, onPrimary: Color = ColorLightTokens.OnPrimary, primaryContainer: Color = ColorLightTokens.PrimaryContainer, onPrimaryContainer: Color = ColorLightTokens.OnPrimaryContainer, inversePrimary: Color = ColorLightTokens.InversePrimary, secondary: Color = ColorLightTokens.Secondary, onSecondary: Color = ColorLightTokens.OnSecondary, secondaryContainer: Color = ColorLightTokens.SecondaryContainer, onSecondaryContainer: Color = ColorLightTokens.OnSecondaryContainer, tertiary: Color = ColorLightTokens.Tertiary, onTertiary: Color = ColorLightTokens.OnTertiary, tertiaryContainer: Color = ColorLightTokens.TertiaryContainer, onTertiaryContainer: Color = ColorLightTokens.OnTertiaryContainer, background: Color = ColorLightTokens.Background, onBackground: Color = ColorLightTokens.OnBackground, surface: Color = ColorLightTokens.Surface, onSurface: Color = ColorLightTokens.OnSurface, surfaceVariant: Color = ColorLightTokens.SurfaceVariant, onSurfaceVariant: Color = ColorLightTokens.OnSurfaceVariant, surfaceTint: Color = primary, inverseSurface: Color = ColorLightTokens.InverseSurface, inverseOnSurface: Color = ColorLightTokens.InverseOnSurface, error: Color = ColorLightTokens.Error, onError: Color = ColorLightTokens.OnError, errorContainer: Color = ColorLightTokens.ErrorContainer, onErrorContainer: Color = ColorLightTokens.OnErrorContainer, outline: Color = ColorLightTokens.Outline, outlineVariant: Color = ColorLightTokens.OutlineVariant, scrim: Color = ColorLightTokens.Scrim, surfaceBright: Color = ColorLightTokens.SurfaceBright, surfaceContainer: Color = ColorLightTokens.SurfaceContainer, surfaceContainerHigh: Color = ColorLightTokens.SurfaceContainerHigh, surfaceContainerHighest: Color = ColorLightTokens.SurfaceContainerHighest, surfaceContainerLow: Color = ColorLightTokens.SurfaceContainerLow, surfaceContainerLowest: Color = ColorLightTokens.SurfaceContainerLowest, surfaceDim: Color = ColorLightTokens.SurfaceDim, ): ColorScheme = lightColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, surfaceDim = surfaceDim, ) @Deprecated( message = "Maintained for binary compatibility. Use overload with additional surface roles instead", level = DeprecationLevel.HIDDEN, ) fun lightColorScheme( primary: Color = ColorLightTokens.Primary, onPrimary: Color = ColorLightTokens.OnPrimary, primaryContainer: Color = ColorLightTokens.PrimaryContainer, onPrimaryContainer: Color = ColorLightTokens.OnPrimaryContainer, inversePrimary: Color = ColorLightTokens.InversePrimary, secondary: Color = ColorLightTokens.Secondary, onSecondary: Color = ColorLightTokens.OnSecondary, secondaryContainer: Color = ColorLightTokens.SecondaryContainer, onSecondaryContainer: Color = ColorLightTokens.OnSecondaryContainer, tertiary: Color = ColorLightTokens.Tertiary, onTertiary: Color = ColorLightTokens.OnTertiary, tertiaryContainer: Color = ColorLightTokens.TertiaryContainer, onTertiaryContainer: Color = ColorLightTokens.OnTertiaryContainer, background: Color = ColorLightTokens.Background, onBackground: Color = ColorLightTokens.OnBackground, surface: Color = ColorLightTokens.Surface, onSurface: Color = ColorLightTokens.OnSurface, surfaceVariant: Color = ColorLightTokens.SurfaceVariant, onSurfaceVariant: Color = ColorLightTokens.OnSurfaceVariant, surfaceTint: Color = primary, inverseSurface: Color = ColorLightTokens.InverseSurface, inverseOnSurface: Color = ColorLightTokens.InverseOnSurface, error: Color = ColorLightTokens.Error, onError: Color = ColorLightTokens.OnError, errorContainer: Color = ColorLightTokens.ErrorContainer, onErrorContainer: Color = ColorLightTokens.OnErrorContainer, outline: Color = ColorLightTokens.Outline, outlineVariant: Color = ColorLightTokens.OutlineVariant, scrim: Color = ColorLightTokens.Scrim, ): ColorScheme = lightColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, ) /** Returns a dark Material color scheme. */ @Deprecated( message = "Maintained for binary compatibility. Use overload with additional surface roles instead", level = DeprecationLevel.HIDDEN, ) fun darkColorScheme( primary: Color = ColorDarkTokens.Primary, onPrimary: Color = ColorDarkTokens.OnPrimary, primaryContainer: Color = ColorDarkTokens.PrimaryContainer, onPrimaryContainer: Color = ColorDarkTokens.OnPrimaryContainer, inversePrimary: Color = ColorDarkTokens.InversePrimary, secondary: Color = ColorDarkTokens.Secondary, onSecondary: Color = ColorDarkTokens.OnSecondary, secondaryContainer: Color = ColorDarkTokens.SecondaryContainer, onSecondaryContainer: Color = ColorDarkTokens.OnSecondaryContainer, tertiary: Color = ColorDarkTokens.Tertiary, onTertiary: Color = ColorDarkTokens.OnTertiary, tertiaryContainer: Color = ColorDarkTokens.TertiaryContainer, onTertiaryContainer: Color = ColorDarkTokens.OnTertiaryContainer, background: Color = ColorDarkTokens.Background, onBackground: Color = ColorDarkTokens.OnBackground, surface: Color = ColorDarkTokens.Surface, onSurface: Color = ColorDarkTokens.OnSurface, surfaceVariant: Color = ColorDarkTokens.SurfaceVariant, onSurfaceVariant: Color = ColorDarkTokens.OnSurfaceVariant, surfaceTint: Color = primary, inverseSurface: Color = ColorDarkTokens.InverseSurface, inverseOnSurface: Color = ColorDarkTokens.InverseOnSurface, error: Color = ColorDarkTokens.Error, onError: Color = ColorDarkTokens.OnError, errorContainer: Color = ColorDarkTokens.ErrorContainer, onErrorContainer: Color = ColorDarkTokens.OnErrorContainer, outline: Color = ColorDarkTokens.Outline, outlineVariant: Color = ColorDarkTokens.OutlineVariant, scrim: Color = ColorDarkTokens.Scrim, surfaceBright: Color = ColorDarkTokens.SurfaceBright, surfaceContainer: Color = ColorDarkTokens.SurfaceContainer, surfaceContainerHigh: Color = ColorDarkTokens.SurfaceContainerHigh, surfaceContainerHighest: Color = ColorDarkTokens.SurfaceContainerHighest, surfaceContainerLow: Color = ColorDarkTokens.SurfaceContainerLow, surfaceContainerLowest: Color = ColorDarkTokens.SurfaceContainerLowest, surfaceDim: Color = ColorDarkTokens.SurfaceDim, ): ColorScheme = darkColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, surfaceBright = surfaceBright, surfaceContainer = surfaceContainer, surfaceContainerHigh = surfaceContainerHigh, surfaceContainerHighest = surfaceContainerHighest, surfaceContainerLow = surfaceContainerLow, surfaceContainerLowest = surfaceContainerLowest, surfaceDim = surfaceDim, ) @Deprecated( message = "Maintained for binary compatibility. Use overload with additional surface roles instead", level = DeprecationLevel.HIDDEN, ) fun darkColorScheme( primary: Color = ColorDarkTokens.Primary, onPrimary: Color = ColorDarkTokens.OnPrimary, primaryContainer: Color = ColorDarkTokens.PrimaryContainer, onPrimaryContainer: Color = ColorDarkTokens.OnPrimaryContainer, inversePrimary: Color = ColorDarkTokens.InversePrimary, secondary: Color = ColorDarkTokens.Secondary, onSecondary: Color = ColorDarkTokens.OnSecondary, secondaryContainer: Color = ColorDarkTokens.SecondaryContainer, onSecondaryContainer: Color = ColorDarkTokens.OnSecondaryContainer, tertiary: Color = ColorDarkTokens.Tertiary, onTertiary: Color = ColorDarkTokens.OnTertiary, tertiaryContainer: Color = ColorDarkTokens.TertiaryContainer, onTertiaryContainer: Color = ColorDarkTokens.OnTertiaryContainer, background: Color = ColorDarkTokens.Background, onBackground: Color = ColorDarkTokens.OnBackground, surface: Color = ColorDarkTokens.Surface, onSurface: Color = ColorDarkTokens.OnSurface, surfaceVariant: Color = ColorDarkTokens.SurfaceVariant, onSurfaceVariant: Color = ColorDarkTokens.OnSurfaceVariant, surfaceTint: Color = primary, inverseSurface: Color = ColorDarkTokens.InverseSurface, inverseOnSurface: Color = ColorDarkTokens.InverseOnSurface, error: Color = ColorDarkTokens.Error, onError: Color = ColorDarkTokens.OnError, errorContainer: Color = ColorDarkTokens.ErrorContainer, onErrorContainer: Color = ColorDarkTokens.OnErrorContainer, outline: Color = ColorDarkTokens.Outline, outlineVariant: Color = ColorDarkTokens.OutlineVariant, scrim: Color = ColorDarkTokens.Scrim, ): ColorScheme = darkColorScheme( primary = primary, onPrimary = onPrimary, primaryContainer = primaryContainer, onPrimaryContainer = onPrimaryContainer, inversePrimary = inversePrimary, secondary = secondary, onSecondary = onSecondary, secondaryContainer = secondaryContainer, onSecondaryContainer = onSecondaryContainer, tertiary = tertiary, onTertiary = onTertiary, tertiaryContainer = tertiaryContainer, onTertiaryContainer = onTertiaryContainer, background = background, onBackground = onBackground, surface = surface, onSurface = onSurface, surfaceVariant = surfaceVariant, onSurfaceVariant = onSurfaceVariant, surfaceTint = surfaceTint, inverseSurface = inverseSurface, inverseOnSurface = inverseOnSurface, error = error, onError = onError, errorContainer = errorContainer, onErrorContainer = onErrorContainer, outline = outline, outlineVariant = outlineVariant, scrim = scrim, ) /** * Helper function for component color tokens. Here is an example on how to use component color * tokens: ``MaterialTheme.colorScheme.fromToken(ExtendedFabBranded.BrandedContainerColor)`` */ @Stable internal fun ColorScheme.fromToken(value: ColorSchemeKeyTokens): Color { return when (value) { ColorSchemeKeyTokens.Background -> background ColorSchemeKeyTokens.Error -> error ColorSchemeKeyTokens.ErrorContainer -> errorContainer ColorSchemeKeyTokens.InverseOnSurface -> inverseOnSurface ColorSchemeKeyTokens.InversePrimary -> inversePrimary ColorSchemeKeyTokens.InverseSurface -> inverseSurface ColorSchemeKeyTokens.OnBackground -> onBackground ColorSchemeKeyTokens.OnError -> onError ColorSchemeKeyTokens.OnErrorContainer -> onErrorContainer ColorSchemeKeyTokens.OnPrimary -> onPrimary ColorSchemeKeyTokens.OnPrimaryContainer -> onPrimaryContainer ColorSchemeKeyTokens.OnSecondary -> onSecondary ColorSchemeKeyTokens.OnSecondaryContainer -> onSecondaryContainer ColorSchemeKeyTokens.OnSurface -> onSurface ColorSchemeKeyTokens.OnSurfaceVariant -> onSurfaceVariant ColorSchemeKeyTokens.SurfaceTint -> surfaceTint ColorSchemeKeyTokens.OnTertiary -> onTertiary ColorSchemeKeyTokens.OnTertiaryContainer -> onTertiaryContainer ColorSchemeKeyTokens.Outline -> outline ColorSchemeKeyTokens.OutlineVariant -> outlineVariant ColorSchemeKeyTokens.Primary -> primary ColorSchemeKeyTokens.PrimaryContainer -> primaryContainer ColorSchemeKeyTokens.Scrim -> scrim ColorSchemeKeyTokens.Secondary -> secondary ColorSchemeKeyTokens.SecondaryContainer -> secondaryContainer ColorSchemeKeyTokens.Surface -> surface ColorSchemeKeyTokens.SurfaceVariant -> surfaceVariant ColorSchemeKeyTokens.SurfaceBright -> surfaceBright ColorSchemeKeyTokens.SurfaceContainer -> surfaceContainer ColorSchemeKeyTokens.SurfaceContainerHigh -> surfaceContainerHigh ColorSchemeKeyTokens.SurfaceContainerHighest -> surfaceContainerHighest ColorSchemeKeyTokens.SurfaceContainerLow -> surfaceContainerLow ColorSchemeKeyTokens.SurfaceContainerLowest -> surfaceContainerLowest ColorSchemeKeyTokens.SurfaceDim -> surfaceDim ColorSchemeKeyTokens.Tertiary -> tertiary ColorSchemeKeyTokens.TertiaryContainer -> tertiaryContainer ColorSchemeKeyTokens.PrimaryFixed -> primaryFixed ColorSchemeKeyTokens.PrimaryFixedDim -> primaryFixedDim ColorSchemeKeyTokens.OnPrimaryFixed -> onPrimaryFixed ColorSchemeKeyTokens.OnPrimaryFixedVariant -> onPrimaryFixedVariant ColorSchemeKeyTokens.SecondaryFixed -> secondaryFixed ColorSchemeKeyTokens.SecondaryFixedDim -> secondaryFixedDim ColorSchemeKeyTokens.OnSecondaryFixed -> onSecondaryFixed ColorSchemeKeyTokens.OnSecondaryFixedVariant -> onSecondaryFixedVariant ColorSchemeKeyTokens.TertiaryFixed -> tertiaryFixed ColorSchemeKeyTokens.TertiaryFixedDim -> tertiaryFixedDim ColorSchemeKeyTokens.OnTertiaryFixed -> onTertiaryFixed ColorSchemeKeyTokens.OnTertiaryFixedVariant -> onTertiaryFixedVariant } } /** * A low level of alpha used to represent disabled components, such as text in a disabled Button. */ internal const val DisabledAlpha = 0.38f /** * Converts a color token key to the local color scheme provided by the theme The color is * subscribed to [MaterialTheme.colorScheme] changes. */ internal val ColorSchemeKeyTokens.value: Color @ReadOnlyComposable @Composable get() = MaterialTheme.colorScheme.fromToken(this) /** * Returns [ColorScheme.surfaceColorAtElevation] with the provided elevation if * [LocalTonalElevationEnabled] is set to true, and the provided background color matches * [ColorScheme.surface]. Otherwise, the provided color is returned unchanged. * * @param backgroundColor The background color to compare to [ColorScheme.surface] * @param elevation The elevation provided to [ColorScheme.surfaceColorAtElevation] if * [backgroundColor] matches surface. * @return [ColorScheme.surfaceColorAtElevation] at [elevation] if [backgroundColor] == * [ColorScheme.surface] and [LocalTonalElevationEnabled] is set to true. Else [backgroundColor] */ @Composable @ReadOnlyComposable internal fun ColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color { val tonalElevationEnabled = LocalTonalElevationEnabled.current return if (backgroundColor == surface && tonalElevationEnabled) { surfaceColorAtElevation(elevation) } else { backgroundColor } } /** * Composition Local used to check if [ColorScheme.applyTonalElevation] will be applied down the * tree. * * Setting this value to false will cause all subsequent surfaces down the tree to not apply * tonalElevation. */ val LocalTonalElevationEnabled = staticCompositionLocalOf { true } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Typography.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.material3.tokens.TypographyKeyTokens import androidx.compose.material3.tokens.TypographyTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.text.TextStyle /** * The Material Design type scale includes a range of contrasting styles that support the needs of * your product and its content. * * Use typography to make writing legible and beautiful. Material's default type scale includes * contrasting and flexible styles to support a wide range of use cases. * * The type scale is a combination of thirteen styles that are supported by the type system. It * contains reusable categories of text, each with an intended application and meaning. * * The emphasized versions of the baseline styles add dynamism and personality to the baseline * styles. It can be used to further stylize select pieces of text. The emphasized states have * pragmatic uses, such as creating clearer division of content and drawing users' eyes to relevant * material. * * To learn more about typography, see * [Material Design typography](https://m3.material.io/styles/typography/overview). * * @property displayLarge displayLarge is the largest display text. * @property displayMedium displayMedium is the second largest display text. * @property displaySmall displaySmall is the smallest display text. * @property headlineLarge headlineLarge is the largest headline, reserved for short, important text * or numerals. For headlines, you can choose an expressive font, such as a display, handwritten, * or script style. These unconventional font designs have details and intricacy that help attract * the eye. * @property headlineMedium headlineMedium is the second largest headline, reserved for short, * important text or numerals. For headlines, you can choose an expressive font, such as a * display, handwritten, or script style. These unconventional font designs have details and * intricacy that help attract the eye. * @property headlineSmall headlineSmall is the smallest headline, reserved for short, important * text or numerals. For headlines, you can choose an expressive font, such as a display, * handwritten, or script style. These unconventional font designs have details and intricacy that * help attract the eye. * @property titleLarge titleLarge is the largest title, and is typically reserved for * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for * subtitles. * @property titleMedium titleMedium is the second largest title, and is typically reserved for * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for * subtitles. * @property titleSmall titleSmall is the smallest title, and is typically reserved for * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for * subtitles. * @property bodyLarge bodyLarge is the largest body, and is typically used for long-form writing as * it works well for small text sizes. For longer sections of text, a serif or sans serif typeface * is recommended. * @property bodyMedium bodyMedium is the second largest body, and is typically used for long-form * writing as it works well for small text sizes. For longer sections of text, a serif or sans * serif typeface is recommended. * @property bodySmall bodySmall is the smallest body, and is typically used for long-form writing * as it works well for small text sizes. For longer sections of text, a serif or sans serif * typeface is recommended. * @property labelLarge labelLarge text is a call to action used in different types of buttons (such * as text, outlined and contained buttons) and in tabs, dialogs, and cards. Button text is * typically sans serif, using all caps text. * @property labelMedium labelMedium is one of the smallest font sizes. It is used sparingly to * annotate imagery or to introduce a headline. * @property labelSmall labelSmall is one of the smallest font sizes. It is used sparingly to * annotate imagery or to introduce a headline. * @property displayLargeEmphasized an emphasized version of [displayLarge]. * @property displayMediumEmphasized an emphasized version of [displayMedium]. * @property displaySmallEmphasized an emphasized version of [displaySmall]. * @property headlineLargeEmphasized an emphasized version of [headlineLarge]. * @property headlineMediumEmphasized an emphasized version of [headlineMedium]. * @property headlineSmallEmphasized an emphasized version of [headlineSmall]. * @property titleLargeEmphasized an emphasized version of [titleLarge]. * @property titleMediumEmphasized an emphasized version of [titleMedium]. * @property titleSmallEmphasized an emphasized version of [titleSmall]. * @property bodyLargeEmphasized an emphasized version of [bodyLarge]. * @property bodyMediumEmphasized an emphasized version of [bodyMedium]. * @property bodySmallEmphasized an emphasized version of [bodySmall]. * @property labelLargeEmphasized an emphasized version of [labelLarge]. * @property labelMediumEmphasized an emphasized version of [labelMedium]. * @property labelSmallEmphasized an emphasized version of [labelSmall]. */ @Immutable class Typography @ExperimentalMaterial3ExpressiveApi constructor( val displayLarge: TextStyle = TypographyTokens.DisplayLarge, val displayMedium: TextStyle = TypographyTokens.DisplayMedium, val displaySmall: TextStyle = TypographyTokens.DisplaySmall, val headlineLarge: TextStyle = TypographyTokens.HeadlineLarge, val headlineMedium: TextStyle = TypographyTokens.HeadlineMedium, val headlineSmall: TextStyle = TypographyTokens.HeadlineSmall, val titleLarge: TextStyle = TypographyTokens.TitleLarge, val titleMedium: TextStyle = TypographyTokens.TitleMedium, val titleSmall: TextStyle = TypographyTokens.TitleSmall, val bodyLarge: TextStyle = TypographyTokens.BodyLarge, val bodyMedium: TextStyle = TypographyTokens.BodyMedium, val bodySmall: TextStyle = TypographyTokens.BodySmall, val labelLarge: TextStyle = TypographyTokens.LabelLarge, val labelMedium: TextStyle = TypographyTokens.LabelMedium, val labelSmall: TextStyle = TypographyTokens.LabelSmall, displayLargeEmphasized: TextStyle = TypographyTokens.DisplayLargeEmphasized, displayMediumEmphasized: TextStyle = TypographyTokens.DisplayMediumEmphasized, displaySmallEmphasized: TextStyle = TypographyTokens.DisplaySmallEmphasized, headlineLargeEmphasized: TextStyle = TypographyTokens.HeadlineLargeEmphasized, headlineMediumEmphasized: TextStyle = TypographyTokens.HeadlineMediumEmphasized, headlineSmallEmphasized: TextStyle = TypographyTokens.HeadlineSmallEmphasized, titleLargeEmphasized: TextStyle = TypographyTokens.TitleLargeEmphasized, titleMediumEmphasized: TextStyle = TypographyTokens.TitleMediumEmphasized, titleSmallEmphasized: TextStyle = TypographyTokens.TitleSmallEmphasized, bodyLargeEmphasized: TextStyle = TypographyTokens.BodyLargeEmphasized, bodyMediumEmphasized: TextStyle = TypographyTokens.BodyMediumEmphasized, bodySmallEmphasized: TextStyle = TypographyTokens.BodySmallEmphasized, labelLargeEmphasized: TextStyle = TypographyTokens.LabelLargeEmphasized, labelMediumEmphasized: TextStyle = TypographyTokens.LabelMediumEmphasized, labelSmallEmphasized: TextStyle = TypographyTokens.LabelSmallEmphasized, ) { @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [displayLarge]. */ val displayLargeEmphasized = displayLargeEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [displayMedium]. */ val displayMediumEmphasized = displayMediumEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [displaySmall]. */ val displaySmallEmphasized = displaySmallEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [headlineLarge]. */ val headlineLargeEmphasized = headlineLargeEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [headlineMedium]. */ val headlineMediumEmphasized = headlineMediumEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [headlineSmall]. */ val headlineSmallEmphasized = headlineSmallEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [titleLarge]. */ val titleLargeEmphasized = titleLargeEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [titleMedium]. */ val titleMediumEmphasized = titleMediumEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [titleSmall]. */ val titleSmallEmphasized = titleSmallEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [bodyLarge]. */ val bodyLargeEmphasized = bodyLargeEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [bodyMedium]. */ val bodyMediumEmphasized = bodyMediumEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [bodySmall]. */ val bodySmallEmphasized = bodySmallEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [labelLarge]. */ val labelLargeEmphasized = labelLargeEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [labelMedium]. */ val labelMediumEmphasized = labelMediumEmphasized @ExperimentalMaterial3ExpressiveApi /** an emphasized version of [labelSmall]. */ val labelSmallEmphasized = labelSmallEmphasized /** * The Material Design type scale includes a range of contrasting styles that support the needs * of your product and its content. * * Use typography to make writing legible and beautiful. Material's default type scale includes * contrasting and flexible styles to support a wide range of use cases. * * The type scale is a combination of thirteen styles that are supported by the type system. It * contains reusable categories of text, each with an intended application and meaning. * * To learn more about typography, see * [Material Design typography](https://m3.material.io/styles/typography/overview). * * @param displayLarge displayLarge is the largest display text. * @param displayMedium displayMedium is the second largest display text. * @param displaySmall displaySmall is the smallest display text. * @param headlineLarge headlineLarge is the largest headline, reserved for short, important * text or numerals. For headlines, you can choose an expressive font, such as a display, * handwritten, or script style. These unconventional font designs have details and intricacy * that help attract the eye. * @param headlineMedium headlineMedium is the second largest headline, reserved for short, * important text or numerals. For headlines, you can choose an expressive font, such as a * display, handwritten, or script style. These unconventional font designs have details and * intricacy that help attract the eye. * @param headlineSmall headlineSmall is the smallest headline, reserved for short, important * text or numerals. For headlines, you can choose an expressive font, such as a display, * handwritten, or script style. These unconventional font designs have details and intricacy * that help attract the eye. * @param titleLarge titleLarge is the largest title, and is typically reserved for * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for * subtitles. * @param titleMedium titleMedium is the second largest title, and is typically reserved for * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for * subtitles. * @param titleSmall titleSmall is the smallest title, and is typically reserved for * medium-emphasis text that is shorter in length. Serif or sans serif typefaces work well for * subtitles. * @param bodyLarge bodyLarge is the largest body, and is typically used for long-form writing * as it works well for small text sizes. For longer sections of text, a serif or sans serif * typeface is recommended. * @param bodyMedium bodyMedium is the second largest body, and is typically used for long-form * writing as it works well for small text sizes. For longer sections of text, a serif or sans * serif typeface is recommended. * @param bodySmall bodySmall is the smallest body, and is typically used for long-form writing * as it works well for small text sizes. For longer sections of text, a serif or sans serif * typeface is recommended. * @param labelLarge labelLarge text is a call to action used in different types of buttons * (such as text, outlined and contained buttons) and in tabs, dialogs, and cards. Button text * is typically sans serif, using all caps text. * @param labelMedium labelMedium is one of the smallest font sizes. It is used sparingly to * annotate imagery or to introduce a headline. * @param labelSmall labelSmall is one of the smallest font sizes. It is used sparingly to * annotate imagery or to introduce a headline. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) constructor( displayLarge: TextStyle = TypographyTokens.DisplayLarge, displayMedium: TextStyle = TypographyTokens.DisplayMedium, displaySmall: TextStyle = TypographyTokens.DisplaySmall, headlineLarge: TextStyle = TypographyTokens.HeadlineLarge, headlineMedium: TextStyle = TypographyTokens.HeadlineMedium, headlineSmall: TextStyle = TypographyTokens.HeadlineSmall, titleLarge: TextStyle = TypographyTokens.TitleLarge, titleMedium: TextStyle = TypographyTokens.TitleMedium, titleSmall: TextStyle = TypographyTokens.TitleSmall, bodyLarge: TextStyle = TypographyTokens.BodyLarge, bodyMedium: TextStyle = TypographyTokens.BodyMedium, bodySmall: TextStyle = TypographyTokens.BodySmall, labelLarge: TextStyle = TypographyTokens.LabelLarge, labelMedium: TextStyle = TypographyTokens.LabelMedium, labelSmall: TextStyle = TypographyTokens.LabelSmall, ) : this( displayLarge = displayLarge, displayMedium = displayMedium, displaySmall = displaySmall, headlineLarge = headlineLarge, headlineMedium = headlineMedium, headlineSmall = headlineSmall, titleLarge = titleLarge, titleMedium = titleMedium, titleSmall = titleSmall, bodyLarge = bodyLarge, bodyMedium = bodyMedium, bodySmall = bodySmall, labelLarge = labelLarge, labelMedium = labelMedium, labelSmall = labelSmall, displayLargeEmphasized = displayLarge, displayMediumEmphasized = displayMedium, displaySmallEmphasized = displaySmall, headlineLargeEmphasized = headlineLarge, headlineMediumEmphasized = headlineMedium, headlineSmallEmphasized = headlineSmall, titleLargeEmphasized = titleLarge, titleMediumEmphasized = titleMedium, titleSmallEmphasized = titleSmall, bodyLargeEmphasized = bodyLarge, bodyMediumEmphasized = bodyMedium, bodySmallEmphasized = bodySmall, labelLargeEmphasized = labelLarge, labelMediumEmphasized = labelMedium, labelSmallEmphasized = labelSmall, ) /** Returns a copy of this Typography, optionally overriding some of the values. */ @ExperimentalMaterial3ExpressiveApi fun copy( displayLarge: TextStyle = this.displayLarge, displayMedium: TextStyle = this.displayMedium, displaySmall: TextStyle = this.displaySmall, headlineLarge: TextStyle = this.headlineLarge, headlineMedium: TextStyle = this.headlineMedium, headlineSmall: TextStyle = this.headlineSmall, titleLarge: TextStyle = this.titleLarge, titleMedium: TextStyle = this.titleMedium, titleSmall: TextStyle = this.titleSmall, bodyLarge: TextStyle = this.bodyLarge, bodyMedium: TextStyle = this.bodyMedium, bodySmall: TextStyle = this.bodySmall, labelLarge: TextStyle = this.labelLarge, labelMedium: TextStyle = this.labelMedium, labelSmall: TextStyle = this.labelSmall, displayLargeEmphasized: TextStyle = this.displayLargeEmphasized, displayMediumEmphasized: TextStyle = this.displayMediumEmphasized, displaySmallEmphasized: TextStyle = this.displaySmallEmphasized, headlineLargeEmphasized: TextStyle = this.headlineLargeEmphasized, headlineMediumEmphasized: TextStyle = this.headlineMediumEmphasized, headlineSmallEmphasized: TextStyle = this.headlineSmallEmphasized, titleLargeEmphasized: TextStyle = this.titleLargeEmphasized, titleMediumEmphasized: TextStyle = this.titleMediumEmphasized, titleSmallEmphasized: TextStyle = this.titleSmallEmphasized, bodyLargeEmphasized: TextStyle = this.bodyLargeEmphasized, bodyMediumEmphasized: TextStyle = this.bodyMediumEmphasized, bodySmallEmphasized: TextStyle = this.bodySmallEmphasized, labelLargeEmphasized: TextStyle = this.labelLargeEmphasized, labelMediumEmphasized: TextStyle = this.labelMediumEmphasized, labelSmallEmphasized: TextStyle = this.labelSmallEmphasized, ): Typography = Typography( displayLarge = displayLarge, displayMedium = displayMedium, displaySmall = displaySmall, headlineLarge = headlineLarge, headlineMedium = headlineMedium, headlineSmall = headlineSmall, titleLarge = titleLarge, titleMedium = titleMedium, titleSmall = titleSmall, bodyLarge = bodyLarge, bodyMedium = bodyMedium, bodySmall = bodySmall, labelLarge = labelLarge, labelMedium = labelMedium, labelSmall = labelSmall, displayLargeEmphasized = displayLargeEmphasized, displayMediumEmphasized = displayMediumEmphasized, displaySmallEmphasized = displaySmallEmphasized, headlineLargeEmphasized = headlineLargeEmphasized, headlineMediumEmphasized = headlineMediumEmphasized, headlineSmallEmphasized = headlineSmallEmphasized, titleLargeEmphasized = titleLargeEmphasized, titleMediumEmphasized = titleMediumEmphasized, titleSmallEmphasized = titleSmallEmphasized, bodyLargeEmphasized = bodyLargeEmphasized, bodyMediumEmphasized = bodyMediumEmphasized, bodySmallEmphasized = bodySmallEmphasized, labelLargeEmphasized = labelLargeEmphasized, labelMediumEmphasized = labelMediumEmphasized, labelSmallEmphasized = labelSmallEmphasized, ) /** Returns a copy of this Typography, optionally overriding some of the values. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun copy( displayLarge: TextStyle = this.displayLarge, displayMedium: TextStyle = this.displayMedium, displaySmall: TextStyle = this.displaySmall, headlineLarge: TextStyle = this.headlineLarge, headlineMedium: TextStyle = this.headlineMedium, headlineSmall: TextStyle = this.headlineSmall, titleLarge: TextStyle = this.titleLarge, titleMedium: TextStyle = this.titleMedium, titleSmall: TextStyle = this.titleSmall, bodyLarge: TextStyle = this.bodyLarge, bodyMedium: TextStyle = this.bodyMedium, bodySmall: TextStyle = this.bodySmall, labelLarge: TextStyle = this.labelLarge, labelMedium: TextStyle = this.labelMedium, labelSmall: TextStyle = this.labelSmall, ): Typography = copy( displayLarge = displayLarge, displayMedium = displayMedium, displaySmall = displaySmall, headlineLarge = headlineLarge, headlineMedium = headlineMedium, headlineSmall = headlineSmall, titleLarge = titleLarge, titleMedium = titleMedium, titleSmall = titleSmall, bodyLarge = bodyLarge, bodyMedium = bodyMedium, bodySmall = bodySmall, labelLarge = labelLarge, labelMedium = labelMedium, labelSmall = labelSmall, displayLargeEmphasized = this.displayLargeEmphasized, displayMediumEmphasized = this.displayMediumEmphasized, displaySmallEmphasized = this.displaySmallEmphasized, headlineLargeEmphasized = this.headlineLargeEmphasized, headlineMediumEmphasized = this.headlineMediumEmphasized, headlineSmallEmphasized = this.headlineSmallEmphasized, titleLargeEmphasized = this.titleLargeEmphasized, titleMediumEmphasized = this.titleMediumEmphasized, titleSmallEmphasized = this.titleSmallEmphasized, bodyLargeEmphasized = this.bodyLargeEmphasized, bodyMediumEmphasized = this.bodyMediumEmphasized, bodySmallEmphasized = this.bodySmallEmphasized, labelLargeEmphasized = this.labelLargeEmphasized, labelMediumEmphasized = this.labelMediumEmphasized, labelSmallEmphasized = this.labelSmallEmphasized, ) @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Typography) return false if (displayLarge != other.displayLarge) return false if (displayMedium != other.displayMedium) return false if (displaySmall != other.displaySmall) return false if (headlineLarge != other.headlineLarge) return false if (headlineMedium != other.headlineMedium) return false if (headlineSmall != other.headlineSmall) return false if (titleLarge != other.titleLarge) return false if (titleMedium != other.titleMedium) return false if (titleSmall != other.titleSmall) return false if (bodyLarge != other.bodyLarge) return false if (bodyMedium != other.bodyMedium) return false if (bodySmall != other.bodySmall) return false if (labelLarge != other.labelLarge) return false if (labelMedium != other.labelMedium) return false if (labelSmall != other.labelSmall) return false if (displayLargeEmphasized != other.displayLargeEmphasized) return false if (displayMediumEmphasized != other.displayMediumEmphasized) return false if (displaySmallEmphasized != other.displaySmallEmphasized) return false if (headlineLargeEmphasized != other.headlineLargeEmphasized) return false if (headlineMediumEmphasized != other.headlineMediumEmphasized) return false if (headlineSmallEmphasized != other.headlineSmallEmphasized) return false if (titleLargeEmphasized != other.titleLargeEmphasized) return false if (titleMediumEmphasized != other.titleMediumEmphasized) return false if (titleSmallEmphasized != other.titleSmallEmphasized) return false if (bodyLargeEmphasized != other.bodyLargeEmphasized) return false if (bodyMediumEmphasized != other.bodyMediumEmphasized) return false if (bodySmallEmphasized != other.bodySmallEmphasized) return false if (labelLargeEmphasized != other.labelLargeEmphasized) return false if (labelMediumEmphasized != other.labelMediumEmphasized) return false if (labelSmallEmphasized != other.labelSmallEmphasized) return false return true } @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun hashCode(): Int { var result = displayLarge.hashCode() result = 31 * result + displayMedium.hashCode() result = 31 * result + displaySmall.hashCode() result = 31 * result + headlineLarge.hashCode() result = 31 * result + headlineMedium.hashCode() result = 31 * result + headlineSmall.hashCode() result = 31 * result + titleLarge.hashCode() result = 31 * result + titleMedium.hashCode() result = 31 * result + titleSmall.hashCode() result = 31 * result + bodyLarge.hashCode() result = 31 * result + bodyMedium.hashCode() result = 31 * result + bodySmall.hashCode() result = 31 * result + labelLarge.hashCode() result = 31 * result + labelMedium.hashCode() result = 31 * result + labelSmall.hashCode() result = 31 * result + displayLargeEmphasized.hashCode() result = 31 * result + displayMediumEmphasized.hashCode() result = 31 * result + displaySmallEmphasized.hashCode() result = 31 * result + headlineLargeEmphasized.hashCode() result = 31 * result + headlineMediumEmphasized.hashCode() result = 31 * result + headlineSmallEmphasized.hashCode() result = 31 * result + titleLargeEmphasized.hashCode() result = 31 * result + titleMediumEmphasized.hashCode() result = 31 * result + titleSmallEmphasized.hashCode() result = 31 * result + bodyLargeEmphasized.hashCode() result = 31 * result + bodyMediumEmphasized.hashCode() result = 31 * result + bodySmallEmphasized.hashCode() result = 31 * result + labelLargeEmphasized.hashCode() result = 31 * result + labelMediumEmphasized.hashCode() result = 31 * result + labelSmallEmphasized.hashCode() return result } @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun toString(): String { return "Typography(displayLarge=$displayLarge, displayMedium=$displayMedium," + "displaySmall=$displaySmall, " + "headlineLarge=$headlineLarge, headlineMedium=$headlineMedium," + " headlineSmall=$headlineSmall, " + "titleLarge=$titleLarge, titleMedium=$titleMedium, titleSmall=$titleSmall, " + "bodyLarge=$bodyLarge, bodyMedium=$bodyMedium, bodySmall=$bodySmall, " + "labelLarge=$labelLarge, labelMedium=$labelMedium, labelSmall=$labelSmall, " + "displayLargeEmphasized=$displayLargeEmphasized, " + "displayMediumEmphasized=$displayMediumEmphasized, " + "displaySmallEmphasized=$displaySmallEmphasized, " + "headlineLargeEmphasized=$headlineLargeEmphasized, " + "headlineMediumEmphasized=$headlineMediumEmphasized, " + "headlineSmallEmphasized=$headlineSmallEmphasized, " + "titleLargeEmphasized=$titleLargeEmphasized, " + "titleMediumEmphasized=$titleMediumEmphasized, " + "titleSmallEmphasized=$titleSmallEmphasized, " + "bodyLargeEmphasized=$bodyLargeEmphasized, " + "bodyMediumEmphasized=$bodyMediumEmphasized, " + "bodySmallEmphasized=$bodySmallEmphasized, " + "labelLargeEmphasized=$labelLargeEmphasized, " + "labelMediumEmphasized=$labelMediumEmphasized, " + "labelSmallEmphasized=$labelSmallEmphasized)" } } /** Helper function for component typography tokens. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal fun Typography.fromToken(value: TypographyKeyTokens): TextStyle { return when (value) { TypographyKeyTokens.DisplayLarge -> displayLarge TypographyKeyTokens.DisplayMedium -> displayMedium TypographyKeyTokens.DisplaySmall -> displaySmall TypographyKeyTokens.HeadlineLarge -> headlineLarge TypographyKeyTokens.HeadlineMedium -> headlineMedium TypographyKeyTokens.HeadlineSmall -> headlineSmall TypographyKeyTokens.TitleLarge -> titleLarge TypographyKeyTokens.TitleMedium -> titleMedium TypographyKeyTokens.TitleSmall -> titleSmall TypographyKeyTokens.BodyLarge -> bodyLarge TypographyKeyTokens.BodyMedium -> bodyMedium TypographyKeyTokens.BodySmall -> bodySmall TypographyKeyTokens.LabelLarge -> labelLarge TypographyKeyTokens.LabelMedium -> labelMedium TypographyKeyTokens.LabelSmall -> labelSmall TypographyKeyTokens.DisplayLargeEmphasized -> displayLargeEmphasized TypographyKeyTokens.DisplayMediumEmphasized -> displayMediumEmphasized TypographyKeyTokens.DisplaySmallEmphasized -> displaySmallEmphasized TypographyKeyTokens.HeadlineLargeEmphasized -> headlineLargeEmphasized TypographyKeyTokens.HeadlineMediumEmphasized -> headlineMediumEmphasized TypographyKeyTokens.HeadlineSmallEmphasized -> headlineSmallEmphasized TypographyKeyTokens.TitleLargeEmphasized -> titleLargeEmphasized TypographyKeyTokens.TitleMediumEmphasized -> titleMediumEmphasized TypographyKeyTokens.TitleSmallEmphasized -> titleSmallEmphasized TypographyKeyTokens.BodyLargeEmphasized -> bodyLargeEmphasized TypographyKeyTokens.BodyMediumEmphasized -> bodyMediumEmphasized TypographyKeyTokens.BodySmallEmphasized -> bodySmallEmphasized TypographyKeyTokens.LabelLargeEmphasized -> labelLargeEmphasized TypographyKeyTokens.LabelMediumEmphasized -> labelMediumEmphasized TypographyKeyTokens.LabelSmallEmphasized -> labelSmallEmphasized } } internal val TypographyKeyTokens.value: TextStyle @Composable @ReadOnlyComposable get() = MaterialTheme.typography.fromToken(this) internal val LocalTypography = staticCompositionLocalOf { Typography() } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Shapes.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.material3.tokens.ShapeKeyTokens import androidx.compose.material3.tokens.ShapeTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape /** * Material surfaces can be displayed in different shapes. Shapes direct attention, identify * components, communicate state, and express brand. * * The shape scale defines the style of container corners, offering a range of roundedness from * square to fully circular. * * There are different sizes of shapes: * - Extra Small * - Small * - Medium * - Large, Large Increased * - Extra Large, Extra Large Increased * - Extra Extra Large * * You can customize the shape system for all components in the [MaterialTheme] or you can do it on * a per component basis. * * You can change the shape that a component has by overriding the shape parameter for that * component. For example, by default, buttons use the shape style “full.” If your product requires * a smaller amount of roundedness, you can override the shape parameter with a different shape * value like [MaterialTheme.shapes.small]. * * To learn more about shapes, see * [Material Design shapes](https://m3.material.io/styles/shape/overview). * * @param extraSmall A shape style with 4 same-sized corners whose size are bigger than * [RectangleShape] and smaller than [Shapes.small]. By default autocomplete menu, select menu, * snackbars, standard menu, and text fields use this shape. * @param small A shape style with 4 same-sized corners whose size are bigger than * [Shapes.extraSmall] and smaller than [Shapes.medium]. By default chips use this shape. * @param medium A shape style with 4 same-sized corners whose size are bigger than [Shapes.small] * and smaller than [Shapes.large]. By default cards and small FABs use this shape. * @param large A shape style with 4 same-sized corners whose size are bigger than [Shapes.medium] * and smaller than [Shapes.extraLarge]. By default extended FABs, FABs, and navigation drawers * use this shape. * @param extraLarge A shape style with 4 same-sized corners whose size are bigger than * [Shapes.large] and smaller than [CircleShape]. By default large FABs use this shape. * @param largeIncreased A shape style with 4 same-sized corners whose size are bigger than * [Shapes.medium] and smaller than [Shapes.extraLarge]. Slightly larger variant to * [Shapes.large]. * @param extraLargeIncreased A shape style with 4 same-sized corners whose size are bigger than * [Shapes.large] and smaller than [Shapes.extraExtraLarge]. Slightly larger variant to * [Shapes.extraLarge]. * @param extraExtraLarge A shape style with 4 same-sized corners whose size are bigger than * [Shapes.extraLarge] and smaller than [CircleShape]. */ // TODO: Update new shape descriptions to list what components leverage them by default. // TODO(b/368578382): Update 'increased' variant kdocs to reference design documentation. @Immutable class Shapes @ExperimentalMaterial3ExpressiveApi constructor( // Shapes None and Full are omitted as None is a RectangleShape and Full is a CircleShape. val extraSmall: CornerBasedShape = ShapeDefaults.ExtraSmall, val small: CornerBasedShape = ShapeDefaults.Small, val medium: CornerBasedShape = ShapeDefaults.Medium, val large: CornerBasedShape = ShapeDefaults.Large, val extraLarge: CornerBasedShape = ShapeDefaults.ExtraLarge, largeIncreased: CornerBasedShape = ShapeDefaults.LargeIncreased, extraLargeIncreased: CornerBasedShape = ShapeDefaults.ExtraLargeIncreased, extraExtraLarge: CornerBasedShape = ShapeDefaults.ExtraExtraLarge, ) { @ExperimentalMaterial3ExpressiveApi /** * A shape style with 4 same-sized corners whose size are bigger than [Shapes.medium] and * smaller than [Shapes.extraLarge]. Slightly larger variant to [Shapes.large]. */ val largeIncreased = largeIncreased @ExperimentalMaterial3ExpressiveApi /** * A shape style with 4 same-sized corners whose size are bigger than [Shapes.large] and smaller * than [Shapes.extraExtraLarge]. Slightly larger variant to [Shapes.extraLarge]. */ val extraLargeIncreased = extraLargeIncreased @ExperimentalMaterial3ExpressiveApi /** * A shape style with 4 same-sized corners whose size are bigger than [Shapes.extraLarge] and * smaller than [CircleShape]. */ val extraExtraLarge = extraExtraLarge /** * Material surfaces can be displayed in different shapes. Shapes direct attention, identify * components, communicate state, and express brand. * * The shape scale defines the style of container corners, offering a range of roundedness from * square to fully circular. * * There are different sizes of shapes: * - Extra Small * - Small * - Medium * - Large, Large Increased * - Extra Large, Extra Large Increased * - Extra Extra Large * * You can customize the shape system for all components in the [MaterialTheme] or you can do it * on a per component basis. * * You can change the shape that a component has by overriding the shape parameter for that * component. For example, by default, buttons use the shape style “full.” If your product * requires a smaller amount of roundedness, you can override the shape parameter with a * different shape value like [MaterialTheme.shapes.small]. * * To learn more about shapes, see * [Material Design shapes](https://m3.material.io/styles/shape/overview). * * @param extraSmall A shape style with 4 same-sized corners whose size are bigger than * [RectangleShape] and smaller than [Shapes.small]. By default autocomplete menu, select * menu, snackbars, standard menu, and text fields use this shape. * @param small A shape style with 4 same-sized corners whose size are bigger than * [Shapes.extraSmall] and smaller than [Shapes.medium]. By default chips use this shape. * @param medium A shape style with 4 same-sized corners whose size are bigger than * [Shapes.small] and smaller than [Shapes.large]. By default cards and small FABs use this * shape. * @param large A shape style with 4 same-sized corners whose size are bigger than * [Shapes.medium] and smaller than [Shapes.extraLarge]. By default extended FABs, FABs, and * navigation drawers use this shape. * @param extraLarge A shape style with 4 same-sized corners whose size are bigger than * [Shapes.large] and smaller than [CircleShape]. By default large FABs use this shape. */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) constructor( extraSmall: CornerBasedShape = ShapeDefaults.ExtraSmall, small: CornerBasedShape = ShapeDefaults.Small, medium: CornerBasedShape = ShapeDefaults.Medium, large: CornerBasedShape = ShapeDefaults.Large, extraLarge: CornerBasedShape = ShapeDefaults.ExtraLarge, ) : this( extraSmall = extraSmall, small = small, medium = medium, large = large, extraLarge = extraLarge, largeIncreased = ShapeDefaults.LargeIncreased, extraLargeIncreased = ShapeDefaults.ExtraLargeIncreased, extraExtraLarge = ShapeDefaults.ExtraExtraLarge, ) /** Returns a copy of this Shapes, optionally overriding some of the values. */ @ExperimentalMaterial3ExpressiveApi fun copy( extraSmall: CornerBasedShape = this.extraSmall, small: CornerBasedShape = this.small, medium: CornerBasedShape = this.medium, large: CornerBasedShape = this.large, extraLarge: CornerBasedShape = this.extraLarge, largeIncreased: CornerBasedShape = this.largeIncreased, extraLargeIncreased: CornerBasedShape = this.extraLargeIncreased, extraExtraLarge: CornerBasedShape = this.extraExtraLarge, ): Shapes = Shapes( extraSmall = extraSmall, small = small, medium = medium, large = large, extraLarge = extraLarge, largeIncreased = largeIncreased, extraLargeIncreased = extraLargeIncreased, extraExtraLarge = extraExtraLarge, ) /** Returns a copy of this Shapes, optionally overriding some of the values. */ fun copy( extraSmall: CornerBasedShape = this.extraSmall, small: CornerBasedShape = this.small, medium: CornerBasedShape = this.medium, large: CornerBasedShape = this.large, extraLarge: CornerBasedShape = this.extraLarge, ): Shapes = Shapes( extraSmall = extraSmall, small = small, medium = medium, large = large, extraLarge = extraLarge, ) @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is Shapes) return false if (extraSmall != other.extraSmall) return false if (small != other.small) return false if (medium != other.medium) return false if (large != other.large) return false if (extraLarge != other.extraLarge) return false if (largeIncreased != other.largeIncreased) return false if (extraLargeIncreased != other.extraLargeIncreased) return false if (extraExtraLarge != other.extraExtraLarge) return false return true } @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun hashCode(): Int { var result = extraSmall.hashCode() result = 31 * result + small.hashCode() result = 31 * result + medium.hashCode() result = 31 * result + large.hashCode() result = 31 * result + extraLarge.hashCode() result = 31 * result + largeIncreased.hashCode() result = 31 * result + extraLargeIncreased.hashCode() result = 31 * result + extraExtraLarge.hashCode() return result } @OptIn(ExperimentalMaterial3ExpressiveApi::class) override fun toString(): String { return "Shapes(" + "extraSmall=$extraSmall, " + "small=$small, " + "medium=$medium, " + "large=$large, " + "largeIncreased=$largeIncreased, " + "extraLarge=$extraLarge, " + "extralargeIncreased=$extraLargeIncreased, " + "extraExtraLarge=$extraExtraLarge)" } /** Cached shapes used in components */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultButtonShapesCached: ButtonShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultToggleButtonShapesCached: ToggleButtonShapes? = null internal var defaultVerticalDragHandleShapesCached: DragHandleShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultIconToggleButtonShapesCached: IconToggleButtonShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultIconButtonShapesCached: IconButtonShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultListItemShapesCached: ListItemShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuStandaloneItemShapesCached: MenuItemShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuLeadingItemShapesCached: MenuItemShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuMiddleItemShapesCached: MenuItemShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuTrailingItemShapesCached: MenuItemShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuStandaloneGroupShapesCached: MenuGroupShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuLeadingGroupShapesCached: MenuGroupShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuMiddleGroupShapesCached: MenuGroupShapes? = null @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal var defaultMenuTrailingGroupShapesCached: MenuGroupShapes? = null } /** Contains the default values used by [Shapes] */ object ShapeDefaults { /** Extra small sized corner shape */ val ExtraSmall: CornerBasedShape = ShapeTokens.CornerExtraSmall /** Small sized corner shape */ val Small: CornerBasedShape = ShapeTokens.CornerSmall /** Medium sized corner shape */ val Medium: CornerBasedShape = ShapeTokens.CornerMedium /** Large sized corner shape */ val Large: CornerBasedShape = ShapeTokens.CornerLarge @ExperimentalMaterial3ExpressiveApi /** Large sized corner shape, slightly larger than [Large] */ val LargeIncreased: CornerBasedShape = ShapeTokens.CornerLargeIncreased /** Extra large sized corner shape */ val ExtraLarge: CornerBasedShape = ShapeTokens.CornerExtraLarge @ExperimentalMaterial3ExpressiveApi /** Extra large sized corner shape, slightly larger than [ExtraLarge] */ val ExtraLargeIncreased: CornerBasedShape = ShapeTokens.CornerExtraLargeIncreased @ExperimentalMaterial3ExpressiveApi /** An extra extra large (XXL) sized corner shape */ val ExtraExtraLarge: CornerBasedShape = ShapeTokens.CornerExtraExtraLarge // TODO(b/368578382): Update 'increased' variant kdocs to reference design documentation. /** A non-rounded corner size */ internal val CornerNone: CornerSize = ShapeTokens.CornerValueNone /** An extra small rounded corner size */ internal val CornerExtraSmall: CornerSize = ShapeTokens.CornerValueExtraSmall /** A small rounded corner size */ internal val CornerSmall: CornerSize = ShapeTokens.CornerValueSmall /** A medium rounded corner size */ internal val CornerMedium: CornerSize = ShapeTokens.CornerValueMedium /** A large rounded corner size */ internal val CornerLarge: CornerSize = ShapeTokens.CornerValueLarge /** A large rounded corner size, slightly larger than [CornerLarge] */ internal val CornerLargeIncreased: CornerSize = ShapeTokens.CornerValueLargeIncreased /** An extra large rounded corner size */ internal val CornerExtraLarge: CornerSize = ShapeTokens.CornerValueExtraLarge /** An extra large rounded corner size, slightly larger than [CornerExtraLarge] */ internal val CornerExtraLargeIncreased: CornerSize = ShapeTokens.CornerValueExtraLargeIncreased /** An extra extra large (XXL) rounded corner size */ internal val CornerExtraExtraLarge: CornerSize = ShapeTokens.CornerValueExtraExtraLarge /** A fully rounded corner size */ internal val CornerFull: CornerSize = CornerSize(100) } /** Helper function for component shape tokens. Used to grab the top values of a shape parameter. */ internal fun CornerBasedShape.top( bottomSize: CornerSize = ShapeDefaults.CornerNone ): CornerBasedShape { return copy(bottomStart = bottomSize, bottomEnd = bottomSize) } /** * Helper function for component shape tokens. Used to grab the bottom values of a shape parameter. */ internal fun CornerBasedShape.bottom( topSize: CornerSize = ShapeDefaults.CornerNone ): CornerBasedShape { return copy(topStart = topSize, topEnd = topSize) } /** * Helper function for component shape tokens. Used to grab the start values of a shape parameter. */ internal fun CornerBasedShape.start( endSize: CornerSize = ShapeDefaults.CornerNone ): CornerBasedShape { return copy(topEnd = endSize, bottomEnd = endSize) } /** Helper function for component shape tokens. Used to grab the end values of a shape parameter. */ internal fun CornerBasedShape.end( startSize: CornerSize = ShapeDefaults.CornerNone ): CornerBasedShape { return copy(topStart = startSize, bottomStart = startSize) } /** * Helper function for component shape tokens. Here is an example on how to use component color * tokens: ``MaterialTheme.shapes.fromToken(FabPrimarySmallTokens.ContainerShape)`` */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal fun Shapes.fromToken(value: ShapeKeyTokens): Shape { return when (value) { ShapeKeyTokens.CornerExtraLarge -> extraLarge ShapeKeyTokens.CornerExtraLargeIncreased -> extraLargeIncreased ShapeKeyTokens.CornerExtraExtraLarge -> extraExtraLarge ShapeKeyTokens.CornerExtraLargeTop -> extraLarge.top() ShapeKeyTokens.CornerExtraSmall -> extraSmall ShapeKeyTokens.CornerExtraSmallTop -> extraSmall.top() ShapeKeyTokens.CornerFull -> CircleShape ShapeKeyTokens.CornerLarge -> large ShapeKeyTokens.CornerLargeIncreased -> largeIncreased ShapeKeyTokens.CornerLargeEnd -> large.end() ShapeKeyTokens.CornerLargeTop -> large.top() ShapeKeyTokens.CornerMedium -> medium ShapeKeyTokens.CornerNone -> RectangleShape ShapeKeyTokens.CornerSmall -> small ShapeKeyTokens.CornerLargeStart -> large.start() } } /** * Converts a shape token key to the local shape provided by the theme The color is subscribed to * [LocalShapes] changes */ internal val ShapeKeyTokens.value: Shape @Composable @ReadOnlyComposable get() = MaterialTheme.shapes.fromToken(this) /** CompositionLocal used to specify the default shapes for the surfaces. */ internal val LocalShapes = staticCompositionLocalOf { Shapes() } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Button.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.internal.ProvideContentColorTextStyle import androidx.compose.material3.internal.animateElevation import androidx.compose.material3.internal.rememberAnimatedShape import androidx.compose.material3.tokens.BaselineButtonTokens import androidx.compose.material3.tokens.ButtonLargeTokens import androidx.compose.material3.tokens.ButtonMediumTokens import androidx.compose.material3.tokens.ButtonSmallTokens import androidx.compose.material3.tokens.ButtonXLargeTokens import androidx.compose.material3.tokens.ButtonXSmallTokens import androidx.compose.material3.tokens.ColorSchemeKeyTokens import androidx.compose.material3.tokens.ElevatedButtonTokens import androidx.compose.material3.tokens.FilledButtonTokens import androidx.compose.material3.tokens.FilledTonalButtonTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.OutlinedButtonTokens import androidx.compose.material3.tokens.TextButtonTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp /** * [Material Design button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Filled button * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-button.png) * * Filled buttons are high-emphasis buttons. Filled buttons have the most visual impact after the * [FloatingActionButton], and should be used for important, final actions that complete a flow, * like "Save", "Join now", or "Confirm". * * @sample androidx.compose.material3.samples.ButtonSample * @sample androidx.compose.material3.samples.ButtonWithIconSample * * Button that uses a square shape instead of the default round shape: * * @sample androidx.compose.material3.samples.SquareButtonSample * * Button that utilizes the small design content padding: * * @sample androidx.compose.material3.samples.SmallButtonSample * * [Button] uses the small design spec as default. For a [Button] that uses the design for extra * small, medium, large, or extra large buttons: * * @sample androidx.compose.material3.samples.XSmallButtonWithIconSample * @sample androidx.compose.material3.samples.MediumButtonWithIconSample * @sample androidx.compose.material3.samples.LargeButtonWithIconSample * @sample androidx.compose.material3.samples.XLargeButtonWithIconSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [ElevatedButton] for an [FilledTonalButton] with a shadow. * - See [TextButton] for a low-emphasis button with no border. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.buttonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. See * [ButtonElevation.shadowElevation]. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.buttonColors(), elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } val containerColor = colors.containerColor(enabled) val contentColor = colors.contentColor(enabled) val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp Surface( onClick = onClick, modifier = modifier.semantics { role = Role.Button }, enabled = enabled, shape = shape, color = containerColor, contentColor = contentColor, shadowElevation = shadowElevation, border = border, interactionSource = interactionSource, ) { ProvideContentColorTextStyle( contentColor = contentColor, textStyle = MaterialTheme.typography.labelLarge, ) { Row( Modifier.defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight, ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } } // TODO add link to image of pressed button /** * [Material Design button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. It also morphs between the shapes provided in [shapes] depending on the state of the * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according * to user interaction. * * ![Filled button * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-button.png) * * Filled buttons are high-emphasis buttons. Filled buttons have the most visual impact after the * [FloatingActionButton], and should be used for important, final actions that complete a flow, * like "Save", "Join now", or "Confirm". * * @sample androidx.compose.material3.samples.ButtonWithAnimatedShapeSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [ElevatedButton] for an [FilledTonalButton] with a shadow. * - See [TextButton] for a low-emphasis button with no border. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param shapes the [ButtonShapes] that this button with morph between depending on the user's * interaction with the button. * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.buttonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. See * [ButtonElevation.shadowElevation]. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable @ExperimentalMaterial3ExpressiveApi fun Button( onClick: () -> Unit, shapes: ButtonShapes, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ButtonColors = ButtonDefaults.buttonColors(), elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.contentPaddingFor(ButtonDefaults.MinHeight), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // TODO Load the motionScheme tokens from the component tokens file // MotionSchemeKeyTokens.DefaultEffects is intentional here to prevent // any bounce in this component. val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value() val pressed by interactionSource.collectIsPressedAsState() val containerColor = colors.containerColor(enabled) val contentColor = colors.contentColor(enabled) val shadowElevation = elevation?.shadowElevation(enabled, interactionSource)?.value ?: 0.dp val buttonShape = shapeByInteraction(shapes, pressed, defaultAnimationSpec) Surface( onClick = onClick, modifier = modifier.semantics { role = Role.Button }, enabled = enabled, shape = buttonShape, color = containerColor, contentColor = contentColor, shadowElevation = shadowElevation, border = border, interactionSource = interactionSource, ) { ProvideContentColorTextStyle( contentColor = contentColor, textStyle = MaterialTheme.typography.labelLarge, ) { Row( Modifier.defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight, ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } } /** * [Material Design elevated button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Elevated button * image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-button.png) * * Elevated buttons are high-emphasis buttons that are essentially [FilledTonalButton]s with a * shadow. To prevent shadow creep, only use them when absolutely necessary, such as when the button * requires visual separation from patterned container. * * @sample androidx.compose.material3.samples.ElevatedButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.elevatedButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See [ButtonDefaults.elevatedButtonElevation]. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable fun ElevatedButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.elevatedShape, colors: ButtonColors = ButtonDefaults.elevatedButtonColors(), elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) // TODO add link to image of pressed elevated button /** * [Material Design elevated button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. It also morphs between the shapes provided in [shapes] depending on the state of the * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according * to user interaction. * * ![Elevated button * image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-button.png) * * Elevated buttons are high-emphasis buttons that are essentially [FilledTonalButton]s with a * shadow. To prevent shadow creep, only use them when absolutely necessary, such as when the button * requires visual separation from patterned container. * * @sample androidx.compose.material3.samples.ElevatedButtonWithAnimatedShapeSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param shapes the [ButtonShapes] that this button with morph between depending on the user's * interaction with the button. * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.elevatedButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. See [ButtonDefaults.elevatedButtonElevation]. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable @ExperimentalMaterial3ExpressiveApi fun ElevatedButton( onClick: () -> Unit, shapes: ButtonShapes, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ButtonColors = ButtonDefaults.elevatedButtonColors(), elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.contentPaddingFor(ButtonDefaults.MinHeight), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, shapes = shapes, modifier = modifier, enabled = enabled, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) /** * [Material Design filled tonal button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Filled tonal button * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-tonal-button.png) * * Filled tonal buttons are medium-emphasis buttons that is an alternative middle ground between * default [Button]s (filled) and [OutlinedButton]s. They can be used in contexts where * lower-priority button requires slightly more emphasis than an outline would give, such as "Next" * in an onboarding flow. Tonal buttons use the secondary color mapping. * * @sample androidx.compose.material3.samples.FilledTonalButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [ElevatedButton] for a [FilledTonalButton] with a shadow. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.filledTonalButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable fun FilledTonalButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.filledTonalShape, colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) // TODO add link to image of pressed filled tonal button /** * [Material Design filled tonal button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. It also morphs between the shapes provided in [shapes] depending on the state of the * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according * to user interaction. * * ![Filled tonal button * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-tonal-button.png) * * Filled tonal buttons are medium-emphasis buttons that is an alternative middle ground between * default [Button]s (filled) and [OutlinedButton]s. They can be used in contexts where * lower-priority button requires slightly more emphasis than an outline would give, such as "Next" * in an onboarding flow. Tonal buttons use the secondary color mapping. * * @sample androidx.compose.material3.samples.FilledTonalButtonWithAnimatedShapeSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [ElevatedButton] for a [FilledTonalButton] with a shadow. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param shapes the [ButtonShapes] that this button with morph between depending on the user's * interaction with the button. * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.filledTonalButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable @ExperimentalMaterial3ExpressiveApi fun FilledTonalButton( onClick: () -> Unit, shapes: ButtonShapes, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.contentPaddingFor(ButtonDefaults.MinHeight), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, shapes = shapes, modifier = modifier, enabled = enabled, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) /** * [Material Design outlined button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Outlined button * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-button.png) * * Outlined buttons are medium-emphasis buttons. They contain actions that are important, but are * not the primary action in an app. Outlined buttons pair well with [Button]s to indicate an * alternative, secondary action. * * @sample androidx.compose.material3.samples.OutlinedButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]). * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.outlinedButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. * @param border the border to draw around the container of this button. Pass `null` for no border. * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable fun OutlinedButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.outlinedShape, colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), elevation: ButtonElevation? = null, border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled), contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) // TODO add link to image of pressed outlined button /** * [Material Design outlined button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. It also morphs between the shapes provided in [shapes] depending on the state of the * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according * to user interaction. * * ![Outlined button * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-button.png) * * Outlined buttons are medium-emphasis buttons. They contain actions that are important, but are * not the primary action in an app. Outlined buttons pair well with [Button]s to indicate an * alternative, secondary action. * * @sample androidx.compose.material3.samples.OutlinedButtonWithAnimatedShapeSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * - See [TextButton] for a low-emphasis button with no border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param shapes the [ButtonShapes] that this button with morph between depending on the user's * interaction with the button. * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.outlinedButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. * @param border the border to draw around the container of this button. Pass `null` for no border. * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text, icon or image. */ @Composable @ExperimentalMaterial3ExpressiveApi fun OutlinedButton( onClick: () -> Unit, shapes: ButtonShapes, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), elevation: ButtonElevation? = null, border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled), contentPadding: PaddingValues = ButtonDefaults.contentPaddingFor(ButtonDefaults.MinHeight), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, shapes = shapes, modifier = modifier, enabled = enabled, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) /** * [Material Design text button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. * * ![Text button * image](https://developer.android.com/images/reference/androidx/compose/material3/text-button.png) * * Text buttons are typically used for less-pronounced actions, including those located in dialogs * and cards. In cards, text buttons help maintain an emphasis on card content. Text buttons are * used for the lowest priority actions, especially when presenting multiple options. * * @sample androidx.compose.material3.samples.TextButtonWithAnimatedShapeSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [ElevatedButton] for a [FilledTonalButton] with a shadow. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this button's container, border (when [border] is not null), * and shadow (when using [elevation]) * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.textButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. A TextButton typically has no elevation, and the default value is `null`. See * [ElevatedButton] for a button with elevation. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text. */ @Composable fun TextButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.textShape, colors: ButtonColors = ButtonDefaults.textButtonColors(), elevation: ButtonElevation? = null, border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) // TODO add link to image of pressed text button /** * [Material Design text button](https://m3.material.io/components/buttons/overview) * * Buttons help people initiate actions, from sending an email, to sharing a document, to liking a * post. It also morphs between the shapes provided in [shapes] depending on the state of the * interaction with the button as long as the shapes provided our [CornerBasedShape]s. If a shape in * [shapes] isn't a [CornerBasedShape], then button will change between the [ButtonShapes] according * to user interaction. * * ![Text button * image](https://developer.android.com/images/reference/androidx/compose/material3/text-button.png) * * Text buttons are typically used for less-pronounced actions, including those located in dialogs * and cards. In cards, text buttons help maintain an emphasis on card content. Text buttons are * used for the lowest priority actions, especially when presenting multiple options. * * @sample androidx.compose.material3.samples.TextButtonSample * * Choose the best button for an action based on the amount of emphasis it needs. The more important * an action is, the higher emphasis its button should be. * - See [Button] for a high-emphasis button without a shadow, also known as a filled button. * - See [ElevatedButton] for a [FilledTonalButton] with a shadow. * - See [FilledTonalButton] for a middle ground between [OutlinedButton] and [Button]. * - See [OutlinedButton] for a medium-emphasis button with a border. * * The default text style for internal [Text] components will be set to [Typography.labelLarge]. * * @param onClick called when this button is clicked * @param shapes the [ButtonShapes] that this button with morph between depending on the user's * interaction with the button. * @param modifier the [Modifier] to be applied to this button * @param enabled controls the enabled state of this button. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [ButtonColors] that will be used to resolve the colors for this button in different * states. See [ButtonDefaults.textButtonColors]. * @param elevation [ButtonElevation] used to resolve the elevation for this button in different * states. This controls the size of the shadow below the button. Additionally, when the container * color is [ColorScheme.surface], this controls the amount of primary color applied as an * overlay. A TextButton typically has no elevation, and the default value is `null`. See * [ElevatedButton] for a button with elevation. * @param border the border to draw around the container of this button * @param contentPadding the spacing values to apply internally between the container and the * content * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this button. You can use this to change the button's appearance or * preview the button in different states. Note that if `null` is provided, interactions will * still happen internally. * @param content The content displayed on the button, expected to be text. */ @ExperimentalMaterial3ExpressiveApi @Composable fun TextButton( onClick: () -> Unit, shapes: ButtonShapes, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ButtonColors = ButtonDefaults.textButtonColors(), elevation: ButtonElevation? = null, border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.contentPaddingFor(ButtonDefaults.MinHeight), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) = Button( onClick = onClick, shapes = shapes, modifier = modifier, enabled = enabled, colors = colors, elevation = elevation, border = border, contentPadding = contentPadding, interactionSource = interactionSource, content = content, ) // TODO(b/201341237): Use token values for 0 elevation? // TODO(b/201341237): Use token values for null border? // TODO(b/201341237): Use token values for no color (transparent)? /** * Contains the default values used by all 5 button types. * * Default values that apply to all buttons types are [MinWidth], [MinHeight], [IconSize], and * [IconSpacing]. * * A default value that applies only to [Button], [ElevatedButton], [FilledTonalButton], and * [OutlinedButton] is [ContentPadding]. * * Default values that apply only to [Button] are [buttonColors] and [buttonElevation]. Default * values that apply only to [ElevatedButton] are [elevatedButtonColors] and * [elevatedButtonElevation]. Default values that apply only to [FilledTonalButton] are * [filledTonalButtonColors] and [filledTonalButtonElevation]. A default value that applies only to * [OutlinedButton] is [outlinedButtonColors]. Default values that apply only to [TextButton] is * [textButtonColors]. */ object ButtonDefaults { private val ButtonLeadingSpace = BaselineButtonTokens.LeadingSpace private val ButtonTrailingSpace = BaselineButtonTokens.TrailingSpace private val ButtonWithIconStartpadding = 16.dp private val SmallStartPadding = ButtonSmallTokens.LeadingSpace private val SmallEndPadding = ButtonSmallTokens.TrailingSpace private val ButtonVerticalPadding = 8.dp /** * The default content padding used by [Button], [ElevatedButton], [FilledTonalButton], * [OutlinedButton], and [TextButton] buttons. * - See [ButtonWithIconContentPadding] for content padding used by [Button] that contains * [Icon]. */ val ContentPadding = PaddingValues( start = ButtonLeadingSpace, top = ButtonVerticalPadding, end = ButtonTrailingSpace, bottom = ButtonVerticalPadding, ) /** The default content padding used by [Button] that contains an [Icon]. */ val ButtonWithIconContentPadding = PaddingValues( start = ButtonWithIconStartpadding, top = ButtonVerticalPadding, end = ButtonTrailingSpace, bottom = ButtonVerticalPadding, ) /** The default content padding used for small [Button] */ @Deprecated("For binary compatibility", level = DeprecationLevel.HIDDEN) @ExperimentalMaterial3ExpressiveApi val SmallButtonContentPadding get() = PaddingValues( start = SmallStartPadding, top = ButtonVerticalPadding, end = SmallEndPadding, bottom = ButtonVerticalPadding, ) /** The default content padding used for small [Button] */ @ExperimentalMaterial3ExpressiveApi val SmallContentPadding get() = PaddingValues( start = SmallStartPadding, top = SmallVerticalPadding, end = SmallEndPadding, bottom = SmallVerticalPadding, ) private fun getSmallContentPadding(hasStartIcon: Boolean, hasEndIcon: Boolean) = PaddingValues( start = if (hasStartIcon) IconSmallHorizontalPadding else SmallStartPadding, top = SmallVerticalPadding, end = if (hasEndIcon) IconSmallHorizontalPadding else SmallEndPadding, bottom = SmallVerticalPadding, ) /** Default content padding for an extra small button. */ @ExperimentalMaterial3ExpressiveApi val ExtraSmallContentPadding get() = PaddingValues( // TODO update with the value from ButtonXSmallTokens.kt once it's been corrected start = 12.dp, end = 12.dp, top = 6.dp, bottom = 6.dp, ) /** Default content padding for a medium button. */ @ExperimentalMaterial3ExpressiveApi val MediumContentPadding get() = PaddingValues( start = MediumLeadingPadding, top = MediumVerticalPadding, end = MediumTrailingPadding, bottom = MediumVerticalPadding, ) private fun getMediumContentPadding(hasLeadingIcon: Boolean, hasTrailingIcon: Boolean) = PaddingValues( start = if (hasLeadingIcon) IconMediumLeadingPadding else MediumLeadingPadding, top = MediumVerticalPadding, end = if (hasTrailingIcon) IconMediumTrailingPadding else MediumTrailingPadding, bottom = MediumVerticalPadding, ) /** Default content padding for a large button. */ @ExperimentalMaterial3ExpressiveApi val LargeContentPadding get() = PaddingValues( start = LargeLeadingPadding, top = LargeVerticalPadding, end = LargeTrailingPadding, bottom = LargeVerticalPadding, ) private fun getLargeContentPadding(hasLeadingIcon: Boolean, hasTrailingIcon: Boolean) = PaddingValues( start = if (hasLeadingIcon) IconLargeLeadingPadding else LargeLeadingPadding, top = LargeVerticalPadding, end = if (hasTrailingIcon) IconLargeTrailingPadding else LargeTrailingPadding, bottom = LargeVerticalPadding, ) /** Default content padding for an extra large button. */ @ExperimentalMaterial3ExpressiveApi val ExtraLargeContentPadding get() = PaddingValues( start = ButtonXLargeTokens.LeadingSpace, end = ButtonXLargeTokens.TrailingSpace, top = 48.dp, bottom = 48.dp, ) private val TextButtonHorizontalPadding = 12.dp /** * The default content padding used by [TextButton]. * - See [TextButtonWithIconContentPadding] for content padding used by [TextButton] that * contains [Icon]. * * Note: it's recommended to use [ContentPadding] instead for a more consistent look between all * buttons variants. */ val TextButtonContentPadding = PaddingValues( start = TextButtonHorizontalPadding, top = ContentPadding.calculateTopPadding(), end = TextButtonHorizontalPadding, bottom = ContentPadding.calculateBottomPadding(), ) private val TextButtonWithIconHorizontalEndPadding = 16.dp /** * The default content padding used by [TextButton] that contains an [Icon]. * * Note: it's recommended to use [ButtonWithIconContentPadding] instead for a more consistent * look between all buttons variants. */ val TextButtonWithIconContentPadding = PaddingValues( start = TextButtonHorizontalPadding, top = ContentPadding.calculateTopPadding(), end = TextButtonWithIconHorizontalEndPadding, bottom = ContentPadding.calculateBottomPadding(), ) /** * The default min width applied for small buttons. Note that you can override it by applying * Modifier.widthIn directly on the button composable. */ val MinWidth = 58.dp /** * The default min height applied for small buttons. Note that you can override it by applying * Modifier.heightIn directly on the button composable. */ val MinHeight = if (shouldUsePrecisionPointerComponentSizing.value) { 36.dp } else { ButtonSmallTokens.ContainerHeight } /** The default height for a extra small button container. */ @ExperimentalMaterial3ExpressiveApi val ExtraSmallContainerHeight = ButtonXSmallTokens.ContainerHeight /** The default height for a medium button container. */ @ExperimentalMaterial3ExpressiveApi val MediumContainerHeight = if (shouldUsePrecisionPointerComponentSizing.value) { 46.dp } else { ButtonMediumTokens.ContainerHeight } /** The default height for a large button container. */ @ExperimentalMaterial3ExpressiveApi val LargeContainerHeight = if (shouldUsePrecisionPointerComponentSizing.value) { 54.dp } else { ButtonLargeTokens.ContainerHeight } /** The default height for a extra large button container. */ @ExperimentalMaterial3ExpressiveApi val ExtraLargeContainerHeight = ButtonXLargeTokens.ContainerHeight /** The default size of the icon when used inside a small button. */ // TODO update with the correct value in BaselineButtonTokens when available val IconSize = 18.dp /** The default size of the icon used inside an extra small button. */ @ExperimentalMaterial3ExpressiveApi val ExtraSmallIconSize = ButtonXSmallTokens.IconSize /** The expressive size of the icon used inside a small button. */ @ExperimentalMaterial3ExpressiveApi val SmallIconSize = ButtonSmallTokens.IconSize /** The default size of the icon used inside a medium button. */ @ExperimentalMaterial3ExpressiveApi val MediumIconSize = ButtonMediumTokens.IconSize /** The default size of the icon used inside a large button. */ @ExperimentalMaterial3ExpressiveApi val LargeIconSize = if (shouldUsePrecisionPointerComponentSizing.value) { 24.dp } else { ButtonLargeTokens.IconSize } /** The default size of the icon used inside an extra large button. */ @ExperimentalMaterial3ExpressiveApi val ExtraLargeIconSize = ButtonXLargeTokens.IconSize /** * The default size of the spacing between an icon and a text when they used inside a small * button. */ val IconSpacing = ButtonSmallTokens.IconLabelSpace /** * The default spacing between an icon and a text when they used inside any extra small button. */ // TODO use the value from ButtonXSmallTokens.kt once it's been corrected @ExperimentalMaterial3ExpressiveApi val ExtraSmallIconSpacing = 4.dp /** The default spacing between an icon and a text when they're inside any medium button. */ @ExperimentalMaterial3ExpressiveApi val MediumIconSpacing = ButtonMediumTokens.IconLabelSpace /** The default spacing between an icon and a text when they're inside any large button. */ @ExperimentalMaterial3ExpressiveApi val LargeIconSpacing = if (shouldUsePrecisionPointerComponentSizing.value) { 8.dp } else { ButtonLargeTokens.IconLabelSpace } /** * The default spacing between an icon and a text when they used inside any extra large button. */ @ExperimentalMaterial3ExpressiveApi val ExtraLargeIconSpacing = ButtonXLargeTokens.IconLabelSpace /** Square shape for default buttons. */ @ExperimentalMaterial3ExpressiveApi val squareShape: Shape @Composable get() = ButtonSmallTokens.ContainerShapeSquare.value /** Pressed shape for default buttons. */ @ExperimentalMaterial3ExpressiveApi val pressedShape: Shape @Composable get() = ButtonSmallTokens.PressedContainerShape.value /** Pressed shape for extra small buttons. */ @ExperimentalMaterial3ExpressiveApi val extraSmallPressedShape: Shape @Composable get() = ButtonXSmallTokens.PressedContainerShape.value /** Pressed shape for medium buttons. */ @ExperimentalMaterial3ExpressiveApi val mediumPressedShape: Shape @Composable get() = ButtonMediumTokens.PressedContainerShape.value /** Pressed shape for large buttons. */ @ExperimentalMaterial3ExpressiveApi val largePressedShape: Shape @Composable get() = ButtonLargeTokens.PressedContainerShape.value /** Pressed shape for extra large buttons. */ @ExperimentalMaterial3ExpressiveApi val extraLargePressedShape: Shape @Composable get() = ButtonXLargeTokens.PressedContainerShape.value /** Default shape for a button. */ val shape: Shape @Composable get() = ButtonSmallTokens.ContainerShapeRound.value /** Default shape for an elevated button. */ val elevatedShape: Shape @Composable get() = ButtonSmallTokens.ContainerShapeRound.value /** Default shape for a filled tonal button. */ val filledTonalShape: Shape @Composable get() = ButtonSmallTokens.ContainerShapeRound.value /** Default shape for an outlined button. */ val outlinedShape: Shape @Composable get() = ButtonSmallTokens.ContainerShapeRound.value /** Default shape for a text button. */ val textShape: Shape @Composable get() = ButtonSmallTokens.ContainerShapeRound.value /** * Creates a [ButtonShapes] that represents the default shape and pressed shape used in a * button. */ @ExperimentalMaterial3ExpressiveApi @Composable fun shapes() = MaterialTheme.shapes.defaultButtonShapes /** * Creates a [ButtonShapes] that represents the default shape and pressedShape used in a * [Button] and its variants. * * @param shape the unchecked shape for [ButtonShapes] * @param pressedShape the unchecked shape for [ButtonShapes] */ @Composable @ExperimentalMaterial3ExpressiveApi fun shapes(shape: Shape? = null, pressedShape: Shape? = null): ButtonShapes = MaterialTheme.shapes.defaultButtonShapes.copy(shape = shape, pressedShape = pressedShape) @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal val Shapes.defaultButtonShapes: ButtonShapes get() { return defaultButtonShapesCached ?: ButtonShapes( shape = fromToken(ButtonSmallTokens.ContainerShapeRound), pressedShape = fromToken(ButtonSmallTokens.PressedContainerShape), ) .also { defaultButtonShapesCached = it } } /** * Creates a [ButtonColors] that represents the default container and content colors used in a * [Button]. */ @Composable fun buttonColors() = MaterialTheme.colorScheme.defaultButtonColors /** * Creates a [ButtonColors] that represents the default container and content colors used in a * [Button]. * * @param containerColor the container color of this [Button] when enabled. * @param contentColor the content color of this [Button] when enabled. * @param disabledContainerColor the container color of this [Button] when not enabled. * @param disabledContentColor the content color of this [Button] when not enabled. */ @Composable fun buttonColors( containerColor: Color = Color.Unspecified, contentColor: Color = Color.Unspecified, disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = Color.Unspecified, ): ButtonColors = MaterialTheme.colorScheme.defaultButtonColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultButtonColors: ButtonColors get() { return defaultButtonColorsCached ?: ButtonColors( containerColor = fromToken(FilledButtonTokens.ContainerColor), contentColor = fromToken(FilledButtonTokens.LabelTextColor), disabledContainerColor = fromToken(FilledButtonTokens.DisabledContainerColor) .copy(alpha = FilledButtonTokens.DisabledContainerOpacity), disabledContentColor = fromToken(FilledButtonTokens.DisabledLabelTextColor) .copy(alpha = FilledButtonTokens.DisabledLabelTextOpacity), ) .also { defaultButtonColorsCached = it } } /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [ElevatedButton]. */ @Composable fun elevatedButtonColors() = MaterialTheme.colorScheme.defaultElevatedButtonColors /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [ElevatedButton]. * * @param containerColor the container color of this [ElevatedButton] when enabled * @param contentColor the content color of this [ElevatedButton] when enabled * @param disabledContainerColor the container color of this [ElevatedButton] when not enabled * @param disabledContentColor the content color of this [ElevatedButton] when not enabled */ @Composable fun elevatedButtonColors( containerColor: Color = Color.Unspecified, contentColor: Color = Color.Unspecified, disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = Color.Unspecified, ): ButtonColors = MaterialTheme.colorScheme.defaultElevatedButtonColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultElevatedButtonColors: ButtonColors get() { return defaultElevatedButtonColorsCached ?: ButtonColors( containerColor = fromToken(ElevatedButtonTokens.ContainerColor), contentColor = fromToken(ElevatedButtonTokens.LabelTextColor), disabledContainerColor = fromToken(ElevatedButtonTokens.DisabledContainerColor) .copy(alpha = ElevatedButtonTokens.DisabledContainerOpacity), disabledContentColor = fromToken(ElevatedButtonTokens.DisabledLabelTextColor) .copy(alpha = ElevatedButtonTokens.DisabledLabelTextOpacity), ) .also { defaultElevatedButtonColorsCached = it } } /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [FilledTonalButton]. */ @Composable fun filledTonalButtonColors() = MaterialTheme.colorScheme.defaultFilledTonalButtonColors /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [FilledTonalButton]. * * @param containerColor the container color of this [FilledTonalButton] when enabled * @param contentColor the content color of this [FilledTonalButton] when enabled * @param disabledContainerColor the container color of this [FilledTonalButton] when not * enabled * @param disabledContentColor the content color of this [FilledTonalButton] when not enabled */ @Composable fun filledTonalButtonColors( containerColor: Color = Color.Unspecified, contentColor: Color = Color.Unspecified, disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = Color.Unspecified, ): ButtonColors = MaterialTheme.colorScheme.defaultFilledTonalButtonColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultFilledTonalButtonColors: ButtonColors get() { return defaultFilledTonalButtonColorsCached ?: ButtonColors( containerColor = fromToken(FilledTonalButtonTokens.ContainerColor), contentColor = fromToken(FilledTonalButtonTokens.LabelTextColor), disabledContainerColor = fromToken(FilledTonalButtonTokens.DisabledContainerColor) .copy(alpha = FilledTonalButtonTokens.DisabledContainerOpacity), disabledContentColor = fromToken(FilledTonalButtonTokens.DisabledLabelTextColor) .copy(alpha = FilledTonalButtonTokens.DisabledLabelTextOpacity), ) .also { defaultFilledTonalButtonColorsCached = it } } /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [OutlinedButton]. */ @Composable fun outlinedButtonColors() = MaterialTheme.colorScheme.defaultOutlinedButtonColors /** * Creates a [ButtonColors] that represents the default container and content colors used in an * [OutlinedButton]. * * @param containerColor the container color of this [OutlinedButton] when enabled * @param contentColor the content color of this [OutlinedButton] when enabled * @param disabledContainerColor the container color of this [OutlinedButton] when not enabled * @param disabledContentColor the content color of this [OutlinedButton] when not enabled */ @Composable fun outlinedButtonColors( containerColor: Color = Color.Unspecified, contentColor: Color = Color.Unspecified, disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = Color.Unspecified, ): ButtonColors = MaterialTheme.colorScheme.defaultOutlinedButtonColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultOutlinedButtonColors: ButtonColors get() { return defaultOutlinedButtonColorsCached ?: ButtonColors( containerColor = Color.Transparent, contentColor = fromToken(OutlinedButtonTokens.LabelTextColor), disabledContainerColor = Color.Transparent, disabledContentColor = fromToken(OutlinedButtonTokens.DisabledLabelTextColor) .copy(alpha = OutlinedButtonTokens.DisabledLabelTextOpacity), ) .also { defaultOutlinedButtonColorsCached = it } } /** * Creates a [ButtonColors] that represents the default container and content colors used in a * [TextButton]. */ @Composable fun textButtonColors() = MaterialTheme.colorScheme.defaultTextButtonColors /** * Creates a [ButtonColors] that represents the default container and content colors used in a * [TextButton]. * * @param containerColor the container color of this [TextButton] when enabled * @param contentColor the content color of this [TextButton] when enabled * @param disabledContainerColor the container color of this [TextButton] when not enabled * @param disabledContentColor the content color of this [TextButton] when not enabled */ @Composable fun textButtonColors( containerColor: Color = Color.Unspecified, contentColor: Color = Color.Unspecified, disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = Color.Unspecified, ): ButtonColors = MaterialTheme.colorScheme.defaultTextButtonColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultTextButtonColors: ButtonColors get() { return defaultTextButtonColorsCached ?: ButtonColors( containerColor = Color.Transparent, // TODO replace with the token value once it's corrected contentColor = fromToken(ColorSchemeKeyTokens.Primary), disabledContainerColor = Color.Transparent, disabledContentColor = fromToken(TextButtonTokens.DisabledLabelColor) .copy(alpha = TextButtonTokens.DisabledLabelOpacity), ) .also { defaultTextButtonColorsCached = it } } /** * Creates a [ButtonElevation] that will animate between the provided values according to the * Material specification for a [Button]. * * @param defaultElevation the elevation used when the [Button] is enabled, and has no other * [Interaction]s. * @param pressedElevation the elevation used when this [Button] is enabled and pressed. * @param focusedElevation the elevation used when the [Button] is enabled and focused. * @param hoveredElevation the elevation used when the [Button] is enabled and hovered. * @param disabledElevation the elevation used when the [Button] is not enabled. */ @Composable fun buttonElevation( defaultElevation: Dp = FilledButtonTokens.ContainerElevation, pressedElevation: Dp = FilledButtonTokens.PressedContainerElevation, focusedElevation: Dp = FilledButtonTokens.FocusedContainerElevation, hoveredElevation: Dp = FilledButtonTokens.HoveredContainerElevation, disabledElevation: Dp = FilledButtonTokens.DisabledContainerElevation, ): ButtonElevation = ButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation, ) /** * Creates a [ButtonElevation] that will animate between the provided values according to the * Material specification for a [ElevatedButton]. * * @param defaultElevation the elevation used when the [ElevatedButton] is enabled, and has no * other [Interaction]s. * @param pressedElevation the elevation used when this [ElevatedButton] is enabled and pressed. * @param focusedElevation the elevation used when the [ElevatedButton] is enabled and focused. * @param hoveredElevation the elevation used when the [ElevatedButton] is enabled and hovered. * @param disabledElevation the elevation used when the [ElevatedButton] is not enabled. */ @Composable fun elevatedButtonElevation( defaultElevation: Dp = ElevatedButtonTokens.ContainerElevation, pressedElevation: Dp = ElevatedButtonTokens.PressedContainerElevation, focusedElevation: Dp = ElevatedButtonTokens.FocusedContainerElevation, hoveredElevation: Dp = ElevatedButtonTokens.HoveredContainerElevation, disabledElevation: Dp = ElevatedButtonTokens.DisabledContainerElevation, ): ButtonElevation = ButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation, ) /** * Creates a [ButtonElevation] that will animate between the provided values according to the * Material specification for a [FilledTonalButton]. * * @param defaultElevation the elevation used when the [FilledTonalButton] is enabled, and has * no other [Interaction]s. * @param pressedElevation the elevation used when this [FilledTonalButton] is enabled and * pressed. * @param focusedElevation the elevation used when the [FilledTonalButton] is enabled and * focused. * @param hoveredElevation the elevation used when the [FilledTonalButton] is enabled and * hovered. * @param disabledElevation the elevation used when the [FilledTonalButton] is not enabled. */ @Composable fun filledTonalButtonElevation( defaultElevation: Dp = FilledTonalButtonTokens.ContainerElevation, pressedElevation: Dp = FilledTonalButtonTokens.PressedContainerElevation, focusedElevation: Dp = FilledTonalButtonTokens.FocusContainerElevation, hoveredElevation: Dp = FilledTonalButtonTokens.HoverContainerElevation, disabledElevation: Dp = 0.dp, ): ButtonElevation = ButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, disabledElevation = disabledElevation, ) /** The default [BorderStroke] used by [OutlinedButton]. */ val outlinedButtonBorder: BorderStroke @Composable @Deprecated( message = "Please use the version that takes an `enabled` param to get the " + "`BorderStroke` with the correct opacity", replaceWith = ReplaceWith("outlinedButtonBorder(enabled)"), ) get() = BorderStroke( width = ButtonSmallTokens.OutlinedOutlineWidth, color = OutlinedButtonTokens.OutlineColor.value, ) /** * The default [BorderStroke] used by [OutlinedButton]. * * @param enabled whether the button is enabled */ @Composable fun outlinedButtonBorder(enabled: Boolean = true): BorderStroke = BorderStroke( width = ButtonSmallTokens.OutlinedOutlineWidth, color = if (enabled) { OutlinedButtonTokens.OutlineColor.value } else { OutlinedButtonTokens.OutlineColor.value.copy( alpha = OutlinedButtonTokens.DisabledContainerOpacity ) }, ) /** * Recommended [ButtonShapes] for a provided button height. * * @param buttonHeight The height of the button */ @Composable @ExperimentalMaterial3ExpressiveApi fun shapesFor(buttonHeight: Dp): ButtonShapes { val xSmallHeight = ExtraSmallContainerHeight val smallHeight = MinHeight val mediumHeight = MediumContainerHeight val largeHeight = LargeContainerHeight val xLargeHeight = ExtraLargeContainerHeight return when { buttonHeight <= (xSmallHeight + smallHeight) / 2 -> shapes(shape = shape, pressedShape = extraSmallPressedShape) buttonHeight <= (smallHeight + mediumHeight) / 2 -> shapes() buttonHeight <= (mediumHeight + largeHeight) / 2 -> shapes(shape = shape, pressedShape = mediumPressedShape) buttonHeight <= (largeHeight + xLargeHeight) / 2 -> shapes(shape = shape, pressedShape = largePressedShape) else -> shapes(shape = shape, pressedShape = extraLargePressedShape) } } /** * Recommended [PaddingValues] for a provided button height. * * @param buttonHeight The height of the button * @param hasStartIcon Whether the button has a leading icon * @param hasEndIcon Whether the button has a trailing icon */ @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun contentPaddingFor( buttonHeight: Dp, hasStartIcon: Boolean = false, hasEndIcon: Boolean = false, ): PaddingValues { val smallHeight = MinHeight val mediumHeight = MediumContainerHeight val largeHeight = LargeContainerHeight val xLargeHeight = ExtraLargeContainerHeight return when { buttonHeight < smallHeight -> ExtraSmallContentPadding buttonHeight < mediumHeight -> getSmallContentPadding(hasStartIcon, hasEndIcon) buttonHeight < largeHeight -> getMediumContentPadding(hasStartIcon, hasEndIcon) buttonHeight < xLargeHeight -> getLargeContentPadding(hasStartIcon, hasEndIcon) else -> ExtraLargeContentPadding } } /** * Recommended [PaddingValues] for a provided button height. * * @param buttonHeight The height of the button */ @Deprecated( message = "Deprecated in favor of function with hasLeadingIcon and hasTrailingIcon params", level = DeprecationLevel.HIDDEN, ) @ExperimentalMaterial3ExpressiveApi fun contentPaddingFor(buttonHeight: Dp): PaddingValues { val smallHeight = MinHeight val mediumHeight = MediumContainerHeight val largeHeight = LargeContainerHeight val xLargeHeight = ExtraLargeContainerHeight return when { buttonHeight < smallHeight -> ExtraSmallContentPadding buttonHeight < mediumHeight -> SmallContentPadding buttonHeight < largeHeight -> MediumContentPadding buttonHeight < xLargeHeight -> LargeContentPadding else -> ExtraLargeContentPadding } } /** * Recommended Icon size for a provided button height. * * @param buttonHeight The height of the button */ @ExperimentalMaterial3ExpressiveApi fun iconSizeFor(buttonHeight: Dp): Dp { val smallHeight = MinHeight val mediumHeight = MediumContainerHeight val largeHeight = LargeContainerHeight val xLargeHeight = ExtraLargeContainerHeight return when { buttonHeight < smallHeight -> ExtraSmallIconSize buttonHeight < mediumHeight -> SmallIconSize buttonHeight < largeHeight -> MediumIconSize buttonHeight < xLargeHeight -> LargeIconSize else -> ExtraLargeIconSize } } /** * Recommended spacing after an [Icon] for a provided button height. * * @param buttonHeight The height of the button */ @ExperimentalMaterial3ExpressiveApi fun iconSpacingFor(buttonHeight: Dp): Dp { val smallHeight = MinHeight val mediumHeight = MediumContainerHeight val largeHeight = LargeContainerHeight val xLargeHeight = ExtraLargeContainerHeight return when { buttonHeight < smallHeight -> ExtraSmallIconSpacing buttonHeight < mediumHeight -> IconSpacing buttonHeight < largeHeight -> MediumIconSpacing buttonHeight < xLargeHeight -> LargeIconSpacing else -> ExtraLargeIconSpacing } } /** * Recommended [TextStyle] for a [Text] provided a button height. * * @param buttonHeight The height of the button */ @Composable @ExperimentalMaterial3ExpressiveApi fun textStyleFor(buttonHeight: Dp): TextStyle { val mediumHeight = MediumContainerHeight val largeHeight = LargeContainerHeight val xLargeHeight = ExtraLargeContainerHeight return when { buttonHeight < mediumHeight -> MaterialTheme.typography.labelLarge buttonHeight < largeHeight -> if (shouldUsePrecisionPointerComponentSizing.value) { MaterialTheme.typography.titleMedium.copy(fontSize = 15.sp, lineHeight = 22.sp) } else { MaterialTheme.typography.titleMedium } buttonHeight < xLargeHeight -> if (shouldUsePrecisionPointerComponentSizing.value) { MaterialTheme.typography.headlineSmall.copy( fontSize = 18.sp, lineHeight = 26.sp, ) } else { MaterialTheme.typography.headlineSmall } else -> MaterialTheme.typography.headlineLarge } } private val SmallVerticalPadding = if (shouldUsePrecisionPointerComponentSizing.value) 8.dp else 10.dp private val IconSmallHorizontalPadding = if (shouldUsePrecisionPointerComponentSizing.value) 12.dp else SmallStartPadding private val MediumLeadingPadding = ButtonMediumTokens.LeadingSpace private val MediumTrailingPadding = ButtonMediumTokens.TrailingSpace private val MediumVerticalPadding = if (shouldUsePrecisionPointerComponentSizing.value) 12.dp else 16.dp private val IconMediumLeadingPadding = if (shouldUsePrecisionPointerComponentSizing.value) { 20.dp } else { ButtonMediumTokens.LeadingSpace } private val IconMediumTrailingPadding = if (shouldUsePrecisionPointerComponentSizing.value) { 20.dp } else { ButtonMediumTokens.TrailingSpace } private val LargeVerticalPadding = if (shouldUsePrecisionPointerComponentSizing.value) 14.dp else 32.dp private val LargeLeadingPadding = if (shouldUsePrecisionPointerComponentSizing.value) { 32.dp } else { ButtonLargeTokens.LeadingSpace } private val LargeTrailingPadding = if (shouldUsePrecisionPointerComponentSizing.value) { 32.dp } else { ButtonLargeTokens.TrailingSpace } private val IconLargeLeadingPadding = if (shouldUsePrecisionPointerComponentSizing.value) { 28.dp } else { ButtonLargeTokens.LeadingSpace } private val IconLargeTrailingPadding = if (shouldUsePrecisionPointerComponentSizing.value) { 28.dp } else { ButtonLargeTokens.TrailingSpace } } /** * Represents the elevation for a button in different states. * - See [ButtonDefaults.buttonElevation] for the default elevation used in a [Button]. * - See [ButtonDefaults.elevatedButtonElevation] for the default elevation used in a * [ElevatedButton]. */ @Stable class ButtonElevation internal constructor( private val defaultElevation: Dp, private val pressedElevation: Dp, private val focusedElevation: Dp, private val hoveredElevation: Dp, private val disabledElevation: Dp, ) { /** * Represents the shadow elevation used in a button, depending on its [enabled] state and * [interactionSource]. * * Shadow elevation is used to apply a shadow around the button to give it higher emphasis. * * @param enabled whether the button is enabled * @param interactionSource the [InteractionSource] for this button */ @Composable internal fun shadowElevation( enabled: Boolean, interactionSource: InteractionSource, ): State { return animateElevation(enabled = enabled, interactionSource = interactionSource) } @Composable private fun animateElevation( enabled: Boolean, interactionSource: InteractionSource, ): State { val interactions = remember { mutableStateListOf() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is HoverInteraction.Enter -> { interactions.add(interaction) } is HoverInteraction.Exit -> { interactions.remove(interaction.enter) } is FocusInteraction.Focus -> { interactions.add(interaction) } is FocusInteraction.Unfocus -> { interactions.remove(interaction.focus) } is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } } } } val interaction = interactions.lastOrNull() val target = if (!enabled) { disabledElevation } else { when (interaction) { is PressInteraction.Press -> pressedElevation is HoverInteraction.Enter -> hoveredElevation is FocusInteraction.Focus -> focusedElevation else -> defaultElevation } } val animatable = remember { Animatable(target, Dp.VectorConverter) } LaunchedEffect(target) { if (animatable.targetValue != target) { if (!enabled) { // No transition when moving to a disabled state animatable.snapTo(target) } else { val lastInteraction = when (animatable.targetValue) { pressedElevation -> PressInteraction.Press(Offset.Zero) hoveredElevation -> HoverInteraction.Enter() focusedElevation -> FocusInteraction.Focus() else -> null } animatable.animateElevation( from = lastInteraction, to = interaction, target = target, ) } } } return animatable.asState() } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is ButtonElevation) return false if (defaultElevation != other.defaultElevation) return false if (pressedElevation != other.pressedElevation) return false if (focusedElevation != other.focusedElevation) return false if (hoveredElevation != other.hoveredElevation) return false if (disabledElevation != other.disabledElevation) return false return true } override fun hashCode(): Int { var result = defaultElevation.hashCode() result = 31 * result + pressedElevation.hashCode() result = 31 * result + focusedElevation.hashCode() result = 31 * result + hoveredElevation.hashCode() result = 31 * result + disabledElevation.hashCode() return result } } /** * Represents the container and content colors used in a button in different states. * * @param containerColor the container color of this [Button] when enabled. * @param contentColor the content color of this [Button] when enabled. * @param disabledContainerColor the container color of this [Button] when not enabled. * @param disabledContentColor the content color of this [Button] when not enabled. * @constructor create an instance with arbitrary colors. * - See [ButtonDefaults.buttonColors] for the default colors used in a [Button]. * - See [ButtonDefaults.elevatedButtonColors] for the default colors used in a [ElevatedButton]. * - See [ButtonDefaults.textButtonColors] for the default colors used in a [TextButton]. */ @Immutable class ButtonColors constructor( val containerColor: Color, val contentColor: Color, val disabledContainerColor: Color, val disabledContentColor: Color, ) { /** * Returns a copy of this ButtonColors, optionally overriding some of the values. This uses the * Color.Unspecified to mean “use the value from the source” */ fun copy( containerColor: Color = this.containerColor, contentColor: Color = this.contentColor, disabledContainerColor: Color = this.disabledContainerColor, disabledContentColor: Color = this.disabledContentColor, ) = ButtonColors( containerColor.takeOrElse { this.containerColor }, contentColor.takeOrElse { this.contentColor }, disabledContainerColor.takeOrElse { this.disabledContainerColor }, disabledContentColor.takeOrElse { this.disabledContentColor }, ) /** * Represents the container color for this button, depending on [enabled]. * * @param enabled whether the button is enabled */ @Stable internal fun containerColor(enabled: Boolean): Color = if (enabled) containerColor else disabledContainerColor /** * Represents the content color for this button, depending on [enabled]. * * @param enabled whether the button is enabled */ @Stable internal fun contentColor(enabled: Boolean): Color = if (enabled) contentColor else disabledContentColor override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is ButtonColors) return false if (containerColor != other.containerColor) return false if (contentColor != other.contentColor) return false if (disabledContainerColor != other.disabledContainerColor) return false if (disabledContentColor != other.disabledContentColor) return false return true } override fun hashCode(): Int { var result = containerColor.hashCode() result = 31 * result + contentColor.hashCode() result = 31 * result + disabledContainerColor.hashCode() result = 31 * result + disabledContentColor.hashCode() return result } } /** * The shapes that will be used in buttons. Button will morph between these shapes depending on the * interaction of the button, assuming all of the shapes are [CornerBasedShape]s. * * @property shape is the active shape. * @property pressedShape is the pressed shape. */ @ExperimentalMaterial3ExpressiveApi @Immutable class ButtonShapes(val shape: Shape, val pressedShape: Shape) { /** Returns a copy of this ButtonShapes, optionally overriding some of the values. */ fun copy(shape: Shape? = this.shape, pressedShape: Shape? = this.pressedShape) = ButtonShapes( shape = shape.takeOrElse { this.shape }, pressedShape = pressedShape.takeOrElse { this.pressedShape }, ) internal fun Shape?.takeOrElse(block: () -> Shape): Shape = this ?: block() override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is ButtonShapes) return false if (shape != other.shape) return false if (pressedShape != other.pressedShape) return false return true } override fun hashCode(): Int { var result = shape.hashCode() result = 31 * result + pressedShape.hashCode() return result } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal val ButtonShapes.hasRoundedCornerShapes: Boolean get() = shape is RoundedCornerShape && pressedShape is RoundedCornerShape @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal val ButtonShapes.hasCornerBasedShapes: Boolean get() = shape is CornerBasedShape && pressedShape is CornerBasedShape @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun shapeByInteraction( shapes: ButtonShapes, pressed: Boolean, animationSpec: FiniteAnimationSpec, ): Shape { val shape = if (pressed) { shapes.pressedShape } else { shapes.shape } if (shapes.hasRoundedCornerShapes) return key(shapes) { rememberAnimatedShape(shape as RoundedCornerShape, animationSpec) } else if (shapes.hasCornerBasedShapes) return key(shapes) { rememberAnimatedShape(shape as CornerBasedShape, animationSpec) } return shape } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Card.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.interaction.DragInteraction import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.material3.internal.animateElevation import androidx.compose.material3.tokens.ElevatedCardTokens import androidx.compose.material3.tokens.FilledCardTokens import androidx.compose.material3.tokens.OutlinedCardTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.Dp /** * [Material Design filled card](https://m3.material.io/components/cards/overview) * * Cards contain contain content and actions that relate information about a subject. Filled cards * provide subtle separation from the background. This has less emphasis than elevated or outlined * cards. * * This Card does not handle input events - see the other Card overloads if you want a clickable or * selectable Card. * * ![Filled card * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-card.png) * * Card sample: * * @sample androidx.compose.material3.samples.CardSample * @param modifier the [Modifier] to be applied to this card * @param shape defines the shape of this card's container, border (when [border] is not null), and * shadow (when using [elevation]) * @param colors [CardColors] that will be used to resolve the colors used for this card in * different states. See [CardDefaults.cardColors]. * @param elevation [CardElevation] used to resolve the elevation for this card in different states. * This controls the size of the shadow below the card. Additionally, when the container color is * [ColorScheme.surface], this controls the amount of primary color applied as an overlay. See * also: [Surface]. * @param border the border to draw around the container of this card * @param content The content displayed on the card */ @Composable fun Card( modifier: Modifier = Modifier, shape: Shape = CardDefaults.shape, colors: CardColors = CardDefaults.cardColors(), elevation: CardElevation = CardDefaults.cardElevation(), border: BorderStroke? = null, content: @Composable ColumnScope.() -> Unit, ) { Surface( modifier = modifier, shape = shape, color = colors.containerColor(enabled = true), contentColor = colors.contentColor(enabled = true), shadowElevation = elevation.shadowElevation(enabled = true, interactionSource = null).value, border = border, ) { Column(content = content) } } /** * [Material Design filled card](https://m3.material.io/components/cards/overview) * * Cards contain contain content and actions that relate information about a subject. Filled cards * provide subtle separation from the background. This has less emphasis than elevated or outlined * cards. * * This Card handles click events, calling its [onClick] lambda. * * ![Filled card * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-card.png) * * Clickable card sample: * * @sample androidx.compose.material3.samples.ClickableCardSample * @param onClick called when this card is clicked * @param modifier the [Modifier] to be applied to this card * @param enabled controls the enabled state of this card. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this card's container, border (when [border] is not null), and * shadow (when using [elevation]) * @param colors [CardColors] that will be used to resolve the color(s) used for this card in * different states. See [CardDefaults.cardColors]. * @param elevation [CardElevation] used to resolve the elevation for this card in different states. * This controls the size of the shadow below the card. Additionally, when the container color is * [ColorScheme.surface], this controls the amount of primary color applied as an overlay. See * also: [Surface]. * @param border the border to draw around the container of this card * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this card. You can use this to change the card's appearance or * preview the card in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content The content displayed on the card */ @Composable fun Card( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = CardDefaults.shape, colors: CardColors = CardDefaults.cardColors(), elevation: CardElevation = CardDefaults.cardElevation(), border: BorderStroke? = null, interactionSource: MutableInteractionSource? = null, content: @Composable ColumnScope.() -> Unit, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } Surface( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, color = colors.containerColor(enabled), contentColor = colors.contentColor(enabled), shadowElevation = elevation.shadowElevation(enabled, interactionSource).value, border = border, interactionSource = interactionSource, ) { Column(content = content) } } /** * [Material Design elevated card](https://m3.material.io/components/cards/overview) * * Elevated cards contain content and actions that relate information about a subject. They have a * drop shadow, providing more separation from the background than filled cards, but less than * outlined cards. * * This ElevatedCard does not handle input events - see the other ElevatedCard overloads if you want * a clickable or selectable ElevatedCard. * * ![Elevated card * image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-card.png) * * Elevated card sample: * * @sample androidx.compose.material3.samples.ElevatedCardSample * @param modifier the [Modifier] to be applied to this card * @param shape defines the shape of this card's container and shadow (when using [elevation]) * @param colors [CardColors] that will be used to resolve the color(s) used for this card in * different states. See [CardDefaults.elevatedCardElevation]. * @param elevation [CardElevation] used to resolve the elevation for this card in different states. * This controls the size of the shadow below the card. Additionally, when the container color is * [ColorScheme.surface], this controls the amount of primary color applied as an overlay. See * also: [Surface]. * @param content The content displayed on the card */ @Composable fun ElevatedCard( modifier: Modifier = Modifier, shape: Shape = CardDefaults.elevatedShape, colors: CardColors = CardDefaults.elevatedCardColors(), elevation: CardElevation = CardDefaults.elevatedCardElevation(), content: @Composable ColumnScope.() -> Unit, ) = Card( modifier = modifier, shape = shape, border = null, elevation = elevation, colors = colors, content = content, ) /** * [Material Design elevated card](https://m3.material.io/components/cards/overview) * * Elevated cards contain content and actions that relate information about a subject. They have a * drop shadow, providing more separation from the background than filled cards, but less than * outlined cards. * * This ElevatedCard handles click events, calling its [onClick] lambda. * * ![Elevated card * image](https://developer.android.com/images/reference/androidx/compose/material3/elevated-card.png) * * Clickable elevated card sample: * * @sample androidx.compose.material3.samples.ClickableElevatedCardSample * @param onClick called when this card is clicked * @param modifier the [Modifier] to be applied to this card * @param enabled controls the enabled state of this card. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this card's container and shadow (when using [elevation]) * @param colors [CardColors] that will be used to resolve the color(s) used for this card in * different states. See [CardDefaults.elevatedCardElevation]. * @param elevation [CardElevation] used to resolve the elevation for this card in different states. * This controls the size of the shadow below the card. Additionally, when the container color is * [ColorScheme.surface], this controls the amount of primary color applied as an overlay. See * also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this card. You can use this to change the card's appearance or * preview the card in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content The content displayed on the card */ @Composable fun ElevatedCard( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = CardDefaults.elevatedShape, colors: CardColors = CardDefaults.elevatedCardColors(), elevation: CardElevation = CardDefaults.elevatedCardElevation(), interactionSource: MutableInteractionSource? = null, content: @Composable ColumnScope.() -> Unit, ) = Card( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, colors = colors, elevation = elevation, border = null, interactionSource = interactionSource, content = content, ) /** * [Material Design outlined card](https://m3.material.io/components/cards/overview) * * Outlined cards contain content and actions that relate information about a subject. They have a * visual boundary around the container. This can provide greater emphasis than the other types. * * This OutlinedCard does not handle input events - see the other OutlinedCard overloads if you want * a clickable or selectable OutlinedCard. * * ![Outlined card * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-card.png) * * Outlined card sample: * * @sample androidx.compose.material3.samples.OutlinedCardSample * @param modifier the [Modifier] to be applied to this card * @param shape defines the shape of this card's container, border (when [border] is not null), and * shadow (when using [elevation]) * @param colors [CardColors] that will be used to resolve the color(s) used for this card in * different states. See [CardDefaults.outlinedCardColors]. * @param elevation [CardElevation] used to resolve the elevation for this card in different states. * This controls the size of the shadow below the card. Additionally, when the container color is * [ColorScheme.surface], this controls the amount of primary color applied as an overlay. See * also: [Surface]. * @param border the border to draw around the container of this card * @param content The content displayed on the card */ @Composable fun OutlinedCard( modifier: Modifier = Modifier, shape: Shape = CardDefaults.outlinedShape, colors: CardColors = CardDefaults.outlinedCardColors(), elevation: CardElevation = CardDefaults.outlinedCardElevation(), border: BorderStroke = CardDefaults.outlinedCardBorder(), content: @Composable ColumnScope.() -> Unit, ) = Card( modifier = modifier, shape = shape, colors = colors, elevation = elevation, border = border, content = content, ) /** * [Material Design outlined card](https://m3.material.io/components/cards/overview) * * Outlined cards contain content and actions that relate information about a subject. They have a * visual boundary around the container. This can provide greater emphasis than the other types. * * This OutlinedCard handles click events, calling its [onClick] lambda. * * ![Outlined card * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-card.png) * * Clickable outlined card sample: * * @sample androidx.compose.material3.samples.ClickableOutlinedCardSample * @param onClick called when this card is clicked * @param modifier the [Modifier] to be applied to this card * @param enabled controls the enabled state of this card. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param shape defines the shape of this card's container, border (when [border] is not null), and * shadow (when using [elevation]) * @param colors [CardColors] that will be used to resolve the color(s) used for this card in * different states. See [CardDefaults.outlinedCardColors]. * @param elevation [CardElevation] used to resolve the elevation for this card in different states. * This controls the size of the shadow below the card. Additionally, when the container color is * [ColorScheme.surface], this controls the amount of primary color applied as an overlay. See * also: [Surface]. * @param border the border to draw around the container of this card * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this card. You can use this to change the card's appearance or * preview the card in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content The content displayed on the card */ @Composable fun OutlinedCard( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = CardDefaults.outlinedShape, colors: CardColors = CardDefaults.outlinedCardColors(), elevation: CardElevation = CardDefaults.outlinedCardElevation(), border: BorderStroke = CardDefaults.outlinedCardBorder(enabled), interactionSource: MutableInteractionSource? = null, content: @Composable ColumnScope.() -> Unit, ) = Card( onClick = onClick, modifier = modifier, enabled = enabled, shape = shape, colors = colors, elevation = elevation, border = border, interactionSource = interactionSource, content = content, ) /** Contains the default values used by all card types. */ object CardDefaults { // shape Defaults /** Default shape for a card. */ val shape: Shape @Composable get() = FilledCardTokens.ContainerShape.value /** Default shape for an elevated card. */ val elevatedShape: Shape @Composable get() = ElevatedCardTokens.ContainerShape.value /** Default shape for an outlined card. */ val outlinedShape: Shape @Composable get() = OutlinedCardTokens.ContainerShape.value /** * Creates a [CardElevation] that will animate between the provided values according to the * Material specification for a [Card]. * * @param defaultElevation the elevation used when the [Card] is has no other [Interaction]s. * @param pressedElevation the elevation used when the [Card] is pressed. * @param focusedElevation the elevation used when the [Card] is focused. * @param hoveredElevation the elevation used when the [Card] is hovered. * @param draggedElevation the elevation used when the [Card] is dragged. * @param disabledElevation the elevation used when the [Card] is disabled. */ @Composable fun cardElevation( defaultElevation: Dp = FilledCardTokens.ContainerElevation, pressedElevation: Dp = FilledCardTokens.PressedContainerElevation, focusedElevation: Dp = FilledCardTokens.FocusContainerElevation, hoveredElevation: Dp = FilledCardTokens.HoverContainerElevation, draggedElevation: Dp = FilledCardTokens.DraggedContainerElevation, disabledElevation: Dp = FilledCardTokens.DisabledContainerElevation, ): CardElevation = CardElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, draggedElevation = draggedElevation, disabledElevation = disabledElevation, ) /** * Creates a [CardElevation] that will animate between the provided values according to the * Material specification for an [ElevatedCard]. * * @param defaultElevation the elevation used when the [ElevatedCard] is has no other * [Interaction]s. * @param pressedElevation the elevation used when the [ElevatedCard] is pressed. * @param focusedElevation the elevation used when the [ElevatedCard] is focused. * @param hoveredElevation the elevation used when the [ElevatedCard] is hovered. * @param draggedElevation the elevation used when the [ElevatedCard] is dragged. * @param disabledElevation the elevation used when the [Card] is disabled. */ @Composable fun elevatedCardElevation( defaultElevation: Dp = ElevatedCardTokens.ContainerElevation, pressedElevation: Dp = ElevatedCardTokens.PressedContainerElevation, focusedElevation: Dp = ElevatedCardTokens.FocusContainerElevation, hoveredElevation: Dp = ElevatedCardTokens.HoverContainerElevation, draggedElevation: Dp = ElevatedCardTokens.DraggedContainerElevation, disabledElevation: Dp = ElevatedCardTokens.DisabledContainerElevation, ): CardElevation = CardElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, draggedElevation = draggedElevation, disabledElevation = disabledElevation, ) /** * Creates a [CardElevation] that will animate between the provided values according to the * Material specification for an [OutlinedCard]. * * @param defaultElevation the elevation used when the [OutlinedCard] is has no other * [Interaction]s. * @param pressedElevation the elevation used when the [OutlinedCard] is pressed. * @param focusedElevation the elevation used when the [OutlinedCard] is focused. * @param hoveredElevation the elevation used when the [OutlinedCard] is hovered. * @param draggedElevation the elevation used when the [OutlinedCard] is dragged. */ @Composable fun outlinedCardElevation( defaultElevation: Dp = OutlinedCardTokens.ContainerElevation, pressedElevation: Dp = defaultElevation, focusedElevation: Dp = defaultElevation, hoveredElevation: Dp = defaultElevation, draggedElevation: Dp = OutlinedCardTokens.DraggedContainerElevation, disabledElevation: Dp = OutlinedCardTokens.DisabledContainerElevation, ): CardElevation = CardElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, draggedElevation = draggedElevation, disabledElevation = disabledElevation, ) /** * Creates a [CardColors] that represents the default container and content colors used in a * [Card]. */ @Composable fun cardColors() = MaterialTheme.colorScheme.defaultCardColors /** * Creates a [CardColors] that represents the default container and content colors used in a * [Card]. * * @param containerColor the container color of this [Card] when enabled. * @param contentColor the content color of this [Card] when enabled. * @param disabledContainerColor the container color of this [Card] when not enabled. * @param disabledContentColor the content color of this [Card] when not enabled. */ @Composable fun cardColors( containerColor: Color = Color.Unspecified, contentColor: Color = contentColorFor(containerColor), disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = contentColor.copy(DisabledAlpha), ): CardColors = MaterialTheme.colorScheme.defaultCardColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultCardColors: CardColors get() { return defaultCardColorsCached ?: CardColors( containerColor = fromToken(FilledCardTokens.ContainerColor), contentColor = contentColorFor(fromToken(FilledCardTokens.ContainerColor)), disabledContainerColor = fromToken(FilledCardTokens.DisabledContainerColor) .copy(alpha = FilledCardTokens.DisabledContainerOpacity) .compositeOver(fromToken(FilledCardTokens.ContainerColor)), disabledContentColor = contentColorFor(fromToken(FilledCardTokens.ContainerColor)) .copy(DisabledAlpha), ) .also { defaultCardColorsCached = it } } /** * Creates a [CardColors] that represents the default container and content colors used in an * [ElevatedCard]. */ @Composable fun elevatedCardColors() = MaterialTheme.colorScheme.defaultElevatedCardColors /** * Creates a [CardColors] that represents the default container and content colors used in an * [ElevatedCard]. * * @param containerColor the container color of this [ElevatedCard] when enabled. * @param contentColor the content color of this [ElevatedCard] when enabled. * @param disabledContainerColor the container color of this [ElevatedCard] when not enabled. * @param disabledContentColor the content color of this [ElevatedCard] when not enabled. */ @Composable fun elevatedCardColors( containerColor: Color = Color.Unspecified, contentColor: Color = contentColorFor(containerColor), disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = contentColor.copy(DisabledAlpha), ): CardColors = MaterialTheme.colorScheme.defaultElevatedCardColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultElevatedCardColors: CardColors get() { return defaultElevatedCardColorsCached ?: CardColors( containerColor = fromToken(ElevatedCardTokens.ContainerColor), contentColor = contentColorFor(fromToken(ElevatedCardTokens.ContainerColor)), disabledContainerColor = fromToken(ElevatedCardTokens.DisabledContainerColor) .copy(alpha = ElevatedCardTokens.DisabledContainerOpacity) .compositeOver( fromToken(ElevatedCardTokens.DisabledContainerColor) ), disabledContentColor = contentColorFor(fromToken(ElevatedCardTokens.ContainerColor)) .copy(DisabledAlpha), ) .also { defaultElevatedCardColorsCached = it } } /** * Creates a [CardColors] that represents the default container and content colors used in an * [OutlinedCard]. */ @Composable fun outlinedCardColors() = MaterialTheme.colorScheme.defaultOutlinedCardColors /** * Creates a [CardColors] that represents the default container and content colors used in an * [OutlinedCard]. * * @param containerColor the container color of this [OutlinedCard] when enabled. * @param contentColor the content color of this [OutlinedCard] when enabled. * @param disabledContainerColor the container color of this [OutlinedCard] when not enabled. * @param disabledContentColor the content color of this [OutlinedCard] when not enabled. */ @Composable fun outlinedCardColors( containerColor: Color = Color.Unspecified, contentColor: Color = contentColorFor(containerColor), disabledContainerColor: Color = Color.Unspecified, disabledContentColor: Color = contentColorFor(containerColor).copy(DisabledAlpha), ): CardColors = MaterialTheme.colorScheme.defaultOutlinedCardColors.copy( containerColor = containerColor, contentColor = contentColor, disabledContainerColor = disabledContainerColor, disabledContentColor = disabledContentColor, ) internal val ColorScheme.defaultOutlinedCardColors: CardColors get() { return defaultOutlinedCardColorsCached ?: CardColors( containerColor = fromToken(OutlinedCardTokens.ContainerColor), contentColor = contentColorFor(fromToken(OutlinedCardTokens.ContainerColor)), disabledContainerColor = fromToken(OutlinedCardTokens.ContainerColor), disabledContentColor = contentColorFor(fromToken(OutlinedCardTokens.ContainerColor)) .copy(DisabledAlpha), ) .also { defaultOutlinedCardColorsCached = it } } /** * Creates a [BorderStroke] that represents the default border used in [OutlinedCard]. * * @param enabled whether the card is enabled */ @Composable fun outlinedCardBorder(enabled: Boolean = true): BorderStroke { val color = if (enabled) { OutlinedCardTokens.OutlineColor.value } else { OutlinedCardTokens.DisabledOutlineColor.value .copy(alpha = OutlinedCardTokens.DisabledOutlineOpacity) .compositeOver(ElevatedCardTokens.ContainerColor.value) } return remember(color) { BorderStroke(OutlinedCardTokens.OutlineWidth, color) } } } /** * Represents the elevation for a card in different states. * - See [CardDefaults.cardElevation] for the default elevation used in a [Card]. * - See [CardDefaults.elevatedCardElevation] for the default elevation used in an [ElevatedCard]. * - See [CardDefaults.outlinedCardElevation] for the default elevation used in an [OutlinedCard]. */ @Immutable class CardElevation internal constructor( private val defaultElevation: Dp, private val pressedElevation: Dp, private val focusedElevation: Dp, private val hoveredElevation: Dp, private val draggedElevation: Dp, private val disabledElevation: Dp, ) { /** * Represents the shadow elevation used in a card, depending on its [enabled] state and * [interactionSource]. * * Shadow elevation is used to apply a shadow around the card to give it higher emphasis. * * @param enabled whether the card is enabled * @param interactionSource the [InteractionSource] for this card */ @Composable internal fun shadowElevation( enabled: Boolean, interactionSource: InteractionSource?, ): State { if (interactionSource == null) { return remember { mutableStateOf(defaultElevation) } } return animateElevation(enabled = enabled, interactionSource = interactionSource) } @Composable private fun animateElevation( enabled: Boolean, interactionSource: InteractionSource, ): State { val interactions = remember { mutableStateListOf() } LaunchedEffect(interactionSource) { interactionSource.interactions.collect { interaction -> when (interaction) { is HoverInteraction.Enter -> { interactions.add(interaction) } is HoverInteraction.Exit -> { interactions.remove(interaction.enter) } is FocusInteraction.Focus -> { interactions.add(interaction) } is FocusInteraction.Unfocus -> { interactions.remove(interaction.focus) } is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } is DragInteraction.Start -> { interactions.add(interaction) } is DragInteraction.Stop -> { interactions.remove(interaction.start) } is DragInteraction.Cancel -> { interactions.remove(interaction.start) } } } } val interaction = interactions.lastOrNull() val target = if (!enabled) { disabledElevation } else { when (interaction) { is PressInteraction.Press -> pressedElevation is HoverInteraction.Enter -> hoveredElevation is FocusInteraction.Focus -> focusedElevation is DragInteraction.Start -> draggedElevation else -> defaultElevation } } val animatable = remember { Animatable(target, Dp.VectorConverter) } LaunchedEffect(target) { if (animatable.targetValue != target) { if (!enabled) { // No transition when moving to a disabled state. animatable.snapTo(target) } else { val lastInteraction = when (animatable.targetValue) { pressedElevation -> PressInteraction.Press(Offset.Zero) hoveredElevation -> HoverInteraction.Enter() focusedElevation -> FocusInteraction.Focus() draggedElevation -> DragInteraction.Start() else -> null } animatable.animateElevation( from = lastInteraction, to = interaction, target = target, ) } } } return animatable.asState() } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is CardElevation) return false if (defaultElevation != other.defaultElevation) return false if (pressedElevation != other.pressedElevation) return false if (focusedElevation != other.focusedElevation) return false if (hoveredElevation != other.hoveredElevation) return false if (disabledElevation != other.disabledElevation) return false return true } override fun hashCode(): Int { var result = defaultElevation.hashCode() result = 31 * result + pressedElevation.hashCode() result = 31 * result + focusedElevation.hashCode() result = 31 * result + hoveredElevation.hashCode() result = 31 * result + disabledElevation.hashCode() return result } } /** * Represents the container and content colors used in a card in different states. * * @param containerColor the container color of this [Card] when enabled. * @param contentColor the content color of this [Card] when enabled. * @param disabledContainerColor the container color of this [Card] when not enabled. * @param disabledContentColor the content color of this [Card] when not enabled. * @constructor create an instance with arbitrary colors. * - See [CardDefaults.cardColors] for the default colors used in a [Card]. * - See [CardDefaults.elevatedCardColors] for the default colors used in a [ElevatedCard]. * - See [CardDefaults.outlinedCardColors] for the default colors used in a [OutlinedCard]. */ @Immutable class CardColors constructor( val containerColor: Color, val contentColor: Color, val disabledContainerColor: Color, val disabledContentColor: Color, ) { /** * Returns a copy of this CardColors, optionally overriding some of the values. This uses the * Color.Unspecified to mean “use the value from the source” */ fun copy( containerColor: Color = this.containerColor, contentColor: Color = this.contentColor, disabledContainerColor: Color = this.disabledContainerColor, disabledContentColor: Color = this.disabledContentColor, ) = CardColors( containerColor.takeOrElse { this.containerColor }, contentColor.takeOrElse { this.contentColor }, disabledContainerColor.takeOrElse { this.disabledContainerColor }, disabledContentColor.takeOrElse { this.disabledContentColor }, ) /** * Represents the container color for this card, depending on [enabled]. * * @param enabled whether the card is enabled */ @Stable internal fun containerColor(enabled: Boolean): Color = if (enabled) containerColor else disabledContainerColor /** * Represents the content color for this card, depending on [enabled]. * * @param enabled whether the card is enabled */ @Stable internal fun contentColor(enabled: Boolean) = if (enabled) contentColor else disabledContentColor override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is CardColors) return false if (containerColor != other.containerColor) return false if (contentColor != other.contentColor) return false if (disabledContainerColor != other.disabledContainerColor) return false if (disabledContentColor != other.disabledContentColor) return false return true } override fun hashCode(): Int { var result = containerColor.hashCode() result = 31 * result + contentColor.hashCode() result = 31 * result + disabledContainerColor.hashCode() result = 31 * result + disabledContentColor.hashCode() return result } } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Scaffold.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged import androidx.compose.material3.internal.MutableWindowInsets import androidx.compose.material3.internal.systemBarsForVisualComponents import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.offset /** * [Material Design layout](https://m3.material.io/foundations/layout/understanding-layout/) * * Scaffold implements the basic Material Design visual layout structure. * * This component provides API to put together several Material components to construct your screen, * by ensuring proper layout strategy for them and collecting necessary data so these components * will work together correctly. * * Simple example of a Scaffold with [TopAppBar] and [FloatingActionButton]: * * @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar * * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. * * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar * @param modifier the [Modifier] to be applied to this scaffold * @param topBar top app bar of the screen, typically a [TopAppBar] * @param bottomBar bottom bar of the screen, typically a [NavigationBar] * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via * [SnackbarHostState.showSnackbar], typically a [SnackbarHost] * @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton] * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition]. * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this scaffold. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param contentWindowInsets window insets to be passed to [content] slot via [PaddingValues] * params. Scaffold will take the insets into account from the top/bottom only if the [topBar]/ * [bottomBar] are not present, as the scaffold expect [topBar]/[bottomBar] to handle insets * instead. Any insets consumed by other insets padding modifiers or [consumeWindowInsets] on a * parent layout will be excluded from [contentWindowInsets]. * @param content content of the screen. The lambda receives a [PaddingValues] that should be * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to * the child of the scroll, and not on the scroll itself. */ @Composable fun Scaffold( modifier: Modifier = Modifier, topBar: @Composable () -> Unit = {}, bottomBar: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {}, floatingActionButtonPosition: FabPosition = FabPosition.End, containerColor: Color = MaterialTheme.colorScheme.background, contentColor: Color = contentColorFor(containerColor), contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, content: @Composable (PaddingValues) -> Unit, ) { val safeInsets = remember(contentWindowInsets) { MutableWindowInsets(contentWindowInsets) } Surface( modifier = modifier.onConsumedWindowInsetsChanged { consumedWindowInsets -> // Exclude currently consumed window insets from user provided contentWindowInsets safeInsets.insets = contentWindowInsets.exclude(consumedWindowInsets) }, color = containerColor, contentColor = contentColor, ) { ScaffoldLayout( fabPosition = floatingActionButtonPosition, topBar = topBar, bottomBar = bottomBar, content = content, snackbar = snackbarHost, contentWindowInsets = safeInsets, fab = floatingActionButton, ) } } /** * Layout for a [Scaffold]'s content. * * @param fabPosition [FabPosition] for the FAB (if present) * @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar] * @param content the main 'body' of the [Scaffold] * @param snackbar the [Snackbar] displayed on top of the [content] * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar] and * above the [bottomBar] * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the [content], * typically a [NavigationBar]. */ @Composable private fun ScaffoldLayout( fabPosition: FabPosition, topBar: @Composable () -> Unit, content: @Composable (PaddingValues) -> Unit, snackbar: @Composable () -> Unit, fab: @Composable () -> Unit, contentWindowInsets: WindowInsets, bottomBar: @Composable () -> Unit, ) { // Create the backing value for the content padding // These values will be updated during measurement, but before subcomposing the body content // Remembering and updating a single PaddingValues avoids needing to recompose when the values // change val contentPadding = remember { object : PaddingValues { var paddingHolder by mutableStateOf(PaddingValues(0.dp)) override fun calculateLeftPadding(layoutDirection: LayoutDirection): Dp = paddingHolder.calculateLeftPadding(layoutDirection) override fun calculateTopPadding(): Dp = paddingHolder.calculateTopPadding() override fun calculateRightPadding(layoutDirection: LayoutDirection): Dp = paddingHolder.calculateRightPadding(layoutDirection) override fun calculateBottomPadding(): Dp = paddingHolder.calculateBottomPadding() } } val topBarContent: @Composable () -> Unit = remember(topBar) { { Box { topBar() } } } val snackbarContent: @Composable () -> Unit = remember(snackbar) { { Box { snackbar() } } } val fabContent: @Composable () -> Unit = remember(fab) { { Box { fab() } } } val bodyContent: @Composable () -> Unit = remember(content, contentPadding) { { Box { content(contentPadding) } } } val bottomBarContent: @Composable () -> Unit = remember(bottomBar) { { Box { bottomBar() } } } SubcomposeLayout { constraints -> val layoutWidth = constraints.maxWidth val layoutHeight = constraints.maxHeight val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) // respect only bottom and horizontal for snackbar and fab val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection) val rightInset = contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection) val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout) val topBarPlaceable = subcompose(ScaffoldLayoutContent.TopBar, topBarContent) .first() .measure(looseConstraints) val snackbarPlaceable = subcompose(ScaffoldLayoutContent.Snackbar, snackbarContent) .first() .measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset)) val fabPlaceable = subcompose(ScaffoldLayoutContent.Fab, fabContent) .first() .measure(looseConstraints.offset(-leftInset - rightInset, -bottomInset)) val isFabEmpty = fabPlaceable.width == 0 && fabPlaceable.height == 0 val fabPlacement = if (!isFabEmpty) { val fabWidth = fabPlaceable.width val fabHeight = fabPlaceable.height // FAB distance from the left of the layout, taking into account LTR / RTL val fabLeftOffset = when (fabPosition) { FabPosition.Start -> { if (layoutDirection == LayoutDirection.Ltr) { FabSpacing.roundToPx() + leftInset } else { layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset } } FabPosition.End, FabPosition.EndOverlay -> { if (layoutDirection == LayoutDirection.Ltr) { layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset } else { FabSpacing.roundToPx() + leftInset } } else -> (layoutWidth - fabWidth + leftInset - rightInset) / 2 } FabPlacement(left = fabLeftOffset, width = fabWidth, height = fabHeight) } else { null } val bottomBarPlaceable = subcompose(ScaffoldLayoutContent.BottomBar, bottomBarContent) .first() .measure(looseConstraints) val isBottomBarEmpty = bottomBarPlaceable.width == 0 && bottomBarPlaceable.height == 0 val fabOffsetFromBottom = fabPlacement?.let { if (isBottomBarEmpty || fabPosition == FabPosition.EndOverlay) { it.height + FabSpacing.roundToPx() + contentWindowInsets.getBottom(this@SubcomposeLayout) } else { // Total height is the bottom bar height + the FAB height + the padding // between the FAB and bottom bar bottomBarPlaceable.height + it.height + FabSpacing.roundToPx() } } val snackbarHeight = snackbarPlaceable.height val snackbarOffsetFromBottom = if (snackbarHeight != 0) { snackbarHeight + (fabOffsetFromBottom ?: bottomBarPlaceable.height.takeIf { !isBottomBarEmpty } ?: contentWindowInsets.getBottom(this@SubcomposeLayout)) } else { 0 } // Update the backing state for the content padding before subcomposing the body val insets = contentWindowInsets.asPaddingValues(this) contentPadding.paddingHolder = PaddingValues( top = if (topBarPlaceable.width == 0 && topBarPlaceable.height == 0) { insets.calculateTopPadding() } else { topBarPlaceable.height.toDp() }, bottom = if (isBottomBarEmpty) { insets.calculateBottomPadding() } else { bottomBarPlaceable.height.toDp() }, start = insets.calculateStartPadding(layoutDirection), end = insets.calculateEndPadding(layoutDirection), ) val bodyContentPlaceable = subcompose(ScaffoldLayoutContent.MainContent, bodyContent) .first() .measure(looseConstraints) layout(layoutWidth, layoutHeight) { // Placing to control drawing order to match default elevation of each placeable bodyContentPlaceable.place(0, 0) topBarPlaceable.place(0, 0) snackbarPlaceable.place( (layoutWidth - snackbarPlaceable.width + contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection) - contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)) / 2, layoutHeight - snackbarOffsetFromBottom, ) // The bottom bar is always at the bottom of the layout bottomBarPlaceable.place(0, layoutHeight - (bottomBarPlaceable.height)) // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL fabPlacement?.let { placement -> fabPlaceable.place(placement.left, layoutHeight - fabOffsetFromBottom!!) } } } } /** Object containing various default values for [Scaffold] component. */ object ScaffoldDefaults { /** Default insets to be used and consumed by the scaffold content slot */ val contentWindowInsets: WindowInsets @Composable get() = WindowInsets.systemBarsForVisualComponents } /** The possible positions for a [FloatingActionButton] attached to a [Scaffold]. */ @kotlin.jvm.JvmInline value class FabPosition internal constructor(@Suppress("unused") private val value: Int) { companion object { /** * Position FAB at the bottom of the screen at the start, above the [NavigationBar] (if it * exists) */ val Start = FabPosition(0) /** * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it * exists) */ val Center = FabPosition(1) /** * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it * exists) */ val End = FabPosition(2) /** * Position FAB at the bottom of the screen at the end, overlaying the [NavigationBar] (if * it exists) */ val EndOverlay = FabPosition(3) } override fun toString(): String { return when (this) { Start -> "FabPosition.Start" Center -> "FabPosition.Center" End -> "FabPosition.End" else -> "FabPosition.EndOverlay" } } } /** * Placement information for a [FloatingActionButton] inside a [Scaffold]. * * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL * support * @property width the width of the FAB * @property height the height of the FAB */ @Immutable internal class FabPlacement(val left: Int, val width: Int, val height: Int) // FAB spacing above the bottom bar / bottom of the Scaffold private val FabSpacing = 16.dp private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar, } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationBar.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.DefaultNavigationBarOverride.NavigationBar import androidx.compose.material3.internal.MappedInteractionSource import androidx.compose.material3.internal.ProvideContentColorTextStyle import androidx.compose.material3.internal.systemBarsForVisualComponents import androidx.compose.material3.tokens.ElevationTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.NavigationBarTokens import androidx.compose.material3.tokens.NavigationBarVerticalItemTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import kotlin.math.roundToInt /** * [Material Design bottom navigation * bar](https://m3.material.io/components/navigation-bar/overview) * * Navigation bars offer a persistent and convenient way to switch between primary destinations in * an app. * * ![Navigation bar * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-bar.png) * * [NavigationBar] should contain three to five [NavigationBarItem]s, each representing a singular * destination. * * A simple example looks like: * * @sample androidx.compose.material3.samples.NavigationBarSample * * See [NavigationBarItem] for configuration specific to each item, and not the overall * [NavigationBar] component. * * @param modifier the [Modifier] to be applied to this navigation bar * @param containerColor the color used for the background of this navigation bar. Use * [Color.Transparent] to have no color. * @param contentColor the preferred color for content inside this navigation bar. Defaults to * either the matching content color for [containerColor], or to the current [LocalContentColor] * if [containerColor] is not a color from the theme. * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color * overlay is applied on top of the container. A higher tonal elevation value will result in a * darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets of the navigation bar. * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s */ @OptIn(ExperimentalMaterial3ComponentOverrideApi::class) @Composable fun NavigationBar( modifier: Modifier = Modifier, containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), tonalElevation: Dp = NavigationBarDefaults.Elevation, windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, content: @Composable RowScope.() -> Unit, ) { with(LocalNavigationBarOverride.current) { NavigationBarOverrideScope( modifier = modifier, containerColor = containerColor, contentColor = contentColor, tonalElevation = tonalElevation, windowInsets = windowInsets, content = content, ) .NavigationBar() } } /** * This override provides the default behavior of the [NavigationBar] component. * * [NavigationBarOverride] used when no override is specified. */ @ExperimentalMaterial3ComponentOverrideApi object DefaultNavigationBarOverride : NavigationBarOverride { @Composable override fun NavigationBarOverrideScope.NavigationBar() { Surface( color = containerColor, contentColor = contentColor, tonalElevation = tonalElevation, modifier = modifier, ) { Row( modifier = Modifier.fillMaxWidth() .windowInsetsPadding(windowInsets) .defaultMinSize(minHeight = NavigationBarHeight) .selectableGroup(), horizontalArrangement = Arrangement.spacedBy(NavigationBarItemHorizontalPadding), verticalAlignment = Alignment.CenterVertically, content = content, ) } } } /** * Material Design navigation bar item. * * Navigation bars offer a persistent and convenient way to switch between primary destinations in * an app. * * The recommended configuration for a [NavigationBarItem] depends on how many items there are * inside a [NavigationBar]: * - Three destinations: Display icons and text labels for all destinations. * - Four destinations: Active destinations display an icon and text label. Inactive destinations * display icons, and text labels are recommended. * - Five destinations: Active destinations display an icon and text label. Inactive destinations * use icons, and use text labels if space permits. * * A [NavigationBarItem] always shows text labels (if it exists) when selected. Showing text labels * if not selected is controlled by [alwaysShowLabel]. * * @param selected whether this item is selected * @param onClick called when this item is clicked * @param icon icon for this item, typically an [Icon] * @param modifier the [Modifier] to be applied to this item * @param enabled controls the enabled state of this item. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param label optional text label for this item * @param alwaysShowLabel whether to always show the label for this item. If `false`, the label will * only be shown when this item is selected. * @param colors [NavigationBarItemColors] that will be used to resolve the colors used for this * item in different states. See [NavigationBarItemDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this item. You can use this to change the item's appearance or * preview the item in different states. Note that if `null` is provided, interactions will still * happen internally. */ @Composable fun RowScope.NavigationBarItem( selected: Boolean, onClick: () -> Unit, icon: @Composable () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, label: @Composable (() -> Unit)? = null, alwaysShowLabel: Boolean = true, colors: NavigationBarItemColors = NavigationBarItemDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // TODO Load the motionScheme tokens from the component tokens file val colorAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value() val styledIcon = @Composable { val iconColor by animateColorAsState( targetValue = colors.iconColor(selected = selected, enabled = enabled), animationSpec = colorAnimationSpec, ) // If there's a label, don't have a11y services repeat the icon description. val clearSemantics = label != null && (alwaysShowLabel || selected) Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) { CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) } } val styledLabel: @Composable (() -> Unit)? = label?.let { @Composable { val style = NavigationBarTokens.LabelTextFont.value val textColor by animateColorAsState( targetValue = colors.textColor(selected = selected, enabled = enabled), animationSpec = colorAnimationSpec, ) ProvideContentColorTextStyle( contentColor = textColor, textStyle = style, content = label, ) } } var itemWidth by remember { mutableIntStateOf(0) } Box( modifier .selectable( selected = selected, onClick = onClick, enabled = enabled, role = Role.Tab, interactionSource = interactionSource, indication = null, ) .defaultMinSize(minHeight = NavigationBarHeight) .weight(1f) .onSizeChanged { itemWidth = it.width }, contentAlignment = Alignment.Center, propagateMinConstraints = true, ) { val alphaAnimationProgress: State = animateFloatAsState( targetValue = if (selected) 1f else 0f, // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.DefaultEffects.value(), ) val sizeAnimationProgress: State = animateFloatAsState( targetValue = if (selected) 1f else 0f, // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.FastSpatial.value(), ) // The entire item is selectable, but only the indicator pill shows the ripple. To achieve // this, we re-map the coordinates of the item's InteractionSource into the coordinates of // the indicator. val density = LocalDensity.current val calculateDeltaOffset = { with(density) { val indicatorWidth = NavigationBarVerticalItemTokens.ActiveIndicatorWidth.roundToPx() Offset((itemWidth - indicatorWidth).toFloat() / 2, IndicatorVerticalOffset.toPx()) } } val offsetInteractionSource = remember(interactionSource, calculateDeltaOffset) { MappedInteractionSource(interactionSource, calculateDeltaOffset) } // The indicator has a width-expansion animation which interferes with the timing of the // ripple, which is why they are separate composables val indicatorRipple = @Composable { Box( Modifier.layoutId(IndicatorRippleLayoutIdTag) .clip(NavigationBarTokens.ItemActiveIndicatorShape.value) .indication(offsetInteractionSource, ripple()) ) } val indicator = @Composable { Box( Modifier.layoutId(IndicatorLayoutIdTag) .graphicsLayer { alpha = alphaAnimationProgress.value } .background( color = colors.indicatorColor, shape = NavigationBarTokens.ItemActiveIndicatorShape.value, ) ) } NavigationBarItemLayout( indicatorRipple = indicatorRipple, indicator = indicator, icon = styledIcon, label = styledLabel, alwaysShowLabel = alwaysShowLabel, alphaAnimationProgress = { alphaAnimationProgress.value }, sizeAnimationProgress = { sizeAnimationProgress.value }, ) } } /** Defaults used in [NavigationBar]. */ object NavigationBarDefaults { /** Default elevation for a navigation bar. */ val Elevation: Dp = ElevationTokens.Level0 /** Default color for a navigation bar. */ val containerColor: Color @Composable get() = NavigationBarTokens.ContainerColor.value /** Default window insets to be used and consumed by navigation bar */ val windowInsets: WindowInsets @Composable get() = WindowInsets.systemBarsForVisualComponents.only( WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom ) } /** Defaults used in [NavigationBarItem]. */ object NavigationBarItemDefaults { /** * Creates a [NavigationBarItemColors] with the provided colors according to the Material * specification. */ @Composable fun colors() = MaterialTheme.colorScheme.defaultNavigationBarItemColors /** * Creates a [NavigationBarItemColors] with the provided colors according to the Material * specification. * * @param selectedIconColor the color to use for the icon when the item is selected. * @param selectedTextColor the color to use for the text label when the item is selected. * @param indicatorColor the color to use for the indicator when the item is selected. * @param unselectedIconColor the color to use for the icon when the item is unselected. * @param unselectedTextColor the color to use for the text label when the item is unselected. * @param disabledIconColor the color to use for the icon when the item is disabled. * @param disabledTextColor the color to use for the text label when the item is disabled. * @return the resulting [NavigationBarItemColors] used for [NavigationBarItem] */ @Composable fun colors( selectedIconColor: Color = Color.Unspecified, selectedTextColor: Color = Color.Unspecified, indicatorColor: Color = Color.Unspecified, unselectedIconColor: Color = Color.Unspecified, unselectedTextColor: Color = Color.Unspecified, disabledIconColor: Color = Color.Unspecified, disabledTextColor: Color = Color.Unspecified, ): NavigationBarItemColors = MaterialTheme.colorScheme.defaultNavigationBarItemColors.copy( selectedIconColor = selectedIconColor, selectedTextColor = selectedTextColor, selectedIndicatorColor = indicatorColor, unselectedIconColor = unselectedIconColor, unselectedTextColor = unselectedTextColor, disabledIconColor = disabledIconColor, disabledTextColor = disabledTextColor, ) internal val ColorScheme.defaultNavigationBarItemColors: NavigationBarItemColors get() { return defaultNavigationBarItemColorsCached ?: NavigationBarItemColors( selectedIconColor = fromToken(NavigationBarTokens.ItemActiveIconColor), selectedTextColor = fromToken(NavigationBarTokens.ItemActiveLabelTextColor), selectedIndicatorColor = fromToken(NavigationBarTokens.ItemActiveIndicatorColor), unselectedIconColor = fromToken(NavigationBarTokens.ItemInactiveIconColor), unselectedTextColor = fromToken(NavigationBarTokens.ItemInactiveLabelTextColor), disabledIconColor = fromToken(NavigationBarTokens.ItemInactiveIconColor) .copy(alpha = DisabledAlpha), disabledTextColor = fromToken(NavigationBarTokens.ItemInactiveLabelTextColor) .copy(alpha = DisabledAlpha), ) .also { defaultNavigationBarItemColorsCached = it } } @Deprecated( "Use overload with disabledIconColor and disabledTextColor", level = DeprecationLevel.HIDDEN, ) @Composable fun colors( selectedIconColor: Color = NavigationBarTokens.ItemActiveIconColor.value, selectedTextColor: Color = NavigationBarTokens.ItemActiveLabelTextColor.value, indicatorColor: Color = NavigationBarTokens.ItemActiveIndicatorColor.value, unselectedIconColor: Color = NavigationBarTokens.ItemInactiveIconColor.value, unselectedTextColor: Color = NavigationBarTokens.ItemInactiveLabelTextColor.value, ): NavigationBarItemColors = NavigationBarItemColors( selectedIconColor = selectedIconColor, selectedTextColor = selectedTextColor, selectedIndicatorColor = indicatorColor, unselectedIconColor = unselectedIconColor, unselectedTextColor = unselectedTextColor, disabledIconColor = unselectedIconColor.copy(alpha = DisabledAlpha), disabledTextColor = unselectedTextColor.copy(alpha = DisabledAlpha), ) } /** * Represents the colors of the various elements of a navigation item. * * @param selectedIconColor the color to use for the icon when the item is selected. * @param selectedTextColor the color to use for the text label when the item is selected. * @param selectedIndicatorColor the color to use for the indicator when the item is selected. * @param unselectedIconColor the color to use for the icon when the item is unselected. * @param unselectedTextColor the color to use for the text label when the item is unselected. * @param disabledIconColor the color to use for the icon when the item is disabled. * @param disabledTextColor the color to use for the text label when the item is disabled. * @constructor create an instance with arbitrary colors. */ @Immutable class NavigationBarItemColors constructor( val selectedIconColor: Color, val selectedTextColor: Color, val selectedIndicatorColor: Color, val unselectedIconColor: Color, val unselectedTextColor: Color, val disabledIconColor: Color, val disabledTextColor: Color, ) { /** * Returns a copy of this NavigationBarItemColors, optionally overriding some of the values. * This uses the Color.Unspecified to mean “use the value from the source” */ fun copy( selectedIconColor: Color = this.selectedIconColor, selectedTextColor: Color = this.selectedTextColor, selectedIndicatorColor: Color = this.selectedIndicatorColor, unselectedIconColor: Color = this.unselectedIconColor, unselectedTextColor: Color = this.unselectedTextColor, disabledIconColor: Color = this.disabledIconColor, disabledTextColor: Color = this.disabledTextColor, ) = NavigationBarItemColors( selectedIconColor.takeOrElse { this.selectedIconColor }, selectedTextColor.takeOrElse { this.selectedTextColor }, selectedIndicatorColor.takeOrElse { this.selectedIndicatorColor }, unselectedIconColor.takeOrElse { this.unselectedIconColor }, unselectedTextColor.takeOrElse { this.unselectedTextColor }, disabledIconColor.takeOrElse { this.disabledIconColor }, disabledTextColor.takeOrElse { this.disabledTextColor }, ) /** * Represents the icon color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected * @param enabled whether the item is enabled */ @Stable internal fun iconColor(selected: Boolean, enabled: Boolean): Color = when { !enabled -> disabledIconColor selected -> selectedIconColor else -> unselectedIconColor } /** * Represents the text color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected * @param enabled whether the item is enabled */ @Stable internal fun textColor(selected: Boolean, enabled: Boolean): Color = when { !enabled -> disabledTextColor selected -> selectedTextColor else -> unselectedTextColor } /** Represents the color of the indicator used for selected items. */ internal val indicatorColor: Color get() = selectedIndicatorColor override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is NavigationBarItemColors) return false if (selectedIconColor != other.selectedIconColor) return false if (unselectedIconColor != other.unselectedIconColor) return false if (selectedTextColor != other.selectedTextColor) return false if (unselectedTextColor != other.unselectedTextColor) return false if (selectedIndicatorColor != other.selectedIndicatorColor) return false if (disabledIconColor != other.disabledIconColor) return false if (disabledTextColor != other.disabledTextColor) return false return true } override fun hashCode(): Int { var result = selectedIconColor.hashCode() result = 31 * result + unselectedIconColor.hashCode() result = 31 * result + selectedTextColor.hashCode() result = 31 * result + unselectedTextColor.hashCode() result = 31 * result + selectedIndicatorColor.hashCode() result = 31 * result + disabledIconColor.hashCode() result = 31 * result + disabledTextColor.hashCode() return result } } /** * Base layout for a [NavigationBarItem]. * * @param indicatorRipple indicator ripple for this item when it is selected * @param indicator indicator for this item when it is selected * @param icon icon for this item * @param label text label for this item * @param alwaysShowLabel whether to always show the label for this item. If false, the label will * only be shown when this item is selected. * @param alphaAnimationProgress progress of the animation, where 0 represents the unselected state * of this item and 1 represents the selected state. This value controls the indicator's opacity. * @param sizeAnimationProgress progress of the animation, where 0 represents the unselected state * of this item and 1 represents the selected state. This value controls other values such as * indicator size, icon and label positions, etc. */ @Composable private fun NavigationBarItemLayout( indicatorRipple: @Composable () -> Unit, indicator: @Composable () -> Unit, icon: @Composable () -> Unit, label: @Composable (() -> Unit)?, alwaysShowLabel: Boolean, alphaAnimationProgress: () -> Float, sizeAnimationProgress: () -> Float, ) { Layout( modifier = Modifier.badgeBounds(), content = { indicatorRipple() indicator() Box(Modifier.layoutId(IconLayoutIdTag)) { icon() } if (label != null) { Box( Modifier.layoutId(LabelLayoutIdTag).graphicsLayer { alpha = if (alwaysShowLabel) 1f else alphaAnimationProgress() } ) { label() } } }, ) { measurables, constraints -> @Suppress("NAME_SHADOWING") // Ensure that the progress is >= 0. It may be negative on bouncy springs, for example. val animationProgress = sizeAnimationProgress().coerceAtLeast(0f) val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) val iconPlaceable = measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints) val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx() val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt() val indicatorHeight = iconPlaceable.height + (IndicatorVerticalPadding * 2).roundToPx() val indicatorRipplePlaceable = measurables .fastFirst { it.layoutId == IndicatorRippleLayoutIdTag } .measure(Constraints.fixed(width = totalIndicatorWidth, height = indicatorHeight)) val indicatorPlaceable = measurables .fastFirstOrNull { it.layoutId == IndicatorLayoutIdTag } ?.measure( Constraints.fixed(width = animatedIndicatorWidth, height = indicatorHeight) ) val labelPlaceable = label?.let { measurables.fastFirst { it.layoutId == LabelLayoutIdTag }.measure(looseConstraints) } if (label == null) { placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints) } else { placeLabelAndIcon( labelPlaceable!!, iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints, alwaysShowLabel, animationProgress, ) } } } /** Places the provided [Placeable]s in the center of the provided [constraints]. */ private fun MeasureScope.placeIcon( iconPlaceable: Placeable, indicatorRipplePlaceable: Placeable, indicatorPlaceable: Placeable?, constraints: Constraints, ): MeasureResult { val width = if (constraints.maxWidth == Constraints.Infinity) { iconPlaceable.width + NavigationBarItemToIconMinimumPadding.roundToPx() * 2 } else { constraints.maxWidth } val height = constraints.constrainHeight(NavigationBarHeight.roundToPx()) val iconX = (width - iconPlaceable.width) / 2 val iconY = (height - iconPlaceable.height) / 2 val rippleX = (width - indicatorRipplePlaceable.width) / 2 val rippleY = (height - indicatorRipplePlaceable.height) / 2 return layout(width, height) { indicatorPlaceable?.let { val indicatorX = (width - it.width) / 2 val indicatorY = (height - it.height) / 2 it.placeRelative(indicatorX, indicatorY) } iconPlaceable.placeRelative(iconX, iconY) indicatorRipplePlaceable.placeRelative(rippleX, rippleY) } } /** * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and * [animationProgress]. * * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] and * [labelPlaceable] will be placed together in the center with padding between them, according to * the spec. * * When [animationProgress] is 1 (representing the selected state), the positions will be the same * as above. * * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in * [placeIcon], and [labelPlaceable] will not be shown. * * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable] * will be placed at a corresponding interpolated position. * * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to * share the same center as [iconPlaceable]. * * @param labelPlaceable text label placeable inside this item * @param iconPlaceable icon placeable inside this item * @param indicatorRipplePlaceable indicator ripple placeable inside this item * @param indicatorPlaceable indicator placeable inside this item, if it exists * @param constraints constraints of the item * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label * positions will not change. If false, positions transition between 'centered icon with no label' * and 'top aligned icon with label'. * @param animationProgress progress of the animation, where 0 represents the unselected state of * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of * the icon and label. */ private fun MeasureScope.placeLabelAndIcon( labelPlaceable: Placeable, iconPlaceable: Placeable, indicatorRipplePlaceable: Placeable, indicatorPlaceable: Placeable?, constraints: Constraints, alwaysShowLabel: Boolean, animationProgress: Float, ): MeasureResult { val contentHeight = iconPlaceable.height + IndicatorVerticalPadding.toPx() + NavigationBarIndicatorToLabelPadding.toPx() + labelPlaceable.height val contentVerticalPadding = ((constraints.minHeight - contentHeight) / 2).coerceAtLeast(IndicatorVerticalPadding.toPx()) val height = contentHeight + contentVerticalPadding * 2 // Icon (when selected) should be `contentVerticalPadding` from top val selectedIconY = contentVerticalPadding val unselectedIconY = if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2 // How far the icon needs to move between unselected and selected states. val iconDistance = unselectedIconY - selectedIconY // The interpolated fraction of iconDistance that all placeables need to move based on // animationProgress. val offset = iconDistance * (1 - animationProgress) // Label should be fixed padding below icon val labelY = selectedIconY + iconPlaceable.height + IndicatorVerticalPadding.toPx() + NavigationBarIndicatorToLabelPadding.toPx() val containerWidth = if (constraints.maxWidth == Constraints.Infinity) { iconPlaceable.width + NavigationBarItemToIconMinimumPadding.roundToPx() * 2 } else { constraints.maxWidth } val labelX = (containerWidth - labelPlaceable.width) / 2 val iconX = (containerWidth - iconPlaceable.width) / 2 val rippleX = (containerWidth - indicatorRipplePlaceable.width) / 2 val rippleY = selectedIconY - IndicatorVerticalPadding.toPx() return layout(containerWidth, height.roundToInt()) { indicatorPlaceable?.let { val indicatorX = (containerWidth - it.width) / 2 val indicatorY = selectedIconY - IndicatorVerticalPadding.roundToPx() it.placeRelative(indicatorX, (indicatorY + offset).roundToInt()) } if (alwaysShowLabel || animationProgress != 0f) { labelPlaceable.placeRelative(labelX, (labelY + offset).roundToInt()) } iconPlaceable.placeRelative(iconX, (selectedIconY + offset).roundToInt()) indicatorRipplePlaceable.placeRelative(rippleX, (rippleY + offset).roundToInt()) } } private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple" private const val IndicatorLayoutIdTag: String = "indicator" private const val IconLayoutIdTag: String = "icon" private const val LabelLayoutIdTag: String = "label" private val NavigationBarHeight: Dp = NavigationBarTokens.TallContainerHeight /*@VisibleForTesting*/ internal val NavigationBarItemHorizontalPadding: Dp = 8.dp /*@VisibleForTesting*/ internal val NavigationBarIndicatorToLabelPadding: Dp = 4.dp private val IndicatorHorizontalPadding: Dp = (NavigationBarVerticalItemTokens.ActiveIndicatorWidth - NavigationBarVerticalItemTokens.IconSize) / 2 /*@VisibleForTesting*/ internal val IndicatorVerticalPadding: Dp = (NavigationBarVerticalItemTokens.ActiveIndicatorHeight - NavigationBarVerticalItemTokens.IconSize) / 2 private val IndicatorVerticalOffset: Dp = 12.dp /*@VisibleForTesting*/ internal val NavigationBarItemToIconMinimumPadding: Dp = 44.dp /** * Interface that allows libraries to override the behavior of the [NavigationBar] component. * * To override this component, implement the member function of this interface, then provide the * implementation to [LocalNavigationBarOverride] in the Compose hierarchy. */ @ExperimentalMaterial3ComponentOverrideApi interface NavigationBarOverride { /** Behavior function that is called by the [NavigationBar] component. */ @Composable fun NavigationBarOverrideScope.NavigationBar() } /** * Parameters available to [NavigationBar]. * * @param modifier the [Modifier] to be applied to this navigation bar * @param containerColor the color used for the background of this navigation bar. Use * [Color.Transparent] to have no color. * @param contentColor the preferred color for content inside this navigation bar. Defaults to * either the matching content color for [containerColor], or to the current [LocalContentColor] * if [containerColor] is not a color from the theme. * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color * overlay is applied on top of the container. A higher tonal elevation value will result in a * darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets of the navigation bar. * @param content the content of this navigation bar, typically 3-5 [NavigationBarItem]s */ @ExperimentalMaterial3ComponentOverrideApi class NavigationBarOverrideScope internal constructor( val modifier: Modifier = Modifier, val containerColor: Color, val contentColor: Color, val tonalElevation: Dp, val windowInsets: WindowInsets, val content: @Composable RowScope.() -> Unit, ) /** CompositionLocal containing the currently-selected [NavigationBarOverride]. */ @ExperimentalMaterial3ComponentOverrideApi val LocalNavigationBarOverride: ProvidableCompositionLocal = compositionLocalOf { DefaultNavigationBarOverride } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationRail.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.DefaultNavigationRailOverride.NavigationRail import androidx.compose.material3.internal.MappedInteractionSource import androidx.compose.material3.internal.ProvideContentColorTextStyle import androidx.compose.material3.internal.systemBarsForVisualComponents import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.NavigationRailBaselineItemTokens import androidx.compose.material3.tokens.NavigationRailCollapsedTokens import androidx.compose.material3.tokens.NavigationRailColorTokens import androidx.compose.material3.tokens.NavigationRailVerticalItemTokens import androidx.compose.material3.tokens.ShapeKeyTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import kotlin.math.roundToInt /** * [Material Design bottom navigation * rail](https://m3.material.io/components/navigation-rail/overview) * * Navigation rails provide access to primary destinations in apps when using tablet and desktop * screens. * * ![Navigation rail * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-rail.png) * * The navigation rail should be used to display three to seven app destinations and, optionally, a * [FloatingActionButton] or a logo header. Each destination is typically represented by an icon and * an optional text label. * * [NavigationRail] should contain multiple [NavigationRailItem]s, each representing a singular * destination. * * A simple example looks like: * * @sample androidx.compose.material3.samples.NavigationRailSample * * See [NavigationRailItem] for configuration specific to each item, and not the overall * NavigationRail component. * * @param modifier the [Modifier] to be applied to this navigation rail * @param containerColor the color used for the background of this navigation rail. Use * [Color.Transparent] to have no color. * @param contentColor the preferred color for content inside this navigation rail. Defaults to * either the matching content color for [containerColor], or to the current [LocalContentColor] * if [containerColor] is not a color from the theme. * @param header optional header that may hold a [FloatingActionButton] or a logo * @param windowInsets a window insets of the navigation rail. * @param content the content of this navigation rail, typically 3-7 [NavigationRailItem]s */ @OptIn(ExperimentalMaterial3ComponentOverrideApi::class) @Composable fun NavigationRail( modifier: Modifier = Modifier, containerColor: Color = NavigationRailDefaults.ContainerColor, contentColor: Color = contentColorFor(containerColor), header: @Composable (ColumnScope.() -> Unit)? = null, windowInsets: WindowInsets = NavigationRailDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { with(LocalNavigationRailOverride.current) { NavigationRailOverrideScope( modifier = modifier, containerColor = containerColor, contentColor = contentColor, header = header, windowInsets = windowInsets, content = content, ) .NavigationRail() } } /** * This override provides the default behavior of the [NavigationRail] component. * * [NavigationRailOverride] used when no override is specified. */ @ExperimentalMaterial3ComponentOverrideApi object DefaultNavigationRailOverride : NavigationRailOverride { @Composable override fun NavigationRailOverrideScope.NavigationRail() { Surface(color = containerColor, contentColor = contentColor, modifier = modifier) { Column( Modifier.fillMaxHeight() .windowInsetsPadding(windowInsets) .widthIn(min = NavigationRailCollapsedTokens.NarrowContainerWidth) .padding(vertical = NavigationRailVerticalPadding) .selectableGroup() .semantics { isTraversalGroup = true }, horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(NavigationRailVerticalPadding), ) { val header = header if (header != null) { header() Spacer(Modifier.height(NavigationRailHeaderPadding)) } content() } } } } /** * Material Design navigation rail item. * * A [NavigationRailItem] represents a destination within a [NavigationRail]. * * Navigation rails provide access to primary destinations in apps when using tablet and desktop * screens. * * The text label is always shown (if it exists) when selected. Showing text labels if not selected * is controlled by [alwaysShowLabel]. * * @param selected whether this item is selected * @param onClick called when this item is clicked * @param icon icon for this item, typically an [Icon] * @param modifier the [Modifier] to be applied to this item * @param enabled controls the enabled state of this item. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param label optional text label for this item * @param alwaysShowLabel whether to always show the label for this item. If false, the label will * only be shown when this item is selected. * @param colors [NavigationRailItemColors] that will be used to resolve the colors used for this * item in different states. See [NavigationRailItemDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this item. You can use this to change the item's appearance or * preview the item in different states. Note that if `null` is provided, interactions will still * happen internally. */ @Composable fun NavigationRailItem( selected: Boolean, onClick: () -> Unit, icon: @Composable () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, label: @Composable (() -> Unit)? = null, alwaysShowLabel: Boolean = true, colors: NavigationRailItemColors = NavigationRailItemDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // TODO Load the motionScheme tokens from the component tokens file val colorAnimationSpec = MotionSchemeKeyTokens.DefaultEffects.value() val styledIcon = @Composable { val iconColor by animateColorAsState( targetValue = colors.iconColor(selected = selected, enabled = enabled), animationSpec = colorAnimationSpec, ) // If there's a label, don't have a11y services repeat the icon description. val clearSemantics = label != null && (alwaysShowLabel || selected) Box(modifier = if (clearSemantics) Modifier.clearAndSetSemantics {} else Modifier) { CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) } } val styledLabel: @Composable (() -> Unit)? = label?.let { @Composable { val style = NavigationRailVerticalItemTokens.LabelTextFont.value val textColor by animateColorAsState( targetValue = colors.textColor(selected = selected, enabled = enabled), animationSpec = colorAnimationSpec, ) ProvideContentColorTextStyle( contentColor = textColor, textStyle = style, content = label, ) } } Box( modifier .selectable( selected = selected, onClick = onClick, enabled = enabled, role = Role.Tab, interactionSource = interactionSource, indication = null, ) .defaultMinSize(minHeight = NavigationRailItemHeight) .widthIn(min = NavigationRailItemWidth), contentAlignment = Alignment.Center, propagateMinConstraints = true, ) { val alphaAnimationProgress: State = animateFloatAsState( targetValue = if (selected) 1f else 0f, // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.DefaultEffects.value(), ) val sizeAnimationProgress: State = animateFloatAsState( targetValue = if (selected) 1f else 0f, // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.FastSpatial.value(), ) // The entire item is selectable, but only the indicator pill shows the ripple. To achieve // this, we re-map the coordinates of the item's InteractionSource into the coordinates of // the indicator. val density = LocalDensity.current val calculateDeltaOffset = { with(density) { val itemWidth = NavigationRailItemWidth.roundToPx() val indicatorWidth = NavigationRailVerticalItemTokens.ActiveIndicatorWidth.roundToPx() Offset((itemWidth - indicatorWidth).toFloat() / 2, 0f) } } val offsetInteractionSource = remember(interactionSource, calculateDeltaOffset) { MappedInteractionSource(interactionSource, calculateDeltaOffset) } val indicatorShape = if (label != null) { NavigationRailBaselineItemTokens.ActiveIndicatorShape.value } else { ShapeKeyTokens.CornerFull.value } // The indicator has a width-expansion animation which interferes with the timing of the // ripple, which is why they are separate composables val indicatorRipple = @Composable { Box( Modifier.layoutId(IndicatorRippleLayoutIdTag) .clip(indicatorShape) .indication(offsetInteractionSource, ripple()) ) } val indicator = @Composable { Box( Modifier.layoutId(IndicatorLayoutIdTag) .graphicsLayer { alpha = alphaAnimationProgress.value } .background(color = colors.indicatorColor, shape = indicatorShape) ) } NavigationRailItemLayout( indicatorRipple = indicatorRipple, indicator = indicator, icon = styledIcon, label = styledLabel, alwaysShowLabel = alwaysShowLabel, alphaAnimationProgress = { alphaAnimationProgress.value }, sizeAnimationProgress = { sizeAnimationProgress.value }, ) } } /** Defaults used in [NavigationRail] */ object NavigationRailDefaults { /** Default container color of a navigation rail. */ val ContainerColor: Color @Composable get() = NavigationRailCollapsedTokens.ContainerColor.value /** Default window insets for navigation rail. */ val windowInsets: WindowInsets @Composable get() = WindowInsets.systemBarsForVisualComponents.only( WindowInsetsSides.Vertical + WindowInsetsSides.Start ) } /** Defaults used in [NavigationRailItem]. */ object NavigationRailItemDefaults { /** * Creates a [NavigationRailItemColors] with the provided colors according to the Material * specification. */ @Composable fun colors() = MaterialTheme.colorScheme.defaultNavigationRailItemColors /** * Creates a [NavigationRailItemColors] with the provided colors according to the Material * specification. * * @param selectedIconColor the color to use for the icon when the item is selected. * @param selectedTextColor the color to use for the text label when the item is selected. * @param indicatorColor the color to use for the indicator when the item is selected. * @param unselectedIconColor the color to use for the icon when the item is unselected. * @param unselectedTextColor the color to use for the text label when the item is unselected. * @param disabledIconColor the color to use for the icon when the item is disabled. * @param disabledTextColor the color to use for the text label when the item is disabled. * @return the resulting [NavigationRailItemColors] used for [NavigationRailItem] */ @Composable fun colors( selectedIconColor: Color = NavigationRailColorTokens.ItemActiveIcon.value, selectedTextColor: Color = NavigationRailColorTokens.ItemActiveLabelText.value, indicatorColor: Color = NavigationRailColorTokens.ItemActiveIndicator.value, unselectedIconColor: Color = NavigationRailColorTokens.ItemInactiveIcon.value, unselectedTextColor: Color = NavigationRailColorTokens.ItemInactiveLabelText.value, disabledIconColor: Color = unselectedIconColor.copy(alpha = DisabledAlpha), disabledTextColor: Color = unselectedTextColor.copy(alpha = DisabledAlpha), ): NavigationRailItemColors = MaterialTheme.colorScheme.defaultNavigationRailItemColors.copy( selectedIconColor = selectedIconColor, selectedTextColor = selectedTextColor, selectedIndicatorColor = indicatorColor, unselectedIconColor = unselectedIconColor, unselectedTextColor = unselectedTextColor, disabledIconColor = disabledIconColor, disabledTextColor = disabledTextColor, ) internal val ColorScheme.defaultNavigationRailItemColors: NavigationRailItemColors get() { return defaultNavigationRailItemColorsCached ?: NavigationRailItemColors( selectedIconColor = fromToken(NavigationRailColorTokens.ItemActiveIcon), selectedTextColor = fromToken(NavigationRailColorTokens.ItemActiveLabelText), selectedIndicatorColor = fromToken(NavigationRailColorTokens.ItemActiveIndicator), unselectedIconColor = fromToken(NavigationRailColorTokens.ItemInactiveIcon), unselectedTextColor = fromToken(NavigationRailColorTokens.ItemInactiveLabelText), disabledIconColor = fromToken(NavigationRailColorTokens.ItemInactiveIcon) .copy(alpha = DisabledAlpha), disabledTextColor = fromToken(NavigationRailColorTokens.ItemInactiveLabelText) .copy(alpha = DisabledAlpha), ) .also { defaultNavigationRailItemColorsCached = it } } @Deprecated( "Use overload with disabledIconColor and disabledTextColor", level = DeprecationLevel.HIDDEN, ) @Composable fun colors( selectedIconColor: Color = NavigationRailColorTokens.ItemActiveIcon.value, selectedTextColor: Color = NavigationRailColorTokens.ItemActiveLabelText.value, indicatorColor: Color = NavigationRailColorTokens.ItemActiveIndicator.value, unselectedIconColor: Color = NavigationRailColorTokens.ItemInactiveIcon.value, unselectedTextColor: Color = NavigationRailColorTokens.ItemInactiveLabelText.value, ): NavigationRailItemColors = NavigationRailItemColors( selectedIconColor = selectedIconColor, selectedTextColor = selectedTextColor, selectedIndicatorColor = indicatorColor, unselectedIconColor = unselectedIconColor, unselectedTextColor = unselectedTextColor, disabledIconColor = unselectedIconColor.copy(alpha = DisabledAlpha), disabledTextColor = unselectedTextColor.copy(alpha = DisabledAlpha), ) } /** * Represents the colors of the various elements of a navigation item. * * @param selectedIconColor the color to use for the icon when the item is selected. * @param selectedTextColor the color to use for the text label when the item is selected. * @param selectedIndicatorColor the color to use for the indicator when the item is selected. * @param unselectedIconColor the color to use for the icon when the item is unselected. * @param unselectedTextColor the color to use for the text label when the item is unselected. * @param disabledIconColor the color to use for the icon when the item is disabled. * @param disabledTextColor the color to use for the text label when the item is disabled. * @constructor create an instance with arbitrary colors. */ @Immutable class NavigationRailItemColors constructor( val selectedIconColor: Color, val selectedTextColor: Color, val selectedIndicatorColor: Color, val unselectedIconColor: Color, val unselectedTextColor: Color, val disabledIconColor: Color, val disabledTextColor: Color, ) { /** * Returns a copy of this NavigationRailItemColors, optionally overriding some of the values. * This uses the Color.Unspecified to mean “use the value from the source” */ fun copy( selectedIconColor: Color = this.selectedIconColor, selectedTextColor: Color = this.selectedTextColor, selectedIndicatorColor: Color = this.selectedIndicatorColor, unselectedIconColor: Color = this.unselectedIconColor, unselectedTextColor: Color = this.unselectedTextColor, disabledIconColor: Color = this.disabledIconColor, disabledTextColor: Color = this.disabledTextColor, ) = NavigationRailItemColors( selectedIconColor.takeOrElse { this.selectedIconColor }, selectedTextColor.takeOrElse { this.selectedTextColor }, selectedIndicatorColor.takeOrElse { this.selectedIndicatorColor }, unselectedIconColor.takeOrElse { this.unselectedIconColor }, unselectedTextColor.takeOrElse { this.unselectedTextColor }, disabledIconColor.takeOrElse { this.disabledIconColor }, disabledTextColor.takeOrElse { this.disabledTextColor }, ) /** * Represents the icon color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected * @param enabled whether the item is enabled */ @Stable internal fun iconColor(selected: Boolean, enabled: Boolean): Color = when { !enabled -> disabledIconColor selected -> selectedIconColor else -> unselectedIconColor } /** * Represents the text color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected * @param enabled whether the item is enabled */ @Stable internal fun textColor(selected: Boolean, enabled: Boolean): Color = when { !enabled -> disabledTextColor selected -> selectedTextColor else -> unselectedTextColor } /** Represents the color of the indicator used for selected items. */ internal val indicatorColor: Color get() = selectedIndicatorColor override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is NavigationRailItemColors) return false if (selectedIconColor != other.selectedIconColor) return false if (unselectedIconColor != other.unselectedIconColor) return false if (selectedTextColor != other.selectedTextColor) return false if (unselectedTextColor != other.unselectedTextColor) return false if (selectedIndicatorColor != other.selectedIndicatorColor) return false if (disabledIconColor != other.disabledIconColor) return false if (disabledTextColor != other.disabledTextColor) return false return true } override fun hashCode(): Int { var result = selectedIconColor.hashCode() result = 31 * result + unselectedIconColor.hashCode() result = 31 * result + selectedTextColor.hashCode() result = 31 * result + unselectedTextColor.hashCode() result = 31 * result + selectedIndicatorColor.hashCode() result = 31 * result + disabledIconColor.hashCode() result = 31 * result + disabledTextColor.hashCode() return result } } /** * Base layout for a [NavigationRailItem]. * * @param indicatorRipple indicator ripple for this item when it is selected * @param indicator indicator for this item when it is selected * @param icon icon for this item * @param label text label for this item * @param alwaysShowLabel whether to always show the label for this item. If false, the label will * only be shown when this item is selected. * @param alphaAnimationProgress progress of the animation, where 0 represents the unselected state * of this item and 1 represents the selected state. This value controls the indicator's color * alpha. * @param sizeAnimationProgress progress of the animation, where 0 represents the unselected state * of this item and 1 represents the selected state. This value controls other values such as * indicator size, icon and label positions, etc. */ @Composable private fun NavigationRailItemLayout( indicatorRipple: @Composable () -> Unit, indicator: @Composable () -> Unit, icon: @Composable () -> Unit, label: @Composable (() -> Unit)?, alwaysShowLabel: Boolean, alphaAnimationProgress: () -> Float, sizeAnimationProgress: () -> Float, ) { Layout( modifier = Modifier.badgeBounds(), content = { indicatorRipple() indicator() Box(Modifier.layoutId(IconLayoutIdTag)) { icon() } if (label != null) { Box( Modifier.layoutId(LabelLayoutIdTag).graphicsLayer { alpha = if (alwaysShowLabel) 1f else alphaAnimationProgress() } ) { label() } } }, ) { measurables, constraints -> @Suppress("NAME_SHADOWING") // Ensure that the progress is >= 0. It may be negative on bouncy springs, for example. val animationProgress = sizeAnimationProgress().coerceAtLeast(0f) val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) val iconPlaceable = measurables.fastFirst { it.layoutId == IconLayoutIdTag }.measure(looseConstraints) val totalIndicatorWidth = iconPlaceable.width + (IndicatorHorizontalPadding * 2).roundToPx() val animatedIndicatorWidth = (totalIndicatorWidth * animationProgress).roundToInt() val indicatorVerticalPadding = if (label == null) { IndicatorVerticalPaddingNoLabel } else { IndicatorVerticalPaddingWithLabel } val indicatorHeight = iconPlaceable.height + (indicatorVerticalPadding * 2).roundToPx() val indicatorRipplePlaceable = measurables .fastFirst { it.layoutId == IndicatorRippleLayoutIdTag } .measure(Constraints.fixed(width = totalIndicatorWidth, height = indicatorHeight)) val indicatorPlaceable = measurables .fastFirstOrNull { it.layoutId == IndicatorLayoutIdTag } ?.measure( Constraints.fixed(width = animatedIndicatorWidth, height = indicatorHeight) ) val labelPlaceable = label?.let { measurables.fastFirst { it.layoutId == LabelLayoutIdTag }.measure(looseConstraints) } if (label == null) { placeIcon(iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints) } else { placeLabelAndIcon( labelPlaceable!!, iconPlaceable, indicatorRipplePlaceable, indicatorPlaceable, constraints, alwaysShowLabel, animationProgress, ) } } } /** Places the provided [Placeable]s in the center of the provided [constraints]. */ private fun MeasureScope.placeIcon( iconPlaceable: Placeable, indicatorRipplePlaceable: Placeable, indicatorPlaceable: Placeable?, constraints: Constraints, ): MeasureResult { val width = constraints.constrainWidth( maxOf( iconPlaceable.width, indicatorRipplePlaceable.width, indicatorPlaceable?.width ?: 0, ) ) val height = constraints.constrainHeight(NavigationRailItemHeight.roundToPx()) val iconX = (width - iconPlaceable.width) / 2 val iconY = (height - iconPlaceable.height) / 2 val rippleX = (width - indicatorRipplePlaceable.width) / 2 val rippleY = (height - indicatorRipplePlaceable.height) / 2 return layout(width, height) { indicatorPlaceable?.let { val indicatorX = (width - it.width) / 2 val indicatorY = (height - it.height) / 2 it.placeRelative(indicatorX, indicatorY) } iconPlaceable.placeRelative(iconX, iconY) indicatorRipplePlaceable.placeRelative(rippleX, rippleY) } } /** * Places the provided [Placeable]s in the correct position, depending on [alwaysShowLabel] and * [animationProgress]. * * When [alwaysShowLabel] is true, the positions do not move. The [iconPlaceable] and * [labelPlaceable] will be placed together in the center with padding between them, according to * the spec. * * When [animationProgress] is 1 (representing the selected state), the positions will be the same * as above. * * Otherwise, when [animationProgress] is 0, [iconPlaceable] will be placed in the center, like in * [placeIcon], and [labelPlaceable] will not be shown. * * When [animationProgress] is animating between these values, [iconPlaceable] and [labelPlaceable] * will be placed at a corresponding interpolated position. * * [indicatorRipplePlaceable] and [indicatorPlaceable] will always be placed in such a way that to * share the same center as [iconPlaceable]. * * @param labelPlaceable text label placeable inside this item * @param iconPlaceable icon placeable inside this item * @param indicatorRipplePlaceable indicator ripple placeable inside this item * @param indicatorPlaceable indicator placeable inside this item, if it exists * @param constraints constraints of the item * @param alwaysShowLabel whether to always show the label for this item. If true, icon and label * positions will not change. If false, positions transition between 'centered icon with no label' * and 'top aligned icon with label'. * @param animationProgress progress of the animation, where 0 represents the unselected state of * this item and 1 represents the selected state. Values between 0 and 1 interpolate positions of * the icon and label. */ private fun MeasureScope.placeLabelAndIcon( labelPlaceable: Placeable, iconPlaceable: Placeable, indicatorRipplePlaceable: Placeable, indicatorPlaceable: Placeable?, constraints: Constraints, alwaysShowLabel: Boolean, animationProgress: Float, ): MeasureResult { val contentHeight = iconPlaceable.height + IndicatorVerticalPaddingWithLabel.toPx() + NavigationRailItemVerticalPadding.toPx() + labelPlaceable.height val contentVerticalPadding = ((constraints.minHeight - contentHeight) / 2).coerceAtLeast( IndicatorVerticalPaddingWithLabel.toPx() ) val height = contentHeight + contentVerticalPadding * 2 // Icon (when selected) should be `contentVerticalPadding` from the top val selectedIconY = contentVerticalPadding val unselectedIconY = if (alwaysShowLabel) selectedIconY else (height - iconPlaceable.height) / 2 // How far the icon needs to move between unselected and selected states val iconDistance = unselectedIconY - selectedIconY // The interpolated fraction of iconDistance that all placeables need to move based on // animationProgress, since the icon is higher in the selected state. val offset = iconDistance * (1 - animationProgress) // Label should be fixed padding below icon val labelY = selectedIconY + iconPlaceable.height + IndicatorVerticalPaddingWithLabel.toPx() + NavigationRailItemVerticalPadding.toPx() val width = constraints.constrainWidth( maxOf(iconPlaceable.width, labelPlaceable.width, indicatorPlaceable?.width ?: 0) ) val labelX = (width - labelPlaceable.width) / 2 val iconX = (width - iconPlaceable.width) / 2 val rippleX = (width - indicatorRipplePlaceable.width) / 2 val rippleY = selectedIconY - IndicatorVerticalPaddingWithLabel.toPx() return layout(width, height.roundToInt()) { indicatorPlaceable?.let { val indicatorX = (width - it.width) / 2 val indicatorY = selectedIconY - IndicatorVerticalPaddingWithLabel.toPx() it.placeRelative(indicatorX, (indicatorY + offset).roundToInt()) } if (alwaysShowLabel || animationProgress != 0f) { labelPlaceable.placeRelative(labelX, (labelY + offset).roundToInt()) } iconPlaceable.placeRelative(iconX, (selectedIconY + offset).roundToInt()) indicatorRipplePlaceable.placeRelative(rippleX, (rippleY + offset).roundToInt()) } } private const val IndicatorRippleLayoutIdTag: String = "indicatorRipple" private const val IndicatorLayoutIdTag: String = "indicator" private const val IconLayoutIdTag: String = "icon" private const val LabelLayoutIdTag: String = "label" /** * Vertical padding between the contents of the [NavigationRail] and its top/bottom, and internally * between items. */ internal val NavigationRailVerticalPadding: Dp = 4.dp /** * Padding at the bottom of the [NavigationRail]'s header. This padding will only be added when the * header is not null. */ private val NavigationRailHeaderPadding: Dp = 8.dp /*@VisibleForTesting*/ /** Width of an individual [NavigationRailItem]. */ internal val NavigationRailItemWidth: Dp = NavigationRailCollapsedTokens.NarrowContainerWidth /*@VisibleForTesting*/ /** Height of an individual [NavigationRailItem]. */ internal val NavigationRailItemHeight: Dp = NavigationRailVerticalItemTokens.ActiveIndicatorWidth /*@VisibleForTesting*/ /** Vertical padding between the contents of a [NavigationRailItem] and its top/bottom. */ internal val NavigationRailItemVerticalPadding: Dp = 4.dp private val IndicatorHorizontalPadding: Dp = (NavigationRailVerticalItemTokens.ActiveIndicatorWidth - NavigationRailBaselineItemTokens.IconSize) / 2 private val IndicatorVerticalPaddingWithLabel: Dp = (NavigationRailVerticalItemTokens.ActiveIndicatorHeight - NavigationRailBaselineItemTokens.IconSize) / 2 private val IndicatorVerticalPaddingNoLabel: Dp = (NavigationRailVerticalItemTokens.ActiveIndicatorWidth - NavigationRailBaselineItemTokens.IconSize) / 2 /** * Interface that allows libraries to override the behavior of the [NavigationRail] component. * * To override this component, implement the member function of this interface, then provide the * implementation to [LocalNavigationRailOverride] in the Compose hierarchy. */ @ExperimentalMaterial3ComponentOverrideApi interface NavigationRailOverride { /** Behavior function that is called by the [NavigationRail] component. */ @Composable fun NavigationRailOverrideScope.NavigationRail() } /** * Parameters available to [NavigationRail]. * * @param modifier the [Modifier] to be applied to this navigation rail * @param containerColor the color used for the background of this navigation rail. Use * [Color.Transparent] to have no color. * @param contentColor the preferred color for content inside this navigation rail. Defaults to * either the matching content color for [containerColor], or to the current [LocalContentColor] * if [containerColor] is not a color from the theme. * @param header optional header that may hold a [FloatingActionButton] or a logo * @param windowInsets a window insets of the navigation rail. * @param content the content of this navigation rail, typically 3-7 [NavigationRailItem]s */ @ExperimentalMaterial3ComponentOverrideApi class NavigationRailOverrideScope internal constructor( val modifier: Modifier = Modifier, val containerColor: Color, val contentColor: Color, val header: @Composable (ColumnScope.() -> Unit)?, val windowInsets: WindowInsets, val content: @Composable ColumnScope.() -> Unit, ) /** CompositionLocal containing the currently-selected [NavigationRailOverride]. */ @ExperimentalMaterial3ComponentOverrideApi val LocalNavigationRailOverride: ProvidableCompositionLocal = compositionLocalOf { DefaultNavigationRailOverride } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/NavigationDrawer.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.TweenSpec import androidx.compose.animation.core.animate import androidx.compose.animation.core.snap import androidx.compose.foundation.gestures.AnchoredDraggableDefaults import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.gestures.snapTo import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.internal.BackEventCompat import androidx.compose.material3.internal.FloatProducer import androidx.compose.material3.internal.PredictiveBack import androidx.compose.material3.internal.PredictiveBackHandler import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.getString import androidx.compose.material3.internal.systemBarsForVisualComponents import androidx.compose.material3.tokens.ElevationTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.NavigationDrawerTokens import androidx.compose.material3.tokens.ScrimTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.type import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.dismiss import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMaxOfOrNull import androidx.compose.ui.util.lerp import kotlin.math.roundToInt import kotlinx.coroutines.CancellationException import kotlinx.coroutines.launch /** Possible values of [DrawerState]. */ enum class DrawerValue { /** The state of the drawer when it is closed. */ Closed, /** The state of the drawer when it is open. */ Open, } /** * State of the [ModalNavigationDrawer] and [DismissibleNavigationDrawer] composable. * * @param initialValue The initial value of the state. * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. */ @Suppress("NotCloseable") @Stable class DrawerState( initialValue: DrawerValue, internal val confirmStateChange: (DrawerValue) -> Boolean = { true }, ) { internal var anchoredDraggableMotionSpec: FiniteAnimationSpec = AnchoredDraggableDefaultAnimationSpec @Suppress("Deprecation") internal val anchoredDraggableState = AnchoredDraggableState( initialValue = initialValue, snapAnimationSpec = anchoredDraggableMotionSpec, decayAnimationSpec = AnchoredDraggableDefaults.DecayAnimationSpec, confirmValueChange = confirmStateChange, positionalThreshold = { distance: Float -> distance * DrawerPositionalThreshold }, velocityThreshold = { with(requireDensity()) { DrawerVelocityThreshold.toPx() } }, ) /** Whether the drawer is open. */ val isOpen: Boolean get() = currentValue == DrawerValue.Open /** Whether the drawer is closed. */ val isClosed: Boolean get() = currentValue == DrawerValue.Closed /** * The current value of the state. * * If no swipe or animation is in progress, this corresponds to the start the drawer currently * in. If a swipe or an animation is in progress, this corresponds the state drawer was in * before the swipe or animation started. */ val currentValue: DrawerValue get() { return anchoredDraggableState.settledValue } /** Whether the state is currently animating. */ val isAnimationRunning: Boolean get() { return anchoredDraggableState.isAnimationRunning } /** * Open the drawer with animation and suspend until it if fully opened or animation has been * cancelled. This method will throw [CancellationException] if the animation is interrupted * * @return the reason the open animation ended */ suspend fun open() = animateTo(targetValue = DrawerValue.Open, animationSpec = openDrawerMotionSpec) /** * Close the drawer with animation and suspend until it if fully closed or animation has been * cancelled. This method will throw [CancellationException] if the animation is interrupted * * @return the reason the close animation ended */ suspend fun close() = animateTo(targetValue = DrawerValue.Closed, animationSpec = closeDrawerMotionSpec) /** * Set the state of the drawer with specific animation * * @param targetValue The new value to animate to. * @param anim The animation that will be used to animate to the new value. */ @Deprecated( message = "This method has been replaced by the open and close methods. The animation " + "spec is now an implementation detail of ModalDrawer." ) suspend fun animateTo(targetValue: DrawerValue, anim: AnimationSpec) { animateTo(targetValue = targetValue, animationSpec = anim) } /** * Set the state without any animation and suspend until it's set * * @param targetValue The new target value */ suspend fun snapTo(targetValue: DrawerValue) { anchoredDraggableState.snapTo(targetValue) } /** * The target value of the drawer state. * * If a swipe is in progress, this is the value that the Drawer would animate to if the swipe * finishes. If an animation is running, this is the target value of that animation. Finally, if * no swipe or animation is in progress, this is the same as the [currentValue]. */ val targetValue: DrawerValue get() = anchoredDraggableState.targetValue /** * The current position (in pixels) of the drawer sheet, or Float.NaN before the offset is * initialized. * * @see [AnchoredDraggableState.offset] for more information. */ @Deprecated( message = "Please access the offset through currentOffset, which returns the value " + "directly instead of wrapping it in a state object.", replaceWith = ReplaceWith("currentOffset"), ) val offset: State = object : State { override val value: Float get() = anchoredDraggableState.offset } /** * The current position (in pixels) of the drawer sheet, or Float.NaN before the offset is * initialized. * * @see [AnchoredDraggableState.offset] for more information. */ val currentOffset: Float get() = anchoredDraggableState.offset internal var density: Density? by mutableStateOf(null) internal var openDrawerMotionSpec: FiniteAnimationSpec = snap() internal var closeDrawerMotionSpec: FiniteAnimationSpec = snap() private fun requireDensity() = requireNotNull(density) { "The density on DrawerState ($this) was not set. Did you use DrawerState" + " with the ModalNavigationDrawer or DismissibleNavigationDrawer composables?" } internal fun requireOffset(): Float = anchoredDraggableState.requireOffset() private suspend fun animateTo( targetValue: DrawerValue, animationSpec: AnimationSpec, velocity: Float = anchoredDraggableState.lastVelocity, ) { anchoredDraggableState.anchoredDrag(targetValue = targetValue) { anchors, latestTarget -> val targetOffset = anchors.positionOf(latestTarget) if (!targetOffset.isNaN()) { var prev = if (currentOffset.isNaN()) 0f else currentOffset animate(prev, targetOffset, velocity, animationSpec) { value, velocity -> // Our onDrag coerces the value within the bounds, but an animation may // overshoot, for example a spring animation or an overshooting interpolator // We respect the user's intention and allow the overshoot, but still use // DraggableState's drag for its mutex. dragTo(value, velocity) prev = value } } } } companion object { /** The default [Saver] implementation for [DrawerState]. */ fun Saver(confirmStateChange: (DrawerValue) -> Boolean) = Saver( save = { it.currentValue }, restore = { DrawerState(it, confirmStateChange) }, ) } } /** * Create and [remember] a [DrawerState]. * * @param initialValue The initial value of the state. * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change. */ @Composable fun rememberDrawerState( initialValue: DrawerValue, confirmStateChange: (DrawerValue) -> Boolean = { true }, ): DrawerState { return rememberSaveable(saver = DrawerState.Saver(confirmStateChange)) { DrawerState(initialValue, confirmStateChange) } } /** * [Material Design navigation drawer](https://m3.material.io/components/navigation-drawer/overview) * * Navigation drawers provide ergonomic access to destinations in an app. * * Modal navigation drawers block interaction with the rest of an app’s content with a scrim. They * are elevated above most of the app’s UI and don’t affect the screen’s layout grid. * * ![Navigation drawer * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-drawer.png) * * @sample androidx.compose.material3.samples.ModalNavigationDrawerSample * @param drawerContent content inside this drawer * @param modifier the [Modifier] to be applied to this drawer * @param drawerState state of the drawer * @param gesturesEnabled whether or not the drawer can be interacted by gestures * @param scrimColor color of the scrim that obscures content when the drawer is open * @param content content of the rest of the UI */ @Composable fun ModalNavigationDrawer( drawerContent: @Composable () -> Unit, modifier: Modifier = Modifier, drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), gesturesEnabled: Boolean = true, scrimColor: Color = DrawerDefaults.scrimColor, content: @Composable () -> Unit, ) { val scope = rememberCoroutineScope() val navigationMenu = getString(Strings.NavigationMenu) val density = LocalDensity.current var anchorsInitialized by remember { mutableStateOf(false) } var minValue by remember(density) { mutableFloatStateOf(0f) } val maxValue = 0f val focusRequester = remember { FocusRequester() } // TODO Load the motionScheme tokens from the component tokens file val anchoredDraggableMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val openMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val closeMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.FastEffects.value() SideEffect { drawerState.density = density drawerState.openDrawerMotionSpec = openMotion drawerState.closeDrawerMotionSpec = closeMotion drawerState.anchoredDraggableMotionSpec = anchoredDraggableMotion } LaunchedEffect(drawerState.isOpen) { if (drawerState.isOpen) { // Keyboard focus should go to first element of the drawer when it opens focusRequester.requestFocus() } } val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl Box( modifier .fillMaxSize() .anchoredDraggable( state = drawerState.anchoredDraggableState, orientation = Orientation.Horizontal, enabled = gesturesEnabled, reverseDirection = isRtl, ) ) { Box { content() } val onDismissRequest = { if (gesturesEnabled && drawerState.confirmStateChange(DrawerValue.Closed)) { scope.launch { drawerState.close() } } } Scrim( contentDescription = getString(Strings.CloseDrawer), onClick = if (drawerState.isOpen) onDismissRequest else null, alpha = { calculateFraction(minValue, maxValue, drawerState.requireOffset()) }, color = scrimColor, ) Layout( content = drawerContent, modifier = Modifier.offset { drawerState.currentOffset.let { offset -> val offsetX = when { !offset.isNaN() -> offset.roundToInt() // If offset is NaN, set offset based on open/closed state drawerState.isOpen -> 0 else -> -DrawerDefaults.MaximumDrawerWidth.roundToPx() } IntOffset(offsetX, 0) } } .semantics { paneTitle = navigationMenu if (drawerState.isOpen) { dismiss { if (drawerState.confirmStateChange(DrawerValue.Closed)) { scope.launch { drawerState.close() } } true } } } .onKeyEvent { // Drawer should close via escape key. if ( drawerState.isOpen && it.type == KeyEventType.KeyUp && it.key == Key.Escape ) { scope.launch { drawerState.close() } return@onKeyEvent true } return@onKeyEvent false } .focusRequester(focusRequester), ) { measurables, constraints -> val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) val placeables = measurables.fastMap { it.measure(looseConstraints) } val width = placeables.fastMaxOfOrNull { it.width } ?: 0 val height = placeables.fastMaxOfOrNull { it.height } ?: 0 layout(width, height) { val currentClosedAnchor = drawerState.anchoredDraggableState.anchors.positionOf(DrawerValue.Closed) val calculatedClosedAnchor = -width.toFloat() if (!anchorsInitialized || currentClosedAnchor != calculatedClosedAnchor) { if (!anchorsInitialized) { anchorsInitialized = true } minValue = calculatedClosedAnchor drawerState.anchoredDraggableState.updateAnchors( DraggableAnchors { DrawerValue.Closed at minValue DrawerValue.Open at maxValue } ) } val isDrawerVisible = calculateFraction(minValue, maxValue, drawerState.requireOffset()) != 0f if (isDrawerVisible) { // Only place the drawer when it's visible so that keyboard focus doesn't // navigate to an offscreen element. placeables.fastForEach { it.placeRelative(0, 0) } } } } } } /** * [Material Design navigation drawer](https://m3.material.io/components/navigation-drawer/overview) * * Navigation drawers provide ergonomic access to destinations in an app. They’re often next to app * content and affect the screen’s layout grid. * * ![Navigation drawer * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-drawer.png) * * Dismissible standard drawers can be used for layouts that prioritize content (such as a photo * gallery) or for apps where users are unlikely to switch destinations often. They should use a * visible navigation menu icon to open and close the drawer. * * @sample androidx.compose.material3.samples.DismissibleNavigationDrawerSample * @param drawerContent content inside this drawer * @param modifier the [Modifier] to be applied to this drawer * @param drawerState state of the drawer * @param gesturesEnabled whether or not the drawer can be interacted by gestures * @param content content of the rest of the UI */ @Composable fun DismissibleNavigationDrawer( drawerContent: @Composable () -> Unit, modifier: Modifier = Modifier, drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed), gesturesEnabled: Boolean = true, content: @Composable () -> Unit, ) { var anchorsInitialized by remember { mutableStateOf(false) } val density = LocalDensity.current val focusRequester = remember { FocusRequester() } // TODO Load the motionScheme tokens from the component tokens file val openMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val closeMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.FastEffects.value() SideEffect { drawerState.density = density drawerState.openDrawerMotionSpec = openMotion drawerState.closeDrawerMotionSpec = closeMotion } LaunchedEffect(drawerState.isOpen) { if (drawerState.isOpen) { // Keyboard focus should go to first element of the drawer when it opens focusRequester.requestFocus() } } val scope = rememberCoroutineScope() val navigationMenu = getString(Strings.NavigationMenu) val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl Box( modifier.anchoredDraggable( state = drawerState.anchoredDraggableState, orientation = Orientation.Horizontal, enabled = gesturesEnabled, reverseDirection = isRtl, ) ) { Layout( content = { Box( Modifier.semantics { paneTitle = navigationMenu if (drawerState.isOpen) { dismiss { if (drawerState.confirmStateChange(DrawerValue.Closed)) { scope.launch { drawerState.close() } } true } } } .onKeyEvent { // Drawer should close via escape key. if ( drawerState.isOpen && it.type == KeyEventType.KeyUp && it.key == Key.Escape ) { scope.launch { drawerState.close() } return@onKeyEvent true } return@onKeyEvent false } .focusRequester(focusRequester) ) { drawerContent() } Box { content() } } ) { measurables, constraints -> val sheetPlaceable = measurables[0].measure(constraints) val contentPlaceable = measurables[1].measure(constraints) layout(contentPlaceable.width, contentPlaceable.height) { val currentClosedAnchor = drawerState.anchoredDraggableState.anchors.positionOf(DrawerValue.Closed) val calculatedClosedAnchor = -sheetPlaceable.width.toFloat() if (!anchorsInitialized || currentClosedAnchor != calculatedClosedAnchor) { if (!anchorsInitialized) { anchorsInitialized = true } drawerState.anchoredDraggableState.updateAnchors( DraggableAnchors { DrawerValue.Closed at calculatedClosedAnchor DrawerValue.Open at 0f } ) } val contentX = sheetPlaceable.width + drawerState.requireOffset().roundToInt() contentPlaceable.placeRelative(contentX, 0) // The drawer is visible when the content has been offset. if (contentX != 0) { // Only place the drawer when it's visible so that keyboard focus doesn't // navigate to an offscreen element. sheetPlaceable.placeRelative(drawerState.requireOffset().roundToInt(), 0) } } } } } /** * [Material Design navigation permanent * drawer](https://m3.material.io/components/navigation-drawer/overview) * * Navigation drawers provide ergonomic access to destinations in an app. They’re often next to app * content and affect the screen’s layout grid. * * ![Navigation drawer * image](https://developer.android.com/images/reference/androidx/compose/material3/navigation-drawer.png) * * The permanent navigation drawer is always visible and usually used for frequently switching * destinations. On mobile screens, use [ModalNavigationDrawer] instead. * * @sample androidx.compose.material3.samples.PermanentNavigationDrawerSample * @param drawerContent content inside this drawer * @param modifier the [Modifier] to be applied to this drawer * @param content content of the rest of the UI */ @Composable fun PermanentNavigationDrawer( drawerContent: @Composable () -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { Row(modifier.fillMaxSize()) { drawerContent() Box { content() } } } /** * Content inside of a modal navigation drawer. * * Note: This version of [ModalDrawerSheet] does not handle back by default. For automatic back * handling and predictive back animations on Android 14+, use the [ModalDrawerSheet] that accepts * `drawerState` as a param. * * @param modifier the [Modifier] to be applied to this drawer's content * @param drawerShape defines the shape of this drawer's container * @param drawerContainerColor the color used for the background of this drawer. Use * [Color.Transparent] to have no color. * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if * [drawerContainerColor] is not a color from the theme. * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent * primary color overlay is applied on top of the container. A higher tonal elevation value will * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets for the sheet. * @param content content inside of a modal navigation drawer */ @Composable fun ModalDrawerSheet( modifier: Modifier = Modifier, drawerShape: Shape = DrawerDefaults.shape, drawerContainerColor: Color = DrawerDefaults.modalContainerColor, drawerContentColor: Color = contentColorFor(drawerContainerColor), drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation, windowInsets: WindowInsets = DrawerDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { DrawerSheet( drawerPredictiveBackState = null, windowInsets = windowInsets, modifier = modifier, drawerShape = drawerShape, drawerContainerColor = drawerContainerColor, drawerContentColor = drawerContentColor, drawerTonalElevation = drawerTonalElevation, content = content, ) } /** * Content inside of a modal navigation drawer. * * Note: This version of [ModalDrawerSheet] requires a [drawerState] to be provided and will handle * back by default for all Android versions, as well as animate during predictive back on Android * 14+. * * @param drawerState state of the drawer * @param modifier the [Modifier] to be applied to this drawer's content * @param drawerShape defines the shape of this drawer's container * @param drawerContainerColor the color used for the background of this drawer. Use * [Color.Transparent] to have no color. * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if * [drawerContainerColor] is not a color from the theme. * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent * primary color overlay is applied on top of the container. A higher tonal elevation value will * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets for the sheet. * @param content content inside of a modal navigation drawer */ @Composable fun ModalDrawerSheet( drawerState: DrawerState, modifier: Modifier = Modifier, drawerShape: Shape = DrawerDefaults.shape, drawerContainerColor: Color = DrawerDefaults.modalContainerColor, drawerContentColor: Color = contentColorFor(drawerContainerColor), drawerTonalElevation: Dp = DrawerDefaults.ModalDrawerElevation, windowInsets: WindowInsets = DrawerDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState -> DrawerSheet( drawerPredictiveBackState = drawerPredictiveBackState, windowInsets = windowInsets, modifier = modifier, drawerShape = drawerShape, drawerContainerColor = drawerContainerColor, drawerContentColor = drawerContentColor, drawerTonalElevation = drawerTonalElevation, drawerOffset = { drawerState.anchoredDraggableState.offset }, content = content, ) } } /** * Content inside of a dismissible navigation drawer. * * Note: This version of [DismissibleDrawerSheet] does not handle back by default. For automatic * back handling and predictive back animations on Android 14+, use the [DismissibleDrawerSheet] * that accepts `drawerState` as a param. * * @param modifier the [Modifier] to be applied to this drawer's content * @param drawerShape defines the shape of this drawer's container * @param drawerContainerColor the color used for the background of this drawer. Use * [Color.Transparent] to have no color. * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if * [drawerContainerColor] is not a color from the theme. * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent * primary color overlay is applied on top of the container. A higher tonal elevation value will * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets for the sheet. * @param content content inside of a dismissible navigation drawer */ @Composable fun DismissibleDrawerSheet( modifier: Modifier = Modifier, drawerShape: Shape = RectangleShape, drawerContainerColor: Color = DrawerDefaults.standardContainerColor, drawerContentColor: Color = contentColorFor(drawerContainerColor), drawerTonalElevation: Dp = DrawerDefaults.DismissibleDrawerElevation, windowInsets: WindowInsets = DrawerDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { DrawerSheet( drawerPredictiveBackState = null, windowInsets = windowInsets, modifier = modifier, drawerShape = drawerShape, drawerContainerColor = drawerContainerColor, drawerContentColor = drawerContentColor, drawerTonalElevation = drawerTonalElevation, content = content, ) } /** * Content inside of a dismissible navigation drawer. * * Note: This version of [DismissibleDrawerSheet] requires a [drawerState] to be provided and will * handle back by default for all Android versions, as well as animate during predictive back on * Android 14+. * * @param drawerState state of the drawer * @param modifier the [Modifier] to be applied to this drawer's content * @param drawerShape defines the shape of this drawer's container * @param drawerContainerColor the color used for the background of this drawer. Use * [Color.Transparent] to have no color. * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if * [drawerContainerColor] is not a color from the theme. * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent * primary color overlay is applied on top of the container. A higher tonal elevation value will * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets for the sheet. * @param content content inside of a dismissible navigation drawer */ @Composable fun DismissibleDrawerSheet( drawerState: DrawerState, modifier: Modifier = Modifier, drawerShape: Shape = RectangleShape, drawerContainerColor: Color = DrawerDefaults.standardContainerColor, drawerContentColor: Color = contentColorFor(drawerContainerColor), drawerTonalElevation: Dp = DrawerDefaults.DismissibleDrawerElevation, windowInsets: WindowInsets = DrawerDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { DrawerPredictiveBackHandler(drawerState) { drawerPredictiveBackState -> DrawerSheet( drawerPredictiveBackState = drawerPredictiveBackState, windowInsets = windowInsets, modifier = modifier, drawerShape = drawerShape, drawerContainerColor = drawerContainerColor, drawerContentColor = drawerContentColor, drawerTonalElevation = drawerTonalElevation, drawerOffset = { drawerState.anchoredDraggableState.offset }, content = content, ) } } /** * Content inside of a permanent navigation drawer. * * @param modifier the [Modifier] to be applied to this drawer's content * @param drawerShape defines the shape of this drawer's container * @param drawerContainerColor the color used for the background of this drawer. Use * [Color.Transparent] to have no color. * @param drawerContentColor the preferred color for content inside this drawer. Defaults to either * the matching content color for [drawerContainerColor], or to the current [LocalContentColor] if * [drawerContainerColor] is not a color from the theme. * @param drawerTonalElevation when [drawerContainerColor] is [ColorScheme.surface], a translucent * primary color overlay is applied on top of the container. A higher tonal elevation value will * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param windowInsets a window insets for the sheet. * @param content content inside a permanent navigation drawer */ @Composable fun PermanentDrawerSheet( modifier: Modifier = Modifier, drawerShape: Shape = RectangleShape, drawerContainerColor: Color = DrawerDefaults.standardContainerColor, drawerContentColor: Color = contentColorFor(drawerContainerColor), drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation, windowInsets: WindowInsets = DrawerDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { val navigationMenu = getString(Strings.NavigationMenu) DrawerSheet( drawerPredictiveBackState = null, windowInsets = windowInsets, modifier = modifier.semantics { paneTitle = navigationMenu }, drawerShape = drawerShape, drawerContainerColor = drawerContainerColor, drawerContentColor = drawerContentColor, drawerTonalElevation = drawerTonalElevation, content = content, ) } @Composable internal fun DrawerSheet( drawerPredictiveBackState: DrawerPredictiveBackState?, windowInsets: WindowInsets, modifier: Modifier = Modifier, drawerShape: Shape = RectangleShape, drawerContainerColor: Color = DrawerDefaults.standardContainerColor, drawerContentColor: Color = contentColorFor(drawerContainerColor), drawerTonalElevation: Dp = DrawerDefaults.PermanentDrawerElevation, drawerOffset: FloatProducer = FloatProducer { 0F }, content: @Composable ColumnScope.() -> Unit, ) { val density = LocalDensity.current val maxWidth = NavigationDrawerTokens.ContainerWidth val maxWidthPx = with(density) { maxWidth.toPx() } val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val predictiveBackDrawerContainerModifier = if (drawerPredictiveBackState != null) { Modifier.predictiveBackDrawerContainer(drawerPredictiveBackState, isRtl) } else { Modifier } Surface( modifier = modifier .sizeIn(minWidth = MinimumDrawerWidth, maxWidth = maxWidth) // Scale up the Surface horizontally in case the drawer offset it greater than zero. // This is done to avoid showing a gap when the drawer opens and bounces when it's // applied with a bouncy motion. Note that the content inside the Surface is scaled // back down to maintain its aspect ratio (see below). .horizontalScaleUp( drawerOffset = drawerOffset, drawerWidth = maxWidthPx, isRtl = isRtl, ) .then(predictiveBackDrawerContainerModifier) .fillMaxHeight(), shape = drawerShape, color = drawerContainerColor, contentColor = drawerContentColor, tonalElevation = drawerTonalElevation, ) { val predictiveBackDrawerChildModifier = if (drawerPredictiveBackState != null) Modifier.predictiveBackDrawerChild(drawerPredictiveBackState, isRtl) else Modifier Column( Modifier.sizeIn(minWidth = MinimumDrawerWidth, maxWidth = maxWidth) // Scale the content down in case the drawer offset is greater than one. The // wrapping Surface is scaled up, so this is done to maintain the content's aspect // ratio. .horizontalScaleDown( drawerOffset = drawerOffset, drawerWidth = maxWidthPx, isRtl = isRtl, ) .then(predictiveBackDrawerChildModifier) .windowInsetsPadding(windowInsets), content = content, ) } } /** * A [Modifier] that scales up the drawing layer on the X axis in case the [drawerOffset] is greater * than zero. The scaling will ensure that there is no visible gap between the drawer and the edge * of the screen in case the drawer bounces when it opens due to a more expressive motion setting. * * A [horizontalScaleDown] should be applied to the content of the drawer to maintain the content * aspect ratio as the container scales up. * * @see horizontalScaleDown */ private fun Modifier.horizontalScaleUp( drawerOffset: FloatProducer, drawerWidth: Float, isRtl: Boolean, ) = graphicsLayer { val offset = drawerOffset() scaleX = if (offset > 0f) 1f + offset / drawerWidth else 1f transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0.5f) } /** * A [Modifier] that scales down the drawing layer on the X axis in case the [drawerOffset] is * greater than zero. This modifier should be applied to the content inside a component that was * scaled up with a [horizontalScaleUp] modifier. It will ensure that the content maintains its * aspect ratio as the container scales up. * * @see horizontalScaleUp */ private fun Modifier.horizontalScaleDown( drawerOffset: FloatProducer, drawerWidth: Float, isRtl: Boolean, ) = graphicsLayer { val offset = drawerOffset() scaleX = if (offset > 0f) 1 / (1f + offset / drawerWidth) else 1f transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0f) } private fun Modifier.predictiveBackDrawerContainer( drawerPredictiveBackState: DrawerPredictiveBackState, isRtl: Boolean, ) = graphicsLayer { scaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState) scaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState) transformOrigin = TransformOrigin(if (isRtl) 1f else 0f, 0.5f) } private fun Modifier.predictiveBackDrawerChild( drawerPredictiveBackState: DrawerPredictiveBackState, isRtl: Boolean, ) = graphicsLayer { // Preserve the original aspect ratio and container alignment of the child // content, and add content margins. val containerScaleX = calculatePredictiveBackScaleX(drawerPredictiveBackState) val containerScaleY = calculatePredictiveBackScaleY(drawerPredictiveBackState) scaleX = if (containerScaleX != 0f) containerScaleY / containerScaleX else 1f transformOrigin = TransformOrigin(if (isRtl) 0f else 1f, 0f) } private fun GraphicsLayerScope.calculatePredictiveBackScaleX( drawerPredictiveBackState: DrawerPredictiveBackState ): Float { val width = size.width return if (width.isNaN() || width == 0f) { 1f } else { val scaleXDirection = if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) 1 else -1 1f + drawerPredictiveBackState.scaleXDistance * scaleXDirection / width } } private fun GraphicsLayerScope.calculatePredictiveBackScaleY( drawerPredictiveBackState: DrawerPredictiveBackState ): Float { val height = size.height return if (height.isNaN() || height == 0f) { 1f } else { 1f - drawerPredictiveBackState.scaleYDistance / height } } /** * Registers a [PredictiveBackHandler] and provides animation values in [DrawerPredictiveBackState] * based on back progress. * * @param drawerState state of the drawer * @param content content of the rest of the UI */ @Composable internal fun DrawerPredictiveBackHandler( drawerState: DrawerState, content: @Composable (DrawerPredictiveBackState) -> Unit, ) { val drawerPredictiveBackState = remember { DrawerPredictiveBackState() } val scope = rememberCoroutineScope() val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl val maxScaleXDistanceGrow: Float val maxScaleXDistanceShrink: Float val maxScaleYDistance: Float with(LocalDensity.current) { maxScaleXDistanceGrow = PredictiveBackDrawerMaxScaleXDistanceGrow.toPx() maxScaleXDistanceShrink = PredictiveBackDrawerMaxScaleXDistanceShrink.toPx() maxScaleYDistance = PredictiveBackDrawerMaxScaleYDistance.toPx() } PredictiveBackHandler(enabled = drawerState.isOpen) { progress -> try { progress.collect { backEvent -> drawerPredictiveBackState.update( PredictiveBack.transform(backEvent.progress), backEvent.swipeEdge == BackEventCompat.EDGE_LEFT, isRtl, maxScaleXDistanceGrow, maxScaleXDistanceShrink, maxScaleYDistance, ) } } catch (e: kotlin.coroutines.cancellation.CancellationException) { drawerPredictiveBackState.clear() } finally { if (drawerPredictiveBackState.swipeEdgeMatchesDrawer) { // If swipe edge matches drawer gravity and we've stretched the drawer horizontally, // un-stretch it smoothly so that it hides completely during the drawer close. scope.launch { animate( initialValue = drawerPredictiveBackState.scaleXDistance, targetValue = 0f, ) { value, _ -> drawerPredictiveBackState.scaleXDistance = value } drawerPredictiveBackState.clear() } } drawerState.close() } } LaunchedEffect(drawerState.isClosed) { if (drawerState.isClosed) { drawerPredictiveBackState.clear() } } content(drawerPredictiveBackState) } /** Object to hold default values for [ModalNavigationDrawer] */ object DrawerDefaults { /** Default Elevation for drawer container in the [ModalNavigationDrawer]. */ val ModalDrawerElevation = ElevationTokens.Level0 /** Default Elevation for drawer container in the [PermanentNavigationDrawer]. */ val PermanentDrawerElevation = NavigationDrawerTokens.StandardContainerElevation /** Default Elevation for drawer container in the [DismissibleNavigationDrawer]. */ val DismissibleDrawerElevation = NavigationDrawerTokens.StandardContainerElevation /** Default shape for a navigation drawer. */ val shape: Shape @Composable get() = NavigationDrawerTokens.ContainerShape.value /** Default color of the scrim that obscures content when the drawer is open */ val scrimColor: Color @Composable get() = ScrimTokens.ContainerColor.value.copy(ScrimTokens.ContainerOpacity) /** Default container color for a navigation drawer */ @Deprecated( message = "Please use standardContainerColor or modalContainerColor instead.", replaceWith = ReplaceWith("standardContainerColor"), level = DeprecationLevel.WARNING, ) val containerColor: Color @Composable get() = NavigationDrawerTokens.StandardContainerColor.value /** * Default container color for a [DismissibleNavigationDrawer] and [PermanentNavigationDrawer] */ val standardContainerColor: Color @Composable get() = NavigationDrawerTokens.StandardContainerColor.value /** Default container color for a [ModalNavigationDrawer] */ val modalContainerColor: Color @Composable get() = NavigationDrawerTokens.ModalContainerColor.value /** Default and maximum width of a navigation drawer */ val MaximumDrawerWidth = NavigationDrawerTokens.ContainerWidth /** Default window insets for drawer sheets */ val windowInsets: WindowInsets @Composable get() = WindowInsets.systemBarsForVisualComponents.only( WindowInsetsSides.Vertical + WindowInsetsSides.Start ) } /** * Material Design navigation drawer item. * * A [NavigationDrawerItem] represents a destination within drawers, either [ModalNavigationDrawer], * [PermanentNavigationDrawer] or [DismissibleNavigationDrawer]. * * @sample androidx.compose.material3.samples.ModalNavigationDrawerSample * @param label text label for this item * @param selected whether this item is selected * @param onClick called when this item is clicked * @param modifier the [Modifier] to be applied to this item * @param icon optional icon for this item, typically an [Icon] * @param badge optional badge to show on this item from the end side * @param shape optional shape for the active indicator * @param colors [NavigationDrawerItemColors] that will be used to resolve the colors used for this * item in different states. See [NavigationDrawerItemDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this item. You can use this to change the item's appearance or * preview the item in different states. Note that if `null` is provided, interactions will still * happen internally. */ @Composable fun NavigationDrawerItem( label: @Composable () -> Unit, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, icon: (@Composable () -> Unit)? = null, badge: (@Composable () -> Unit)? = null, shape: Shape = NavigationDrawerTokens.ActiveIndicatorShape.value, colors: NavigationDrawerItemColors = NavigationDrawerItemDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { Surface( selected = selected, onClick = onClick, modifier = modifier .semantics { role = Role.Tab } .heightIn(min = NavigationDrawerTokens.ActiveIndicatorHeight) .fillMaxWidth(), shape = shape, color = colors.containerColor(selected).value, interactionSource = interactionSource, ) { Row( Modifier.padding(start = 16.dp, end = 24.dp), verticalAlignment = Alignment.CenterVertically, ) { if (icon != null) { val iconColor = colors.iconColor(selected).value CompositionLocalProvider(LocalContentColor provides iconColor, content = icon) Spacer(Modifier.width(12.dp)) } Box(Modifier.weight(1f)) { val labelColor = colors.textColor(selected).value CompositionLocalProvider(LocalContentColor provides labelColor, content = label) } if (badge != null) { Spacer(Modifier.width(12.dp)) val badgeColor = colors.badgeColor(selected).value CompositionLocalProvider(LocalContentColor provides badgeColor, content = badge) } } } } /** Represents the colors of the various elements of a drawer item. */ @Stable interface NavigationDrawerItemColors { /** * Represents the icon color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected */ @Composable fun iconColor(selected: Boolean): State /** * Represents the text color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected */ @Composable fun textColor(selected: Boolean): State /** * Represents the badge color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected */ @Composable fun badgeColor(selected: Boolean): State /** * Represents the container color for this item, depending on whether it is [selected]. * * @param selected whether the item is selected */ @Composable fun containerColor(selected: Boolean): State } /** Defaults used in [NavigationDrawerItem]. */ object NavigationDrawerItemDefaults { /** * Creates a [NavigationDrawerItemColors] with the provided colors according to the Material * specification. * * @param selectedContainerColor the color to use for the background of the item when selected * @param unselectedContainerColor the color to use for the background of the item when * unselected * @param selectedIconColor the color to use for the icon when the item is selected. * @param unselectedIconColor the color to use for the icon when the item is unselected. * @param selectedTextColor the color to use for the text label when the item is selected. * @param unselectedTextColor the color to use for the text label when the item is unselected. * @param selectedBadgeColor the color to use for the badge when the item is selected. * @param unselectedBadgeColor the color to use for the badge when the item is unselected. * @return the resulting [NavigationDrawerItemColors] used for [NavigationDrawerItem] */ @Composable fun colors( selectedContainerColor: Color = NavigationDrawerTokens.ActiveIndicatorColor.value, unselectedContainerColor: Color = Color.Transparent, selectedIconColor: Color = NavigationDrawerTokens.ActiveIconColor.value, unselectedIconColor: Color = NavigationDrawerTokens.InactiveIconColor.value, selectedTextColor: Color = NavigationDrawerTokens.ActiveLabelTextColor.value, unselectedTextColor: Color = NavigationDrawerTokens.InactiveLabelTextColor.value, selectedBadgeColor: Color = selectedTextColor, unselectedBadgeColor: Color = unselectedTextColor, ): NavigationDrawerItemColors = DefaultDrawerItemsColor( selectedIconColor, unselectedIconColor, selectedTextColor, unselectedTextColor, selectedContainerColor, unselectedContainerColor, selectedBadgeColor, unselectedBadgeColor, ) /** * Default external padding for a [NavigationDrawerItem] according to the Material * specification. */ val ItemPadding = PaddingValues(horizontal = 12.dp) } @Stable internal class DrawerPredictiveBackState { var swipeEdgeMatchesDrawer by mutableStateOf(true) var scaleXDistance by mutableFloatStateOf(0f) var scaleYDistance by mutableFloatStateOf(0f) fun update( progress: Float, swipeEdgeLeft: Boolean, isRtl: Boolean, maxScaleXDistanceGrow: Float, maxScaleXDistanceShrink: Float, maxScaleYDistance: Float, ) { swipeEdgeMatchesDrawer = swipeEdgeLeft != isRtl val maxScaleXDistance = if (swipeEdgeMatchesDrawer) maxScaleXDistanceGrow else maxScaleXDistanceShrink scaleXDistance = lerp(0f, maxScaleXDistance, progress) scaleYDistance = lerp(0f, maxScaleYDistance, progress) } fun clear() { swipeEdgeMatchesDrawer = true scaleXDistance = 0f scaleYDistance = 0f } } private class DefaultDrawerItemsColor( val selectedIconColor: Color, val unselectedIconColor: Color, val selectedTextColor: Color, val unselectedTextColor: Color, val selectedContainerColor: Color, val unselectedContainerColor: Color, val selectedBadgeColor: Color, val unselectedBadgeColor: Color, ) : NavigationDrawerItemColors { @Composable override fun iconColor(selected: Boolean): State { return rememberUpdatedState(if (selected) selectedIconColor else unselectedIconColor) } @Composable override fun textColor(selected: Boolean): State { return rememberUpdatedState(if (selected) selectedTextColor else unselectedTextColor) } @Composable override fun containerColor(selected: Boolean): State { return rememberUpdatedState( if (selected) selectedContainerColor else unselectedContainerColor ) } @Composable override fun badgeColor(selected: Boolean): State { return rememberUpdatedState(if (selected) selectedBadgeColor else unselectedBadgeColor) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is DefaultDrawerItemsColor) return false if (selectedIconColor != other.selectedIconColor) return false if (unselectedIconColor != other.unselectedIconColor) return false if (selectedTextColor != other.selectedTextColor) return false if (unselectedTextColor != other.unselectedTextColor) return false if (selectedContainerColor != other.selectedContainerColor) return false if (unselectedContainerColor != other.unselectedContainerColor) return false if (selectedBadgeColor != other.selectedBadgeColor) return false return unselectedBadgeColor == other.unselectedBadgeColor } override fun hashCode(): Int { var result = selectedIconColor.hashCode() result = 31 * result + unselectedIconColor.hashCode() result = 31 * result + selectedTextColor.hashCode() result = 31 * result + unselectedTextColor.hashCode() result = 31 * result + selectedContainerColor.hashCode() result = 31 * result + unselectedContainerColor.hashCode() result = 31 * result + selectedBadgeColor.hashCode() result = 31 * result + unselectedBadgeColor.hashCode() return result } } private fun calculateFraction(a: Float, b: Float, pos: Float) = ((pos - a) / (b - a)).coerceIn(0f, 1f) private val DrawerPositionalThreshold = 0.5f private val DrawerVelocityThreshold = 400.dp private val MinimumDrawerWidth = 240.dp internal val PredictiveBackDrawerMaxScaleXDistanceGrow = 12.dp internal val PredictiveBackDrawerMaxScaleXDistanceShrink = 24.dp internal val PredictiveBackDrawerMaxScaleYDistance = 48.dp // TODO: b/177571613 this should be a proper decay settling // this is taken from the DrawerLayout's DragViewHelper as a min duration. private val AnchoredDraggableDefaultAnimationSpec = TweenSpec(durationMillis = 256) ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TextField.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.VectorConverter import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.AnimationVector4D import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.snap import androidx.compose.foundation.ScrollState import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.KeyboardActionHandler import androidx.compose.foundation.text.input.OutputTransformation import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material3.MaterialTheme.LocalMaterialTheme import androidx.compose.material3.TextFieldDefaults.defaultTextFieldColors import androidx.compose.material3.internal.AboveLabelBottomPadding import androidx.compose.material3.internal.AboveLabelHorizontalPadding import androidx.compose.material3.internal.ContainerId import androidx.compose.material3.internal.FloatProducer import androidx.compose.material3.internal.LabelId import androidx.compose.material3.internal.LeadingId import androidx.compose.material3.internal.MinFocusedLabelLineHeight import androidx.compose.material3.internal.MinSupportingTextLineHeight import androidx.compose.material3.internal.MinTextLineHeight import androidx.compose.material3.internal.PlaceholderId import androidx.compose.material3.internal.PrefixId import androidx.compose.material3.internal.PrefixSuffixTextPadding import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.SuffixId import androidx.compose.material3.internal.SupportingId import androidx.compose.material3.internal.TextFieldId import androidx.compose.material3.internal.TrailingId import androidx.compose.material3.internal.defaultErrorSemantics import androidx.compose.material3.internal.expandedAlignment import androidx.compose.material3.internal.getString import androidx.compose.material3.internal.heightOrZero import androidx.compose.material3.internal.layoutId import androidx.compose.material3.internal.minimizedAlignment import androidx.compose.material3.internal.minimizedLabelHalfHeight import androidx.compose.material3.internal.subtractConstraintSafely import androidx.compose.material3.internal.textFieldHorizontalIconPadding import androidx.compose.material3.internal.textFieldLabelMinHeight import androidx.compose.material3.internal.widthOrZero import androidx.compose.material3.tokens.FilledTextFieldTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.MotionTokens.EasingEmphasizedAccelerateCubicBezier import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.CacheDrawModifierNode import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.addOutline import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.offset import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.lerp import kotlin.math.max import kotlin.math.roundToInt import kotlinx.coroutines.Job import kotlinx.coroutines.launch /** * [Material Design filled text field](https://m3.material.io/components/text-fields/overview) * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Filled text fields have more visual emphasis than outlined text fields, making them stand out * when surrounded by other content and components. * * ![Filled text field * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-text-field.png) * * If you are looking for an outlined version, see [OutlinedTextField]. For a text field * specifically designed for passwords or other secure content, see [SecureTextField]. * * This overload of [TextField] uses [TextFieldState] to keep track of its text content and position * of the cursor or selection. * * A simple single line text field looks like: * * @sample androidx.compose.material3.samples.SimpleTextFieldSample * * You can control the initial text input and selection: * * @sample androidx.compose.material3.samples.TextFieldWithInitialValueAndSelection * * Use input and output transformations to control user input and the displayed text: * * @sample androidx.compose.material3.samples.TextFieldWithTransformations * * You may provide a placeholder: * * @sample androidx.compose.material3.samples.TextFieldWithPlaceholder * * You can also provide leading and trailing icons: * * @sample androidx.compose.material3.samples.TextFieldWithIcons * * You can also provide a prefix or suffix to the text: * * @sample androidx.compose.material3.samples.TextFieldWithPrefixAndSuffix * * To handle the error input state, use [isError] parameter: * * @sample androidx.compose.material3.samples.TextFieldWithErrorState * * Additionally, you may provide additional message at the bottom: * * @sample androidx.compose.material3.samples.TextFieldWithSupportingText * * You can change the content padding to create a dense text field: * * @sample androidx.compose.material3.samples.DenseTextFieldContentPadding * * Hiding a software keyboard on IME action performed: * * @sample androidx.compose.material3.samples.TextFieldWithHideKeyboardOnImeAction * @param state [TextFieldState] object that holds the internal editing state of the text field. * @param modifier the [Modifier] to be applied to this text field. * @param enabled controls the enabled state of this text field. When `false`, this component will * not respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param readOnly controls the editable state of the text field. When `true`, the text field cannot * be modified. However, a user can focus it and copy text from it. Read-only text fields are * usually used to display pre-filled forms that a user cannot edit. * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. * @param labelPosition the position of the label. See [TextFieldLabelPosition]. * @param label the optional label to be displayed with this text field. The default text style uses * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded. * @param placeholder the optional placeholder to be displayed when the input text is empty. The * default text style uses [Typography.bodyLarge]. * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field * container. * @param trailingIcon the optional trailing icon to be displayed at the end of the text field * container. * @param prefix the optional prefix to be displayed before the input text in the text field. * @param suffix the optional suffix to be displayed after the input text in the text field. * @param supportingText the optional supporting text to be displayed below the text field. * @param isError indicates if the text field's current value is in error. When `true`, the * components of the text field will be displayed in an error color, and an error will be * announced to accessibility services. * @param inputTransformation optional [InputTransformation] that will be used to transform changes * to the [TextFieldState] made by the user. The transformation will be applied to changes made by * hardware and software keyboard events, pasting or dropping text, accessibility services, and * tests. The transformation will _not_ be applied when changing the [state] programmatically, or * when the transformation is changed. If the transformation is changed on an existing text field, * it will be applied to the next user edit. The transformation will not immediately affect the * current [state]. * @param outputTransformation optional [OutputTransformation] that transforms how the contents of * the text field are presented. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction]. * @param onKeyboardAction called when the user presses the action button in the input method editor * (IME), or by pressing the enter key on a hardware keyboard. By default this parameter is null, * and would execute the default behavior for a received IME Action e.g., [ImeAction.Done] would * close the keyboard, [ImeAction.Next] would switch the focus to the next focusable item on the * screen. * @param lineLimits whether the text field should be [SingleLine], scroll horizontally, and ignore * newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline * characters ('\n') within the text will be replaced with regular whitespace (' '). * @param onTextLayout Callback that is executed when the text layout becomes queryable. The * callback receives a function that returns a [TextLayoutResult] if the layout can be calculated, * or null if it cannot. The function reads the layout result from a snapshot state object, and * will invalidate its caller when the layout result changes. A [TextLayoutResult] object contains * paragraph information, size of the text, baselines and other details. [Density] scope is the * one that was used while creating the given text layout. * @param scrollState scroll state that manages either horizontal or vertical scroll of the text * field. If [lineLimits] is [SingleLine], this text field is treated as single line with * horizontal scroll behavior. Otherwise, the text field becomes vertically scrollable. * @param shape defines the shape of this text field's container. * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [TextFieldDefaults.colors]. * @param contentPadding the padding applied to the inner text field that separates it from the * surrounding elements of the text field. Note that the padding values may not be respected if * they are incompatible with the text field's size constraints or layout. See * [TextFieldDefaults.contentPaddingWithLabel] and [TextFieldDefaults.contentPaddingWithoutLabel]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TextField( state: TextFieldState, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(), label: @Composable (TextFieldLabelScope.() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, suffix: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null, isError: Boolean = false, inputTransformation: InputTransformation? = null, outputTransformation: OutputTransformation? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, onKeyboardAction: KeyboardActionHandler? = null, lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null, scrollState: ScrollState = rememberScrollState(), shape: Shape = TextFieldDefaults.shape, colors: TextFieldColors = TextFieldDefaults.colors(), contentPadding: PaddingValues = if (label == null || labelPosition is TextFieldLabelPosition.Above) { TextFieldDefaults.contentPaddingWithoutLabel() } else { TextFieldDefaults.contentPaddingWithLabel() }, interactionSource: MutableInteractionSource? = null, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { val focused = interactionSource.collectIsFocusedAsState().value colors.textColor(enabled, isError, focused) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { BasicTextField( state = state, modifier = modifier .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight, ), enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError)), keyboardOptions = keyboardOptions, onKeyboardAction = onKeyboardAction, lineLimits = lineLimits, onTextLayout = onTextLayout, interactionSource = interactionSource, inputTransformation = inputTransformation, outputTransformation = outputTransformation, scrollState = scrollState, decorator = TextFieldDefaults.decorator( state = state, enabled = enabled, lineLimits = lineLimits, outputTransformation = outputTransformation, interactionSource = interactionSource, labelPosition = labelPosition, label = label, placeholder = placeholder, leadingIcon = leadingIcon, trailingIcon = trailingIcon, prefix = prefix, suffix = suffix, supportingText = supportingText, isError = isError, colors = colors, contentPadding = contentPadding, container = { TextFieldDefaults.Container( enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, shape = shape, ) }, ), ) } } /** * [Material Design filled text field](https://m3.material.io/components/text-fields/overview) * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Filled text fields have more visual emphasis than outlined text fields, making them stand out * when surrounded by other content and components. * * ![Filled text field * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-text-field.png) * * If you are looking for an outlined version, see [OutlinedTextField]. * * If apart from input text change you also want to observe the cursor location, selection range, or * IME composition use the TextField overload with the [TextFieldValue] parameter instead. * * @param value the input text to be shown in the text field * @param onValueChange the callback that is triggered when the input service updates the text. An * updated text comes as a parameter of the callback * @param modifier the [Modifier] to be applied to this text field * @param enabled controls the enabled state of this text field. When `false`, this component will * not respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param readOnly controls the editable state of the text field. When `true`, the text field cannot * be modified. However, a user can focus it and copy text from it. Read-only text fields are * usually used to display pre-filled forms that a user cannot edit. * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. * @param label the optional label to be displayed with this text field. The default text style uses * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded. * @param placeholder the optional placeholder to be displayed when the text field is in focus and * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge] * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field * container * @param trailingIcon the optional trailing icon to be displayed at the end of the text field * container * @param prefix the optional prefix to be displayed before the input text in the text field * @param suffix the optional suffix to be displayed after the input text in the text field * @param supportingText the optional supporting text to be displayed below the text field * @param isError indicates if the text field's current value is in error. If set to true, the * label, bottom indicator and trailing icon by default will be displayed in error color * @param visualTransformation transforms the visual representation of the input [value] For * example, you can use * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to * create a password text field. By default, no visual transformation is applied. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction]. * @param keyboardActions when the input service emits an IME action, the corresponding callback is * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction]. * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return * key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines * attribute will be automatically set to 1. * @param maxLines the maximum height in terms of maximum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. * @param shape defines the shape of this text field's container * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [TextFieldDefaults.colors]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, suffix: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, interactionSource: MutableInteractionSource? = null, shape: Shape = TextFieldDefaults.shape, colors: TextFieldColors = TextFieldDefaults.colors(), ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { val focused = interactionSource.collectIsFocusedAsState().value colors.textColor(enabled, isError, focused) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { BasicTextField( value = value, modifier = modifier .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight, ), onValueChange = onValueChange, enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError)), visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, maxLines = maxLines, minLines = minLines, decorationBox = @Composable { innerTextField -> // places leading icon, text field with label and placeholder, trailing icon TextFieldDefaults.DecorationBox( value = value, visualTransformation = visualTransformation, innerTextField = innerTextField, placeholder = placeholder, label = label, leadingIcon = leadingIcon, trailingIcon = trailingIcon, prefix = prefix, suffix = suffix, supportingText = supportingText, shape = shape, singleLine = singleLine, enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, ) }, ) } } /** * [Material Design filled text field](https://m3.material.io/components/text-fields/overview) * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Filled text fields have more visual emphasis than outlined text fields, making them stand out * when surrounded by other content and components. * * ![Filled text field * image](https://developer.android.com/images/reference/androidx/compose/material3/filled-text-field.png) * * If you are looking for an outlined version, see [OutlinedTextField]. * * This overload provides access to the input text, cursor position, selection range and IME * composition. If you only want to observe an input text change, use the TextField overload with * the [String] parameter instead. * * @param value the input [TextFieldValue] to be shown in the text field * @param onValueChange the callback that is triggered when the input service updates values in * [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback * @param modifier the [Modifier] to be applied to this text field * @param enabled controls the enabled state of this text field. When `false`, this component will * not respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param readOnly controls the editable state of the text field. When `true`, the text field cannot * be modified. However, a user can focus it and copy text from it. Read-only text fields are * usually used to display pre-filled forms that a user cannot edit. * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. * @param label the optional label to be displayed with this text field. The default text style uses * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded. * @param placeholder the optional placeholder to be displayed when the text field is in focus and * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge] * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field * container * @param trailingIcon the optional trailing icon to be displayed at the end of the text field * container * @param prefix the optional prefix to be displayed before the input text in the text field * @param suffix the optional suffix to be displayed after the input text in the text field * @param supportingText the optional supporting text to be displayed below the text field * @param isError indicates if the text field's current value is in error state. If set to true, the * label, bottom indicator and trailing icon by default will be displayed in error color * @param visualTransformation transforms the visual representation of the input [value]. For * example, you can use * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to * create a password text field. By default, no visual transformation is applied. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction]. * @param keyboardActions when the input service emits an IME action, the corresponding callback is * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction]. * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return * key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines * attribute will be automatically set to 1. * @param maxLines the maximum height in terms of maximum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. * @param shape defines the shape of this text field's container * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [TextFieldDefaults.colors]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, suffix: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, interactionSource: MutableInteractionSource? = null, shape: Shape = TextFieldDefaults.shape, colors: TextFieldColors = TextFieldDefaults.colors(), ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { val focused = interactionSource.collectIsFocusedAsState().value colors.textColor(enabled, isError, focused) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { BasicTextField( value = value, modifier = modifier .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = TextFieldDefaults.MinWidth, minHeight = TextFieldDefaults.MinHeight, ), onValueChange = onValueChange, enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError)), visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, maxLines = maxLines, minLines = minLines, decorationBox = @Composable { innerTextField -> // places leading icon, text field with label and placeholder, trailing icon TextFieldDefaults.DecorationBox( value = value.text, visualTransformation = visualTransformation, innerTextField = innerTextField, placeholder = placeholder, label = label, leadingIcon = leadingIcon, trailingIcon = trailingIcon, prefix = prefix, suffix = suffix, supportingText = supportingText, shape = shape, singleLine = singleLine, enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, ) }, ) } } /** * Composable responsible for measuring and laying out leading and trailing icons, label, * placeholder and the input field. */ @Composable internal fun TextFieldLayout( modifier: Modifier, textField: @Composable () -> Unit, label: @Composable (() -> Unit)?, placeholder: @Composable ((Modifier) -> Unit)?, leading: @Composable (() -> Unit)?, trailing: @Composable (() -> Unit)?, prefix: @Composable (() -> Unit)?, suffix: @Composable (() -> Unit)?, singleLine: Boolean, labelPosition: TextFieldLabelPosition, labelProgress: FloatProducer, container: @Composable () -> Unit, supporting: @Composable (() -> Unit)?, paddingValues: PaddingValues, ) { val minimizedLabelHalfHeight = minimizedLabelHalfHeight() val measurePolicy = remember( singleLine, labelPosition, labelProgress, paddingValues, minimizedLabelHalfHeight, ) { TextFieldMeasurePolicy( singleLine = singleLine, labelPosition = labelPosition, labelProgress = labelProgress, paddingValues = paddingValues, minimizedLabelHalfHeight = minimizedLabelHalfHeight, ) } val layoutDirection = LocalLayoutDirection.current Layout( modifier = modifier, content = { // The container is given as a Composable instead of a background modifier so that // elements like supporting text can be placed outside of it while still contributing // to the text field's measurements overall. container() if (leading != null) { Box( modifier = Modifier.layoutId(LeadingId).minimumInteractiveComponentSize(), contentAlignment = Alignment.Center, ) { leading() } } if (trailing != null) { Box( modifier = Modifier.layoutId(TrailingId).minimumInteractiveComponentSize(), contentAlignment = Alignment.Center, ) { trailing() } } val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection) val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection) val horizontalIconPadding = textFieldHorizontalIconPadding() val startPadding = if (leading != null) { (startTextFieldPadding - horizontalIconPadding).coerceAtLeast(0.dp) } else { startTextFieldPadding } val endPadding = if (trailing != null) { (endTextFieldPadding - horizontalIconPadding).coerceAtLeast(0.dp) } else { endTextFieldPadding } if (prefix != null) { Box( Modifier.layoutId(PrefixId) .heightIn(min = MinTextLineHeight) .wrapContentHeight() .padding(start = startPadding, end = PrefixSuffixTextPadding) ) { prefix() } } if (suffix != null) { Box( Modifier.layoutId(SuffixId) .heightIn(min = MinTextLineHeight) .wrapContentHeight() .padding(start = PrefixSuffixTextPadding, end = endPadding) ) { suffix() } } val labelPadding = if (labelPosition is TextFieldLabelPosition.Above) { Modifier.padding( start = AboveLabelHorizontalPadding, end = AboveLabelHorizontalPadding, bottom = AboveLabelBottomPadding, ) } else { Modifier.padding(start = startPadding, end = endPadding) } if (label != null) { Box( Modifier.layoutId(LabelId) .textFieldLabelMinHeight { lerp(MinTextLineHeight, MinFocusedLabelLineHeight, labelProgress()) } .wrapContentHeight() .then(labelPadding) ) { label() } } val textPadding = Modifier.heightIn(min = MinTextLineHeight) .wrapContentHeight() .padding( start = if (prefix == null) startPadding else 0.dp, end = if (suffix == null) endPadding else 0.dp, ) if (placeholder != null) { placeholder(Modifier.layoutId(PlaceholderId).then(textPadding)) } Box( modifier = Modifier.layoutId(TextFieldId).then(textPadding), propagateMinConstraints = true, ) { textField() } if (supporting != null) { @OptIn(ExperimentalMaterial3Api::class) Box( Modifier.layoutId(SupportingId) .heightIn(min = MinSupportingTextLineHeight) .wrapContentHeight() .padding(TextFieldDefaults.supportingTextPadding()) ) { supporting() } } }, measurePolicy = measurePolicy, ) } private class TextFieldMeasurePolicy( private val singleLine: Boolean, private val labelPosition: TextFieldLabelPosition, private val labelProgress: FloatProducer, private val paddingValues: PaddingValues, private val minimizedLabelHalfHeight: Dp, ) : MeasurePolicy { override fun MeasureScope.measure( measurables: List, constraints: Constraints, ): MeasureResult { val labelProgress = labelProgress() val topPaddingValue = paddingValues.calculateTopPadding().roundToPx() val bottomPaddingValue = paddingValues.calculateBottomPadding().roundToPx() var occupiedSpaceHorizontally = 0 var occupiedSpaceVertically = 0 val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) // measure leading icon val leadingPlaceable = measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(looseConstraints) occupiedSpaceHorizontally += leadingPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, leadingPlaceable.heightOrZero) // measure trailing icon val trailingPlaceable = measurables .fastFirstOrNull { it.layoutId == TrailingId } ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += trailingPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, trailingPlaceable.heightOrZero) // measure prefix val prefixPlaceable = measurables .fastFirstOrNull { it.layoutId == PrefixId } ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += prefixPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, prefixPlaceable.heightOrZero) // measure suffix val suffixPlaceable = measurables .fastFirstOrNull { it.layoutId == SuffixId } ?.measure(looseConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += suffixPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, suffixPlaceable.heightOrZero) val isLabelAbove = labelPosition is TextFieldLabelPosition.Above val labelMeasurable = measurables.fastFirstOrNull { it.layoutId == LabelId } var labelPlaceable: Placeable? = null val labelIntrinsicHeight: Int if (!isLabelAbove) { // if label is not Above, we can measure it like normal val labelConstraints = looseConstraints.offset( vertical = -bottomPaddingValue, horizontal = -occupiedSpaceHorizontally, ) labelPlaceable = labelMeasurable?.measure(labelConstraints) labelIntrinsicHeight = 0 } else { // if label is Above, it must be measured after other elements, but we // reserve space for it using its intrinsic height as a heuristic labelIntrinsicHeight = labelMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0 } // supporting text must be measured after other elements, but we // reserve space for it using its intrinsic height as a heuristic val supportingMeasurable = measurables.fastFirstOrNull { it.layoutId == SupportingId } val supportingIntrinsicHeight = supportingMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0 // at most one of these is non-zero val labelHeightOrIntrinsic = labelPlaceable.heightOrZero + labelIntrinsicHeight // measure input field val effectiveTopOffset = topPaddingValue + labelHeightOrIntrinsic val textFieldConstraints = constraints .copy(minHeight = 0) .offset( vertical = -effectiveTopOffset - bottomPaddingValue - supportingIntrinsicHeight, horizontal = -occupiedSpaceHorizontally, ) val textFieldPlaceable = measurables.fastFirst { it.layoutId == TextFieldId }.measure(textFieldConstraints) // measure placeholder val placeholderConstraints = textFieldConstraints.copy(minWidth = 0) val placeholderPlaceable = measurables .fastFirstOrNull { it.layoutId == PlaceholderId } ?.measure(placeholderConstraints) occupiedSpaceVertically = max( occupiedSpaceVertically, max(textFieldPlaceable.heightOrZero, placeholderPlaceable.heightOrZero) + effectiveTopOffset + bottomPaddingValue, ) val width = calculateWidth( leadingWidth = leadingPlaceable.widthOrZero, trailingWidth = trailingPlaceable.widthOrZero, prefixWidth = prefixPlaceable.widthOrZero, suffixWidth = suffixPlaceable.widthOrZero, textFieldWidth = textFieldPlaceable.width, labelWidth = labelPlaceable.widthOrZero, placeholderWidth = placeholderPlaceable.widthOrZero, constraints = constraints, ) if (isLabelAbove) { // now that we know the width, measure label val labelConstraints = looseConstraints.copy(maxHeight = labelIntrinsicHeight, maxWidth = width) labelPlaceable = labelMeasurable?.measure(labelConstraints) } // measure supporting text val supportingConstraints = looseConstraints .offset(vertical = -occupiedSpaceVertically) .copy(minHeight = 0, maxWidth = width) val supportingPlaceable = supportingMeasurable?.measure(supportingConstraints) val supportingHeight = supportingPlaceable.heightOrZero val totalHeight = calculateHeight( textFieldHeight = textFieldPlaceable.height, labelHeight = labelPlaceable.heightOrZero, leadingHeight = leadingPlaceable.heightOrZero, trailingHeight = trailingPlaceable.heightOrZero, prefixHeight = prefixPlaceable.heightOrZero, suffixHeight = suffixPlaceable.heightOrZero, placeholderHeight = placeholderPlaceable.heightOrZero, supportingHeight = supportingPlaceable.heightOrZero, constraints = constraints, isLabelAbove = isLabelAbove, labelProgress = labelProgress, ) val height = totalHeight - supportingHeight - (if (isLabelAbove) labelPlaceable.heightOrZero else 0) val containerPlaceable = measurables .fastFirst { it.layoutId == ContainerId } .measure( Constraints( minWidth = if (width != Constraints.Infinity) width else 0, maxWidth = width, minHeight = if (height != Constraints.Infinity) height else 0, maxHeight = height, ) ) return layout(width, totalHeight) { if (labelPlaceable != null) { val labelStartY = when { isLabelAbove -> 0 singleLine -> Alignment.CenterVertically.align(labelPlaceable.height, height) else -> // The padding defined by the user only applies to the text field when // the label is focused. More padding needs to be added when the text // field is unfocused. topPaddingValue + minimizedLabelHalfHeight.roundToPx() } val labelEndY = when { isLabelAbove -> 0 else -> topPaddingValue } placeWithLabel( width = width, totalHeight = totalHeight, textfieldPlaceable = textFieldPlaceable, labelPlaceable = labelPlaceable, placeholderPlaceable = placeholderPlaceable, leadingPlaceable = leadingPlaceable, trailingPlaceable = trailingPlaceable, prefixPlaceable = prefixPlaceable, suffixPlaceable = suffixPlaceable, containerPlaceable = containerPlaceable, supportingPlaceable = supportingPlaceable, labelStartY = labelStartY, labelEndY = labelEndY, isLabelAbove = isLabelAbove, labelProgress = labelProgress, textPosition = topPaddingValue + (if (isLabelAbove) 0 else labelPlaceable.height), layoutDirection = layoutDirection, ) } else { placeWithoutLabel( width = width, totalHeight = totalHeight, textPlaceable = textFieldPlaceable, placeholderPlaceable = placeholderPlaceable, leadingPlaceable = leadingPlaceable, trailingPlaceable = trailingPlaceable, prefixPlaceable = prefixPlaceable, suffixPlaceable = suffixPlaceable, containerPlaceable = containerPlaceable, supportingPlaceable = supportingPlaceable, density = density, ) } } } override fun IntrinsicMeasureScope.maxIntrinsicHeight( measurables: List, width: Int, ): Int { return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> intrinsicMeasurable.maxIntrinsicHeight(w) } } override fun IntrinsicMeasureScope.minIntrinsicHeight( measurables: List, width: Int, ): Int { return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> intrinsicMeasurable.minIntrinsicHeight(w) } } override fun IntrinsicMeasureScope.maxIntrinsicWidth( measurables: List, height: Int, ): Int { return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> intrinsicMeasurable.maxIntrinsicWidth(h) } } override fun IntrinsicMeasureScope.minIntrinsicWidth( measurables: List, height: Int, ): Int { return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> intrinsicMeasurable.minIntrinsicWidth(h) } } private fun intrinsicWidth( measurables: List, height: Int, intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int, ): Int { val textFieldWidth = intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height) val labelWidth = measurables .fastFirstOrNull { it.layoutId == LabelId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val trailingWidth = measurables .fastFirstOrNull { it.layoutId == TrailingId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val prefixWidth = measurables .fastFirstOrNull { it.layoutId == PrefixId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val suffixWidth = measurables .fastFirstOrNull { it.layoutId == SuffixId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val leadingWidth = measurables .fastFirstOrNull { it.layoutId == LeadingId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val placeholderWidth = measurables .fastFirstOrNull { it.layoutId == PlaceholderId } ?.let { intrinsicMeasurer(it, height) } ?: 0 return calculateWidth( leadingWidth = leadingWidth, trailingWidth = trailingWidth, prefixWidth = prefixWidth, suffixWidth = suffixWidth, textFieldWidth = textFieldWidth, labelWidth = labelWidth, placeholderWidth = placeholderWidth, constraints = Constraints(), ) } private fun IntrinsicMeasureScope.intrinsicHeight( measurables: List, width: Int, intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int, ): Int { var remainingWidth = width val leadingHeight = measurables .fastFirstOrNull { it.layoutId == LeadingId } ?.let { remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) intrinsicMeasurer(it, width) } ?: 0 val trailingHeight = measurables .fastFirstOrNull { it.layoutId == TrailingId } ?.let { remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) intrinsicMeasurer(it, width) } ?: 0 val labelHeight = measurables .fastFirstOrNull { it.layoutId == LabelId } ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0 val prefixHeight = measurables .fastFirstOrNull { it.layoutId == PrefixId } ?.let { val height = intrinsicMeasurer(it, remainingWidth) remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val suffixHeight = measurables .fastFirstOrNull { it.layoutId == SuffixId } ?.let { val height = intrinsicMeasurer(it, remainingWidth) remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val textFieldHeight = intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth) val placeholderHeight = measurables .fastFirstOrNull { it.layoutId == PlaceholderId } ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0 val supportingHeight = measurables .fastFirstOrNull { it.layoutId == SupportingId } ?.let { intrinsicMeasurer(it, width) } ?: 0 return calculateHeight( textFieldHeight = textFieldHeight, labelHeight = labelHeight, leadingHeight = leadingHeight, trailingHeight = trailingHeight, prefixHeight = prefixHeight, suffixHeight = suffixHeight, placeholderHeight = placeholderHeight, supportingHeight = supportingHeight, constraints = Constraints(), isLabelAbove = labelPosition is TextFieldLabelPosition.Above, labelProgress = labelProgress(), ) } private fun calculateWidth( leadingWidth: Int, trailingWidth: Int, prefixWidth: Int, suffixWidth: Int, textFieldWidth: Int, labelWidth: Int, placeholderWidth: Int, constraints: Constraints, ): Int { val affixTotalWidth = prefixWidth + suffixWidth val middleSection = maxOf( textFieldWidth + affixTotalWidth, placeholderWidth + affixTotalWidth, // Prefix/suffix does not get applied to label labelWidth, ) val wrappedWidth = leadingWidth + middleSection + trailingWidth return constraints.constrainWidth(wrappedWidth) } private fun Density.calculateHeight( textFieldHeight: Int, labelHeight: Int, leadingHeight: Int, trailingHeight: Int, prefixHeight: Int, suffixHeight: Int, placeholderHeight: Int, supportingHeight: Int, constraints: Constraints, isLabelAbove: Boolean, labelProgress: Float, ): Int { val verticalPadding = (paddingValues.calculateTopPadding() + paddingValues.calculateBottomPadding()) .roundToPx() val inputFieldHeight = maxOf( textFieldHeight, placeholderHeight, prefixHeight, suffixHeight, if (isLabelAbove) 0 else lerp(labelHeight, 0, labelProgress), ) val hasLabel = labelHeight > 0 val nonOverlappedLabelHeight = if (hasLabel && !isLabelAbove) { // The label animates from overlapping the input field to floating above it, // so its contribution to the height calculation changes over time. A baseline // height is provided in the unfocused state to keep the overall height consistent // across the animation. max( (minimizedLabelHalfHeight * 2).roundToPx(), lerp( 0, labelHeight, EasingEmphasizedAccelerateCubicBezier.transform(labelProgress), ), ) } else { 0 } val middleSectionHeight = verticalPadding + nonOverlappedLabelHeight + inputFieldHeight return constraints.constrainHeight( (if (isLabelAbove) labelHeight else 0) + maxOf(leadingHeight, trailingHeight, middleSectionHeight) + supportingHeight ) } /** * Places the provided text field, placeholder, and label in the TextField given the * PaddingValues when there is a label. When there is no label, [placeWithoutLabel] is used * instead. */ private fun Placeable.PlacementScope.placeWithLabel( width: Int, totalHeight: Int, textfieldPlaceable: Placeable, labelPlaceable: Placeable, placeholderPlaceable: Placeable?, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, prefixPlaceable: Placeable?, suffixPlaceable: Placeable?, containerPlaceable: Placeable, supportingPlaceable: Placeable?, labelStartY: Int, labelEndY: Int, isLabelAbove: Boolean, labelProgress: Float, textPosition: Int, layoutDirection: LayoutDirection, ) { val yOffset = if (isLabelAbove) labelPlaceable.height else 0 // place container containerPlaceable.place(0, yOffset) // Most elements should be positioned w.r.t the text field's "visual" height, i.e., // excluding the label (if it's Above) and the supporting text on bottom val height = totalHeight - supportingPlaceable.heightOrZero - (if (isLabelAbove) labelPlaceable.height else 0) leadingPlaceable?.placeRelative( 0, yOffset + Alignment.CenterVertically.align(leadingPlaceable.height, height), ) val labelY = lerp(labelStartY, labelEndY, labelProgress) if (isLabelAbove) { val labelX = labelPosition.minimizedAlignment.align( size = labelPlaceable.width, space = width, layoutDirection = layoutDirection, ) // Not placeRelative because alignment already handles RTL labelPlaceable.place(labelX, labelY) } else { val leftIconWidth = if (layoutDirection == LayoutDirection.Ltr) leadingPlaceable.widthOrZero else trailingPlaceable.widthOrZero val labelStartX = labelPosition.expandedAlignment.align( size = labelPlaceable.width, space = width - leadingPlaceable.widthOrZero - trailingPlaceable.widthOrZero, layoutDirection = layoutDirection, ) + leftIconWidth val labelEndX = labelPosition.minimizedAlignment.align( size = labelPlaceable.width, space = width - leadingPlaceable.widthOrZero - trailingPlaceable.widthOrZero, layoutDirection = layoutDirection, ) + leftIconWidth val labelX = lerp(labelStartX, labelEndX, labelProgress) // Not placeRelative because alignment already handles RTL labelPlaceable.place(labelX, labelY) } prefixPlaceable?.placeRelative(leadingPlaceable.widthOrZero, yOffset + textPosition) val textHorizontalPosition = leadingPlaceable.widthOrZero + prefixPlaceable.widthOrZero textfieldPlaceable.placeRelative(textHorizontalPosition, yOffset + textPosition) placeholderPlaceable?.placeRelative(textHorizontalPosition, yOffset + textPosition) suffixPlaceable?.placeRelative( width - trailingPlaceable.widthOrZero - suffixPlaceable.width, yOffset + textPosition, ) trailingPlaceable?.placeRelative( width - trailingPlaceable.width, yOffset + Alignment.CenterVertically.align(trailingPlaceable.height, height), ) supportingPlaceable?.placeRelative(0, yOffset + height) } /** * Places the provided text field and placeholder in [TextField] when there is no label. When * there is a label, [placeWithLabel] is used */ private fun Placeable.PlacementScope.placeWithoutLabel( width: Int, totalHeight: Int, textPlaceable: Placeable, placeholderPlaceable: Placeable?, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, prefixPlaceable: Placeable?, suffixPlaceable: Placeable?, containerPlaceable: Placeable, supportingPlaceable: Placeable?, density: Float, ) { // place container containerPlaceable.place(IntOffset.Zero) // Most elements should be positioned w.r.t the text field's "visual" height, i.e., // excluding the supporting text on bottom val height = totalHeight - supportingPlaceable.heightOrZero val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt() leadingPlaceable?.placeRelative( 0, Alignment.CenterVertically.align(leadingPlaceable.height, height), ) // Single line text field without label places its text components centered vertically. // Multiline text field without label places its text components at the top with padding. fun calculateVerticalPosition(placeable: Placeable): Int { return if (singleLine) { Alignment.CenterVertically.align(placeable.height, height) } else { topPadding } } prefixPlaceable?.placeRelative( leadingPlaceable.widthOrZero, calculateVerticalPosition(prefixPlaceable), ) val textHorizontalPosition = leadingPlaceable.widthOrZero + prefixPlaceable.widthOrZero textPlaceable.placeRelative( textHorizontalPosition, calculateVerticalPosition(textPlaceable), ) placeholderPlaceable?.placeRelative( textHorizontalPosition, calculateVerticalPosition(placeholderPlaceable), ) suffixPlaceable?.placeRelative( width - trailingPlaceable.widthOrZero - suffixPlaceable.width, calculateVerticalPosition(suffixPlaceable), ) trailingPlaceable?.placeRelative( width - trailingPlaceable.width, Alignment.CenterVertically.align(trailingPlaceable.height, height), ) supportingPlaceable?.placeRelative(0, height) } } internal data class IndicatorLineElement( val enabled: Boolean, val isError: Boolean, val interactionSource: InteractionSource, val colors: TextFieldColors?, val textFieldShape: Shape?, val focusedIndicatorLineThickness: Dp, val unfocusedIndicatorLineThickness: Dp, ) : ModifierNodeElement() { override fun create(): IndicatorLineNode { return IndicatorLineNode( enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, textFieldShape = textFieldShape, focusedIndicatorWidth = focusedIndicatorLineThickness, unfocusedIndicatorWidth = unfocusedIndicatorLineThickness, ) } override fun update(node: IndicatorLineNode) { node.update( enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, textFieldShape = textFieldShape, focusedIndicatorWidth = focusedIndicatorLineThickness, unfocusedIndicatorWidth = unfocusedIndicatorLineThickness, ) } override fun InspectorInfo.inspectableProperties() { name = "indicatorLine" properties["enabled"] = enabled properties["isError"] = isError properties["interactionSource"] = interactionSource properties["colors"] = colors properties["textFieldShape"] = textFieldShape properties["focusedIndicatorLineThickness"] = focusedIndicatorLineThickness properties["unfocusedIndicatorLineThickness"] = unfocusedIndicatorLineThickness } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) internal class IndicatorLineNode( private var enabled: Boolean, private var isError: Boolean, private var interactionSource: InteractionSource, colors: TextFieldColors?, textFieldShape: Shape?, private var focusedIndicatorWidth: Dp, private var unfocusedIndicatorWidth: Dp, ) : DelegatingNode(), CompositionLocalConsumerModifierNode { private var focused = false private var trackFocusStateJob: Job? = null private var _colors: TextFieldColors? = colors private val colors: TextFieldColors get() = _colors ?: currentValueOf(LocalMaterialTheme) .colorScheme .defaultTextFieldColors(currentValueOf(LocalTextSelectionColors)) // Must be initialized in `onAttach` so `colors` can read from the `MaterialTheme` private var colorAnimatable: Animatable? = null private var _shape: Shape? = textFieldShape private set(value) { if (field != value) { field = value drawWithCacheModifierNode.invalidateDrawCache() } } private val shape: Shape get() = _shape ?: currentValueOf(LocalMaterialTheme) .shapes .fromToken(FilledTextFieldTokens.ContainerShape) private val widthAnimatable: Animatable = Animatable( initialValue = if (focused && this.enabled) this.focusedIndicatorWidth else this.unfocusedIndicatorWidth, typeConverter = Dp.VectorConverter, ) fun update( enabled: Boolean, isError: Boolean, interactionSource: InteractionSource, colors: TextFieldColors?, textFieldShape: Shape?, focusedIndicatorWidth: Dp, unfocusedIndicatorWidth: Dp, ) { var shouldInvalidate = false if (this.enabled != enabled) { this.enabled = enabled shouldInvalidate = true } if (this.isError != isError) { this.isError = isError shouldInvalidate = true } if (this.interactionSource !== interactionSource) { this.interactionSource = interactionSource trackFocusStateJob?.cancel() trackFocusStateJob = coroutineScope.launch { trackFocusState() } } if (this._colors != colors) { this._colors = colors shouldInvalidate = true } if (this._shape != textFieldShape) { this._shape = textFieldShape shouldInvalidate = true } if (this.focusedIndicatorWidth != focusedIndicatorWidth) { this.focusedIndicatorWidth = focusedIndicatorWidth shouldInvalidate = true } if (this.unfocusedIndicatorWidth != unfocusedIndicatorWidth) { this.unfocusedIndicatorWidth = unfocusedIndicatorWidth shouldInvalidate = true } if (shouldInvalidate) { invalidateIndicator() } } override val shouldAutoInvalidate: Boolean get() = false override fun onAttach() { trackFocusStateJob = coroutineScope.launch { trackFocusState() } if (colorAnimatable == null) { val initialColor = colors.indicatorColor(enabled, isError, focused) colorAnimatable = Animatable( initialValue = initialColor, typeConverter = Color.VectorConverter(initialColor.colorSpace), ) } } /** Copied from [InteractionSource.collectIsFocusedAsState] */ private suspend fun trackFocusState() { focused = false val focusInteractions = mutableListOf() interactionSource.interactions.collect { interaction -> when (interaction) { is FocusInteraction.Focus -> focusInteractions.add(interaction) is FocusInteraction.Unfocus -> focusInteractions.remove(interaction.focus) } val isFocused = focusInteractions.isNotEmpty() if (isFocused != focused) { focused = isFocused invalidateIndicator() } } } private fun invalidateIndicator() { coroutineScope.launch { colorAnimatable?.animateTo( targetValue = colors.indicatorColor(enabled, isError, focused), animationSpec = if (enabled) { currentValueOf(LocalMaterialTheme) .motionScheme .fromToken(MotionSchemeKeyTokens.FastEffects) } else { snap() }, ) } coroutineScope.launch { widthAnimatable.animateTo( targetValue = if (focused && enabled) focusedIndicatorWidth else unfocusedIndicatorWidth, animationSpec = if (enabled) { currentValueOf(LocalMaterialTheme) .motionScheme .fromToken(MotionSchemeKeyTokens.FastSpatial) } else { snap() }, ) } } private val drawWithCacheModifierNode = delegate( CacheDrawModifierNode { val strokeWidth = widthAnimatable.value.toPx() val textFieldShapePath = Path().apply { addOutline( this@IndicatorLineNode.shape.createOutline( size, layoutDirection, density = this@CacheDrawModifierNode, ) ) } val linePath = Path().apply { addRect( Rect( left = 0f, top = size.height - strokeWidth, right = size.width, bottom = size.height, ) ) } val clippedLine = linePath and textFieldShapePath onDrawWithContent { drawContent() drawPath(path = clippedLine, brush = SolidColor(colorAnimatable!!.value)) } } ) } /** Padding from text field top to label top, and from input field bottom to text field bottom */ /*@VisibleForTesting*/ internal val TextFieldWithLabelVerticalPadding = 8.dp ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/OutlinedTextField.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.foundation.ScrollState import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.input.InputTransformation import androidx.compose.foundation.text.input.KeyboardActionHandler import androidx.compose.foundation.text.input.OutputTransformation import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldLineLimits.MultiLine import androidx.compose.foundation.text.input.TextFieldLineLimits.SingleLine import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.material3.internal.AboveLabelBottomPadding import androidx.compose.material3.internal.AboveLabelHorizontalPadding import androidx.compose.material3.internal.ContainerId import androidx.compose.material3.internal.FloatProducer import androidx.compose.material3.internal.LabelId import androidx.compose.material3.internal.LeadingId import androidx.compose.material3.internal.MinFocusedLabelLineHeight import androidx.compose.material3.internal.MinSupportingTextLineHeight import androidx.compose.material3.internal.MinTextLineHeight import androidx.compose.material3.internal.PlaceholderId import androidx.compose.material3.internal.PrefixId import androidx.compose.material3.internal.PrefixSuffixTextPadding import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.SuffixId import androidx.compose.material3.internal.SupportingId import androidx.compose.material3.internal.TextFieldId import androidx.compose.material3.internal.TrailingId import androidx.compose.material3.internal.defaultErrorSemantics import androidx.compose.material3.internal.expandedAlignment import androidx.compose.material3.internal.getString import androidx.compose.material3.internal.heightOrZero import androidx.compose.material3.internal.layoutId import androidx.compose.material3.internal.minimizedAlignment import androidx.compose.material3.internal.minimizedLabelHalfHeight import androidx.compose.material3.internal.subtractConstraintSafely import androidx.compose.material3.internal.textFieldHorizontalIconPadding import androidx.compose.material3.internal.textFieldLabelMinHeight import androidx.compose.material3.internal.widthOrZero import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.ClipOp import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.constrainHeight import androidx.compose.ui.unit.constrainWidth import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.offset import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import androidx.compose.ui.util.lerp import kotlin.math.max import kotlin.math.roundToInt /** * [Material Design outlined text field](https://m3.material.io/components/text-fields/overview) * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Outlined text fields have less visual emphasis than filled text fields. When they appear in * places like forms, where many text fields are placed together, their reduced emphasis helps * simplify the layout. * * ![Outlined text field * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-text-field.png) * * If you are looking for a filled version, see [TextField]. For a text field specifically designed * for passwords or other secure content, see [OutlinedSecureTextField]. * * This overload of [OutlinedTextField] uses [TextFieldState] to keep track of its text content and * position of the cursor or selection. * * See example usage: * * @sample androidx.compose.material3.samples.SimpleOutlinedTextFieldSample * @sample androidx.compose.material3.samples.OutlinedTextFieldWithInitialValueAndSelection * @param state [TextFieldState] object that holds the internal editing state of the text field. * @param modifier the [Modifier] to be applied to this text field. * @param enabled controls the enabled state of this text field. When `false`, this component will * not respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param readOnly controls the editable state of the text field. When `true`, the text field cannot * be modified. However, a user can focus it and copy text from it. Read-only text fields are * usually used to display pre-filled forms that a user cannot edit. * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. * @param labelPosition the position of the label. See [TextFieldLabelPosition]. * @param label the optional label to be displayed with this text field. The default text style uses * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded. * @param placeholder the optional placeholder to be displayed when the input text is empty. The * default text style uses [Typography.bodyLarge]. * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field * container. * @param trailingIcon the optional trailing icon to be displayed at the end of the text field * container. * @param prefix the optional prefix to be displayed before the input text in the text field. * @param suffix the optional suffix to be displayed after the input text in the text field. * @param supportingText the optional supporting text to be displayed below the text field. * @param isError indicates if the text field's current value is in error. When `true`, the * components of the text field will be displayed in an error color, and an error will be * announced to accessibility services. * @param inputTransformation optional [InputTransformation] that will be used to transform changes * to the [TextFieldState] made by the user. The transformation will be applied to changes made by * hardware and software keyboard events, pasting or dropping text, accessibility services, and * tests. The transformation will _not_ be applied when changing the [state] programmatically, or * when the transformation is changed. If the transformation is changed on an existing text field, * it will be applied to the next user edit. The transformation will not immediately affect the * current [state]. * @param outputTransformation optional [OutputTransformation] that transforms how the contents of * the text field are presented. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction]. * @param onKeyboardAction called when the user presses the action button in the input method editor * (IME), or by pressing the enter key on a hardware keyboard. By default this parameter is null, * and would execute the default behavior for a received IME Action e.g., [ImeAction.Done] would * close the keyboard, [ImeAction.Next] would switch the focus to the next focusable item on the * screen. * @param lineLimits whether the text field should be [SingleLine], scroll horizontally, and ignore * newlines; or [MultiLine] and grow and scroll vertically. If [SingleLine] is passed, all newline * characters ('\n') within the text will be replaced with regular whitespace (' '). * @param onTextLayout Callback that is executed when the text layout becomes queryable. The * callback receives a function that returns a [TextLayoutResult] if the layout can be calculated, * or null if it cannot. The function reads the layout result from a snapshot state object, and * will invalidate its caller when the layout result changes. A [TextLayoutResult] object contains * paragraph information, size of the text, baselines and other details. [Density] scope is the * one that was used while creating the given text layout. * @param scrollState scroll state that manages either horizontal or vertical scroll of the text * field. If [lineLimits] is [SingleLine], this text field is treated as single line with * horizontal scroll behavior. Otherwise, the text field becomes vertically scrollable. * @param shape defines the shape of this text field's border. * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [OutlinedTextFieldDefaults.colors]. * @param contentPadding the padding applied to the inner text field that separates it from the * surrounding elements of the text field. Note that the padding values may not be respected if * they are incompatible with the text field's size constraints or layout. See * [OutlinedTextFieldDefaults.contentPadding]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun OutlinedTextField( state: TextFieldState, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, labelPosition: TextFieldLabelPosition = TextFieldLabelPosition.Attached(), label: @Composable (TextFieldLabelScope.() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, suffix: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null, isError: Boolean = false, inputTransformation: InputTransformation? = null, outputTransformation: OutputTransformation? = null, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, onKeyboardAction: KeyboardActionHandler? = null, lineLimits: TextFieldLineLimits = TextFieldLineLimits.Default, onTextLayout: (Density.(getResult: () -> TextLayoutResult?) -> Unit)? = null, scrollState: ScrollState = rememberScrollState(), shape: Shape = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors(), contentPadding: PaddingValues = OutlinedTextFieldDefaults.contentPadding(), interactionSource: MutableInteractionSource? = null, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { val focused = interactionSource.collectIsFocusedAsState().value colors.textColor(enabled, isError, focused) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { BasicTextField( state = state, modifier = modifier .then( if (label != null && labelPosition !is TextFieldLabelPosition.Above) { Modifier // Merge semantics at the beginning of the modifier chain to ensure // padding is considered part of the text field. .semantics(mergeDescendants = true) {} .padding(top = minimizedLabelHalfHeight()) } else { Modifier } ) .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = OutlinedTextFieldDefaults.MinWidth, minHeight = OutlinedTextFieldDefaults.MinHeight, ), enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError)), keyboardOptions = keyboardOptions, onKeyboardAction = onKeyboardAction, lineLimits = lineLimits, onTextLayout = onTextLayout, interactionSource = interactionSource, inputTransformation = inputTransformation, outputTransformation = outputTransformation, scrollState = scrollState, decorator = OutlinedTextFieldDefaults.decorator( state = state, enabled = enabled, lineLimits = lineLimits, outputTransformation = outputTransformation, interactionSource = interactionSource, labelPosition = labelPosition, label = label, placeholder = placeholder, leadingIcon = leadingIcon, trailingIcon = trailingIcon, prefix = prefix, suffix = suffix, supportingText = supportingText, isError = isError, colors = colors, contentPadding = contentPadding, container = { OutlinedTextFieldDefaults.Container( enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, shape = shape, ) }, ), ) } } /** * [Material Design outlined text field](https://m3.material.io/components/text-fields/overview) * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Outlined text fields have less visual emphasis than filled text fields. When they appear in * places like forms, where many text fields are placed together, their reduced emphasis helps * simplify the layout. * * ![Outlined text field * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-text-field.png) * * If apart from input text change you also want to observe the cursor location, selection range, or * IME composition use the OutlinedTextField overload with the [TextFieldValue] parameter instead. * * @param value the input text to be shown in the text field * @param onValueChange the callback that is triggered when the input service updates the text. An * updated text comes as a parameter of the callback * @param modifier the [Modifier] to be applied to this text field * @param enabled controls the enabled state of this text field. When `false`, this component will * not respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param readOnly controls the editable state of the text field. When `true`, the text field cannot * be modified. However, a user can focus it and copy text from it. Read-only text fields are * usually used to display pre-filled forms that a user cannot edit. * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. * @param label the optional label to be displayed with this text field. The default text style uses * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded. * @param placeholder the optional placeholder to be displayed when the text field is in focus and * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge] * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field * container * @param trailingIcon the optional trailing icon to be displayed at the end of the text field * container * @param prefix the optional prefix to be displayed before the input text in the text field * @param suffix the optional suffix to be displayed after the input text in the text field * @param supportingText the optional supporting text to be displayed below the text field * @param isError indicates if the text field's current value is in error. If set to true, the * label, bottom indicator and trailing icon by default will be displayed in error color * @param visualTransformation transforms the visual representation of the input [value] For * example, you can use * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to * create a password text field. By default, no visual transformation is applied. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction] * @param keyboardActions when the input service emits an IME action, the corresponding callback is * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction] * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return * key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines * attribute will be automatically set to 1. * @param maxLines the maximum height in terms of maximum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. * @param shape defines the shape of this text field's border * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [OutlinedTextFieldDefaults.colors]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun OutlinedTextField( value: String, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, suffix: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, interactionSource: MutableInteractionSource? = null, shape: Shape = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors(), ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { val focused = interactionSource.collectIsFocusedAsState().value colors.textColor(enabled, isError, focused) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { BasicTextField( value = value, modifier = modifier .then( if (label != null) { Modifier // Merge semantics at the beginning of the modifier chain to ensure // padding is considered part of the text field. .semantics(mergeDescendants = true) {} .padding(top = minimizedLabelHalfHeight()) } else { Modifier } ) .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = OutlinedTextFieldDefaults.MinWidth, minHeight = OutlinedTextFieldDefaults.MinHeight, ), onValueChange = onValueChange, enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError)), visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, maxLines = maxLines, minLines = minLines, decorationBox = @Composable { innerTextField -> OutlinedTextFieldDefaults.DecorationBox( value = value, visualTransformation = visualTransformation, innerTextField = innerTextField, placeholder = placeholder, label = label, leadingIcon = leadingIcon, trailingIcon = trailingIcon, prefix = prefix, suffix = suffix, supportingText = supportingText, singleLine = singleLine, enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, container = { OutlinedTextFieldDefaults.Container( enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, shape = shape, ) }, ) }, ) } } /** * [Material Design outlined text field](https://m3.material.io/components/text-fields/overview) * * Text fields allow users to enter text into a UI. They typically appear in forms and dialogs. * Outlined text fields have less visual emphasis than filled text fields. When they appear in * places like forms, where many text fields are placed together, their reduced emphasis helps * simplify the layout. * * ![Outlined text field * image](https://developer.android.com/images/reference/androidx/compose/material3/outlined-text-field.png) * * This overload provides access to the input text, cursor position and selection range and IME * composition. If you only want to observe an input text change, use the OutlinedTextField overload * with the [String] parameter instead. * * @param value the input [TextFieldValue] to be shown in the text field * @param onValueChange the callback that is triggered when the input service updates values in * [TextFieldValue]. An updated [TextFieldValue] comes as a parameter of the callback * @param modifier the [Modifier] to be applied to this text field * @param enabled controls the enabled state of this text field. When `false`, this component will * not respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param readOnly controls the editable state of the text field. When `true`, the text field cannot * be modified. However, a user can focus it and copy text from it. Read-only text fields are * usually used to display pre-filled forms that a user cannot edit. * @param textStyle the style to be applied to the input text. Defaults to [LocalTextStyle]. * @param label the optional label to be displayed with this text field. The default text style uses * [Typography.bodySmall] when minimized and [Typography.bodyLarge] when expanded. * @param placeholder the optional placeholder to be displayed when the text field is in focus and * the input text is empty. The default text style for internal [Text] is [Typography.bodyLarge] * @param leadingIcon the optional leading icon to be displayed at the beginning of the text field * container * @param trailingIcon the optional trailing icon to be displayed at the end of the text field * container * @param prefix the optional prefix to be displayed before the input text in the text field * @param suffix the optional suffix to be displayed after the input text in the text field * @param supportingText the optional supporting text to be displayed below the text field * @param isError indicates if the text field's current value is in error state. If set to true, the * label, bottom indicator and trailing icon by default will be displayed in error color * @param visualTransformation transforms the visual representation of the input [value] For * example, you can use * [PasswordVisualTransformation][androidx.compose.ui.text.input.PasswordVisualTransformation] to * create a password text field. By default, no visual transformation is applied. * @param keyboardOptions software keyboard options that contains configuration such as * [KeyboardType] and [ImeAction] * @param keyboardActions when the input service emits an IME action, the corresponding callback is * called. Note that this IME action may be different from what you specified in * [KeyboardOptions.imeAction] * @param singleLine when `true`, this text field becomes a single horizontally scrolling text field * instead of wrapping onto multiple lines. The keyboard will be informed to not show the return * key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the maxLines * attribute will be automatically set to 1. * @param maxLines the maximum height in terms of maximum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param minLines the minimum height in terms of minimum number of visible lines. It is required * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this text field. You can use this to change the text field's * appearance or preview the text field in different states. Note that if `null` is provided, * interactions will still happen internally. * @param shape defines the shape of this text field's border * @param colors [TextFieldColors] that will be used to resolve the colors used for this text field * in different states. See [OutlinedTextFieldDefaults.colors]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun OutlinedTextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, readOnly: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, label: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, prefix: @Composable (() -> Unit)? = null, suffix: @Composable (() -> Unit)? = null, supportingText: @Composable (() -> Unit)? = null, isError: Boolean = false, visualTransformation: VisualTransformation = VisualTransformation.None, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, interactionSource: MutableInteractionSource? = null, shape: Shape = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors(), ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // If color is not provided via the text style, use content color as a default val textColor = textStyle.color.takeOrElse { val focused = interactionSource.collectIsFocusedAsState().value colors.textColor(enabled, isError, focused) } val mergedTextStyle = textStyle.merge(TextStyle(color = textColor)) CompositionLocalProvider(LocalTextSelectionColors provides colors.textSelectionColors) { BasicTextField( value = value, modifier = modifier .then( if (label != null) { Modifier // Merge semantics at the beginning of the modifier chain to ensure // padding is considered part of the text field. .semantics(mergeDescendants = true) {} .padding(top = minimizedLabelHalfHeight()) } else { Modifier } ) .defaultErrorSemantics(isError, getString(Strings.DefaultErrorMessage)) .defaultMinSize( minWidth = OutlinedTextFieldDefaults.MinWidth, minHeight = OutlinedTextFieldDefaults.MinHeight, ), onValueChange = onValueChange, enabled = enabled, readOnly = readOnly, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError)), visualTransformation = visualTransformation, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, maxLines = maxLines, minLines = minLines, decorationBox = @Composable { innerTextField -> OutlinedTextFieldDefaults.DecorationBox( value = value.text, visualTransformation = visualTransformation, innerTextField = innerTextField, placeholder = placeholder, label = label, leadingIcon = leadingIcon, trailingIcon = trailingIcon, prefix = prefix, suffix = suffix, supportingText = supportingText, singleLine = singleLine, enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, container = { OutlinedTextFieldDefaults.Container( enabled = enabled, isError = isError, interactionSource = interactionSource, colors = colors, shape = shape, ) }, ) }, ) } } /** * Layout of the leading and trailing icons and the text field, label and placeholder in * [OutlinedTextField]. It doesn't use Row to position the icons and middle part because label * should not be positioned in the middle part. */ @Composable internal fun OutlinedTextFieldLayout( modifier: Modifier, textField: @Composable () -> Unit, placeholder: @Composable ((Modifier) -> Unit)?, label: @Composable (() -> Unit)?, leading: @Composable (() -> Unit)?, trailing: @Composable (() -> Unit)?, prefix: @Composable (() -> Unit)?, suffix: @Composable (() -> Unit)?, singleLine: Boolean, labelPosition: TextFieldLabelPosition, labelProgress: FloatProducer, onLabelMeasured: (Size) -> Unit, container: @Composable () -> Unit, supporting: @Composable (() -> Unit)?, paddingValues: PaddingValues, ) { val horizontalIconPadding = textFieldHorizontalIconPadding() val measurePolicy = remember( onLabelMeasured, singleLine, labelPosition, labelProgress, paddingValues, horizontalIconPadding, ) { OutlinedTextFieldMeasurePolicy( onLabelMeasured = onLabelMeasured, singleLine = singleLine, labelPosition = labelPosition, labelProgress = labelProgress, paddingValues = paddingValues, horizontalIconPadding = horizontalIconPadding, ) } val layoutDirection = LocalLayoutDirection.current Layout( modifier = modifier, content = { container() if (leading != null) { Box( modifier = Modifier.layoutId(LeadingId).minimumInteractiveComponentSize(), contentAlignment = Alignment.Center, ) { leading() } } if (trailing != null) { Box( modifier = Modifier.layoutId(TrailingId).minimumInteractiveComponentSize(), contentAlignment = Alignment.Center, ) { trailing() } } val startTextFieldPadding = paddingValues.calculateStartPadding(layoutDirection) val endTextFieldPadding = paddingValues.calculateEndPadding(layoutDirection) val startPadding = if (leading != null) { (startTextFieldPadding - horizontalIconPadding).coerceAtLeast(0.dp) } else { startTextFieldPadding } val endPadding = if (trailing != null) { (endTextFieldPadding - horizontalIconPadding).coerceAtLeast(0.dp) } else { endTextFieldPadding } if (prefix != null) { Box( Modifier.layoutId(PrefixId) .heightIn(min = MinTextLineHeight) .wrapContentHeight() .padding(start = startPadding, end = PrefixSuffixTextPadding) ) { prefix() } } if (suffix != null) { Box( Modifier.layoutId(SuffixId) .heightIn(min = MinTextLineHeight) .wrapContentHeight() .padding(start = PrefixSuffixTextPadding, end = endPadding) ) { suffix() } } val textPadding = Modifier.heightIn(min = MinTextLineHeight) .wrapContentHeight() .padding( start = if (prefix == null) startPadding else 0.dp, end = if (suffix == null) endPadding else 0.dp, ) if (placeholder != null) { placeholder(Modifier.layoutId(PlaceholderId).then(textPadding)) } Box( modifier = Modifier.layoutId(TextFieldId).then(textPadding), propagateMinConstraints = true, ) { textField() } val labelPadding = if (labelPosition is TextFieldLabelPosition.Above) { Modifier.padding( start = AboveLabelHorizontalPadding, end = AboveLabelHorizontalPadding, bottom = AboveLabelBottomPadding, ) } else { Modifier } if (label != null) { Box( Modifier.textFieldLabelMinHeight { lerp(MinTextLineHeight, MinFocusedLabelLineHeight, labelProgress()) } .wrapContentHeight() .layoutId(LabelId) .then(labelPadding) ) { label() } } if (supporting != null) { Box( Modifier.layoutId(SupportingId) .heightIn(min = MinSupportingTextLineHeight) .wrapContentHeight() .padding(TextFieldDefaults.supportingTextPadding()) ) { supporting() } } }, measurePolicy = measurePolicy, ) } private class OutlinedTextFieldMeasurePolicy( private val onLabelMeasured: (Size) -> Unit, private val singleLine: Boolean, private val labelPosition: TextFieldLabelPosition, private val labelProgress: FloatProducer, private val paddingValues: PaddingValues, private val horizontalIconPadding: Dp, ) : MeasurePolicy { override fun MeasureScope.measure( measurables: List, constraints: Constraints, ): MeasureResult { val labelProgress = labelProgress() var occupiedSpaceHorizontally = 0 var occupiedSpaceVertically = 0 val bottomPadding = paddingValues.calculateBottomPadding().roundToPx() val relaxedConstraints = constraints.copy(minWidth = 0, minHeight = 0) // measure leading icon val leadingPlaceable = measurables.fastFirstOrNull { it.layoutId == LeadingId }?.measure(relaxedConstraints) occupiedSpaceHorizontally += leadingPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, leadingPlaceable.heightOrZero) // measure trailing icon val trailingPlaceable = measurables .fastFirstOrNull { it.layoutId == TrailingId } ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += trailingPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, trailingPlaceable.heightOrZero) // measure prefix val prefixPlaceable = measurables .fastFirstOrNull { it.layoutId == PrefixId } ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += prefixPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, prefixPlaceable.heightOrZero) // measure suffix val suffixPlaceable = measurables .fastFirstOrNull { it.layoutId == SuffixId } ?.measure(relaxedConstraints.offset(horizontal = -occupiedSpaceHorizontally)) occupiedSpaceHorizontally += suffixPlaceable.widthOrZero occupiedSpaceVertically = max(occupiedSpaceVertically, suffixPlaceable.heightOrZero) // measure label val isLabelAbove = labelPosition is TextFieldLabelPosition.Above val labelMeasurable = measurables.fastFirstOrNull { it.layoutId == LabelId } var labelPlaceable: Placeable? = null val labelIntrinsicHeight: Int if (!isLabelAbove) { // if label is not Above, we can measure it like normal val totalHorizontalPadding = paddingValues.calculateLeftPadding(layoutDirection).roundToPx() + paddingValues.calculateRightPadding(layoutDirection).roundToPx() val labelHorizontalConstraintOffset = lerp( occupiedSpaceHorizontally + totalHorizontalPadding, // label in middle totalHorizontalPadding, // label in outline labelProgress, ) val labelConstraints = relaxedConstraints.offset( horizontal = -labelHorizontalConstraintOffset, vertical = -bottomPadding, ) labelPlaceable = labelMeasurable?.measure(labelConstraints) val labelSize = labelPlaceable?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: Size.Zero onLabelMeasured(labelSize) labelIntrinsicHeight = 0 } else { // if label is Above, it must be measured after other elements, but we // reserve space for it using its intrinsic height as a heuristic labelIntrinsicHeight = labelMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0 } // supporting text must be measured after other elements, but we // reserve space for it using its intrinsic height as a heuristic val supportingMeasurable = measurables.fastFirstOrNull { it.layoutId == SupportingId } val supportingIntrinsicHeight = supportingMeasurable?.minIntrinsicHeight(constraints.minWidth) ?: 0 // measure text field val topPadding = if (isLabelAbove) { paddingValues.calculateTopPadding().roundToPx() } else { max( labelPlaceable.heightOrZero / 2, paddingValues.calculateTopPadding().roundToPx(), ) } val textConstraints = constraints .offset( horizontal = -occupiedSpaceHorizontally, vertical = -bottomPadding - topPadding - labelIntrinsicHeight - supportingIntrinsicHeight, ) .copy(minHeight = 0) val textFieldPlaceable = measurables.fastFirst { it.layoutId == TextFieldId }.measure(textConstraints) // measure placeholder val placeholderConstraints = textConstraints.copy(minWidth = 0) val placeholderPlaceable = measurables .fastFirstOrNull { it.layoutId == PlaceholderId } ?.measure(placeholderConstraints) occupiedSpaceVertically = max( occupiedSpaceVertically, max(textFieldPlaceable.heightOrZero, placeholderPlaceable.heightOrZero) + topPadding + bottomPadding, ) val width = calculateWidth( leadingPlaceableWidth = leadingPlaceable.widthOrZero, trailingPlaceableWidth = trailingPlaceable.widthOrZero, prefixPlaceableWidth = prefixPlaceable.widthOrZero, suffixPlaceableWidth = suffixPlaceable.widthOrZero, textFieldPlaceableWidth = textFieldPlaceable.width, labelPlaceableWidth = labelPlaceable.widthOrZero, placeholderPlaceableWidth = placeholderPlaceable.widthOrZero, constraints = constraints, labelProgress = labelProgress, ) if (isLabelAbove) { // now that we know the width, measure label val labelConstraints = relaxedConstraints.copy(maxHeight = labelIntrinsicHeight, maxWidth = width) labelPlaceable = labelMeasurable?.measure(labelConstraints) val labelSize = labelPlaceable?.let { Size(it.width.toFloat(), it.height.toFloat()) } ?: Size.Zero onLabelMeasured(labelSize) } // measure supporting text val supportingConstraints = relaxedConstraints .offset(vertical = -occupiedSpaceVertically) .copy(minHeight = 0, maxWidth = width) val supportingPlaceable = supportingMeasurable?.measure(supportingConstraints) val supportingHeight = supportingPlaceable.heightOrZero val totalHeight = calculateHeight( leadingHeight = leadingPlaceable.heightOrZero, trailingHeight = trailingPlaceable.heightOrZero, prefixHeight = prefixPlaceable.heightOrZero, suffixHeight = suffixPlaceable.heightOrZero, textFieldHeight = textFieldPlaceable.height, labelHeight = labelPlaceable.heightOrZero, placeholderHeight = placeholderPlaceable.heightOrZero, supportingHeight = supportingPlaceable.heightOrZero, constraints = constraints, isLabelAbove = isLabelAbove, labelProgress = labelProgress, ) val height = totalHeight - supportingHeight - (if (isLabelAbove) labelPlaceable.heightOrZero else 0) val containerPlaceable = measurables .fastFirst { it.layoutId == ContainerId } .measure( Constraints( minWidth = if (width != Constraints.Infinity) width else 0, maxWidth = width, minHeight = if (height != Constraints.Infinity) height else 0, maxHeight = height, ) ) return layout(width, totalHeight) { place( totalHeight = totalHeight, width = width, leadingPlaceable = leadingPlaceable, trailingPlaceable = trailingPlaceable, prefixPlaceable = prefixPlaceable, suffixPlaceable = suffixPlaceable, textFieldPlaceable = textFieldPlaceable, labelPlaceable = labelPlaceable, placeholderPlaceable = placeholderPlaceable, containerPlaceable = containerPlaceable, supportingPlaceable = supportingPlaceable, density = density, layoutDirection = layoutDirection, isLabelAbove = isLabelAbove, labelProgress = labelProgress, iconPadding = horizontalIconPadding.toPx(), ) } } override fun IntrinsicMeasureScope.maxIntrinsicHeight( measurables: List, width: Int, ): Int { return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> intrinsicMeasurable.maxIntrinsicHeight(w) } } override fun IntrinsicMeasureScope.minIntrinsicHeight( measurables: List, width: Int, ): Int { return intrinsicHeight(measurables, width) { intrinsicMeasurable, w -> intrinsicMeasurable.minIntrinsicHeight(w) } } override fun IntrinsicMeasureScope.maxIntrinsicWidth( measurables: List, height: Int, ): Int { return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> intrinsicMeasurable.maxIntrinsicWidth(h) } } override fun IntrinsicMeasureScope.minIntrinsicWidth( measurables: List, height: Int, ): Int { return intrinsicWidth(measurables, height) { intrinsicMeasurable, h -> intrinsicMeasurable.minIntrinsicWidth(h) } } private fun IntrinsicMeasureScope.intrinsicWidth( measurables: List, height: Int, intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int, ): Int { val textFieldWidth = intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, height) val labelWidth = measurables .fastFirstOrNull { it.layoutId == LabelId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val trailingWidth = measurables .fastFirstOrNull { it.layoutId == TrailingId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val leadingWidth = measurables .fastFirstOrNull { it.layoutId == LeadingId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val prefixWidth = measurables .fastFirstOrNull { it.layoutId == PrefixId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val suffixWidth = measurables .fastFirstOrNull { it.layoutId == SuffixId } ?.let { intrinsicMeasurer(it, height) } ?: 0 val placeholderWidth = measurables .fastFirstOrNull { it.layoutId == PlaceholderId } ?.let { intrinsicMeasurer(it, height) } ?: 0 return calculateWidth( leadingPlaceableWidth = leadingWidth, trailingPlaceableWidth = trailingWidth, prefixPlaceableWidth = prefixWidth, suffixPlaceableWidth = suffixWidth, textFieldPlaceableWidth = textFieldWidth, labelPlaceableWidth = labelWidth, placeholderPlaceableWidth = placeholderWidth, constraints = Constraints(), labelProgress = labelProgress(), ) } private fun IntrinsicMeasureScope.intrinsicHeight( measurables: List, width: Int, intrinsicMeasurer: (IntrinsicMeasurable, Int) -> Int, ): Int { val labelProgress = labelProgress() var remainingWidth = width val leadingHeight = measurables .fastFirstOrNull { it.layoutId == LeadingId } ?.let { remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) intrinsicMeasurer(it, width) } ?: 0 val trailingHeight = measurables .fastFirstOrNull { it.layoutId == TrailingId } ?.let { remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) intrinsicMeasurer(it, width) } ?: 0 val labelHeight = measurables .fastFirstOrNull { it.layoutId == LabelId } ?.let { intrinsicMeasurer(it, lerp(remainingWidth, width, labelProgress)) } ?: 0 val prefixHeight = measurables .fastFirstOrNull { it.layoutId == PrefixId } ?.let { val height = intrinsicMeasurer(it, remainingWidth) remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val suffixHeight = measurables .fastFirstOrNull { it.layoutId == SuffixId } ?.let { val height = intrinsicMeasurer(it, remainingWidth) remainingWidth = remainingWidth.subtractConstraintSafely( it.maxIntrinsicWidth(Constraints.Infinity) ) height } ?: 0 val textFieldHeight = intrinsicMeasurer(measurables.fastFirst { it.layoutId == TextFieldId }, remainingWidth) val placeholderHeight = measurables .fastFirstOrNull { it.layoutId == PlaceholderId } ?.let { intrinsicMeasurer(it, remainingWidth) } ?: 0 val supportingHeight = measurables .fastFirstOrNull { it.layoutId == SupportingId } ?.let { intrinsicMeasurer(it, width) } ?: 0 return calculateHeight( leadingHeight = leadingHeight, trailingHeight = trailingHeight, prefixHeight = prefixHeight, suffixHeight = suffixHeight, textFieldHeight = textFieldHeight, labelHeight = labelHeight, placeholderHeight = placeholderHeight, supportingHeight = supportingHeight, constraints = Constraints(), isLabelAbove = labelPosition is TextFieldLabelPosition.Above, labelProgress = labelProgress, ) } /** * Calculate the width of the [OutlinedTextField] given all elements that should be placed * inside. */ private fun Density.calculateWidth( leadingPlaceableWidth: Int, trailingPlaceableWidth: Int, prefixPlaceableWidth: Int, suffixPlaceableWidth: Int, textFieldPlaceableWidth: Int, labelPlaceableWidth: Int, placeholderPlaceableWidth: Int, constraints: Constraints, labelProgress: Float, ): Int { val affixTotalWidth = prefixPlaceableWidth + suffixPlaceableWidth val middleSection = maxOf( textFieldPlaceableWidth + affixTotalWidth, placeholderPlaceableWidth + affixTotalWidth, // Prefix/suffix does not get applied to label lerp(labelPlaceableWidth, 0, labelProgress), ) val wrappedWidth = leadingPlaceableWidth + middleSection + trailingPlaceableWidth // Actual LayoutDirection doesn't matter; we only need the sum val labelHorizontalPadding = (paddingValues.calculateLeftPadding(LayoutDirection.Ltr) + paddingValues.calculateRightPadding(LayoutDirection.Ltr)) .toPx() val focusedLabelWidth = ((labelPlaceableWidth + labelHorizontalPadding) * labelProgress).roundToInt() return constraints.constrainWidth(max(wrappedWidth, focusedLabelWidth)) } /** * Calculate the height of the [OutlinedTextField] given all elements that should be placed * inside. This includes the supporting text, if it exists, even though this element is not * "visually" inside the text field. */ private fun Density.calculateHeight( leadingHeight: Int, trailingHeight: Int, prefixHeight: Int, suffixHeight: Int, textFieldHeight: Int, labelHeight: Int, placeholderHeight: Int, supportingHeight: Int, constraints: Constraints, isLabelAbove: Boolean, labelProgress: Float, ): Int { val inputFieldHeight = maxOf( textFieldHeight, placeholderHeight, prefixHeight, suffixHeight, if (isLabelAbove) 0 else lerp(labelHeight, 0, labelProgress), ) val topPadding = paddingValues.calculateTopPadding().toPx() val actualTopPadding = if (isLabelAbove) { topPadding } else { lerp(topPadding, max(topPadding, labelHeight / 2f), labelProgress) } val bottomPadding = paddingValues.calculateBottomPadding().toPx() val middleSectionHeight = actualTopPadding + inputFieldHeight + bottomPadding return constraints.constrainHeight( (if (isLabelAbove) labelHeight else 0) + maxOf(leadingHeight, trailingHeight, middleSectionHeight.roundToInt()) + supportingHeight ) } /** * Places the provided text field, placeholder, label, optional leading and trailing icons * inside the [OutlinedTextField] */ private fun Placeable.PlacementScope.place( totalHeight: Int, width: Int, leadingPlaceable: Placeable?, trailingPlaceable: Placeable?, prefixPlaceable: Placeable?, suffixPlaceable: Placeable?, textFieldPlaceable: Placeable, labelPlaceable: Placeable?, placeholderPlaceable: Placeable?, containerPlaceable: Placeable, supportingPlaceable: Placeable?, density: Float, layoutDirection: LayoutDirection, isLabelAbove: Boolean, labelProgress: Float, iconPadding: Float, ) { val yOffset = if (isLabelAbove) labelPlaceable.heightOrZero else 0 // place container containerPlaceable.place(0, yOffset) // Most elements should be positioned w.r.t the text field's "visual" height, i.e., // excluding the label (if it's Above) and the supporting text on bottom val height = totalHeight - supportingPlaceable.heightOrZero - (if (isLabelAbove) labelPlaceable.heightOrZero else 0) val topPadding = (paddingValues.calculateTopPadding().value * density).roundToInt() // placed center vertically and to the start edge horizontally leadingPlaceable?.placeRelative( 0, yOffset + Alignment.CenterVertically.align(leadingPlaceable.height, height), ) // label position is animated // in single line text field, label is centered vertically before animation starts labelPlaceable?.let { val startY = when { isLabelAbove -> 0 singleLine -> Alignment.CenterVertically.align(it.height, height) else -> topPadding } val endY = when { isLabelAbove -> 0 else -> -(it.height / 2) } val positionY = lerp(startY, endY, labelProgress) if (isLabelAbove) { val positionX = labelPosition.minimizedAlignment.align( size = labelPlaceable.width, space = width, layoutDirection = layoutDirection, ) // Not placeRelative because alignment already handles RTL labelPlaceable.place(positionX, positionY) } else { val startPadding = paddingValues.calculateStartPadding(layoutDirection).value * density val endPadding = paddingValues.calculateEndPadding(layoutDirection).value * density val leadingPlusPadding = if (leadingPlaceable == null) { startPadding } else { leadingPlaceable.width + (startPadding - iconPadding).coerceAtLeast(0f) } val trailingPlusPadding = if (trailingPlaceable == null) { endPadding } else { trailingPlaceable.width + (endPadding - iconPadding).coerceAtLeast(0f) } val leftPadding = if (layoutDirection == LayoutDirection.Ltr) startPadding else endPadding val leftIconPlusPadding = if (layoutDirection == LayoutDirection.Ltr) leadingPlusPadding else trailingPlusPadding val startX = labelPosition.expandedAlignment.align( size = labelPlaceable.width, space = width - (leadingPlusPadding + trailingPlusPadding).roundToInt(), layoutDirection = layoutDirection, ) + leftIconPlusPadding val endX = labelPosition.minimizedAlignment.align( size = labelPlaceable.width, space = width - (startPadding + endPadding).roundToInt(), layoutDirection = layoutDirection, ) + leftPadding val positionX = lerp(startX, endX, labelProgress).roundToInt() // Not placeRelative because alignment already handles RTL labelPlaceable.place(positionX, positionY) } } fun calculateVerticalPosition(placeable: Placeable): Int { val defaultPosition = yOffset + if (singleLine) { // Single line text fields have text components centered vertically. Alignment.CenterVertically.align(placeable.height, height) } else { // Multiline text fields have text components aligned to top with padding. topPadding } return if (labelPosition is TextFieldLabelPosition.Above) { defaultPosition } else { // Ensure components are placed below label when it's in the border max(defaultPosition, labelPlaceable.heightOrZero / 2) } } prefixPlaceable?.placeRelative( leadingPlaceable.widthOrZero, calculateVerticalPosition(prefixPlaceable), ) val textHorizontalPosition = leadingPlaceable.widthOrZero + prefixPlaceable.widthOrZero textFieldPlaceable.placeRelative( textHorizontalPosition, calculateVerticalPosition(textFieldPlaceable), ) // placed similar to the input text above placeholderPlaceable?.placeRelative( textHorizontalPosition, calculateVerticalPosition(placeholderPlaceable), ) suffixPlaceable?.placeRelative( width - trailingPlaceable.widthOrZero - suffixPlaceable.width, calculateVerticalPosition(suffixPlaceable), ) // placed center vertically and to the end edge horizontally trailingPlaceable?.placeRelative( width - trailingPlaceable.width, yOffset + Alignment.CenterVertically.align(trailingPlaceable.height, height), ) // place supporting text supportingPlaceable?.placeRelative(0, yOffset + height) } } internal fun Modifier.outlineCutout( labelSize: () -> Size, alignment: Alignment.Horizontal, paddingValues: PaddingValues, ) = this.drawWithContent { val labelSizeValue = labelSize() val labelWidth = labelSizeValue.width if (labelWidth > 0f) { val innerPadding = OutlinedTextFieldInnerPadding.toPx() val leftPadding = paddingValues.calculateLeftPadding(layoutDirection).toPx() val rightPadding = paddingValues.calculateRightPadding(layoutDirection).toPx() val labelCenter = alignment.align( size = labelWidth.roundToInt(), space = (size.width - leftPadding - rightPadding).roundToInt(), layoutDirection = layoutDirection, ) + leftPadding + (labelWidth / 2) val left = (labelCenter - (labelWidth / 2) - innerPadding).coerceAtLeast(0f) val right = (labelCenter + (labelWidth / 2) + innerPadding).coerceAtMost(size.width) val labelHeight = labelSizeValue.height // using label height as a cutout area to make sure that no hairline artifacts are // left when we clip the border clipRect(left, -labelHeight / 2, right, labelHeight / 2, ClipOp.Difference) { this@drawWithContent.drawContent() } } else { this@drawWithContent.drawContent() } } private val OutlinedTextFieldInnerPadding = 4.dp ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/BottomSheetScaffold.kt ```kotlin /* * Copyright 2023 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.AnchoredDraggableDefaults import androidx.compose.foundation.gestures.DraggableAnchors import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.anchoredDraggable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.requiredHeightIn import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.SheetValue.Expanded import androidx.compose.material3.SheetValue.Hidden import androidx.compose.material3.SheetValue.PartiallyExpanded import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.draggableAnchors import androidx.compose.material3.internal.getString import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.Layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.collapse import androidx.compose.ui.semantics.dismiss import androidx.compose.ui.semantics.expand import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMaxOfOrNull import kotlin.math.max import kotlin.math.roundToInt import kotlinx.coroutines.launch /** * [Material Design standard bottom sheet * scaffold](https://m3.material.io/components/bottom-sheets/overview) * * Standard bottom sheets co-exist with the screen’s main UI region and allow for simultaneously * viewing and interacting with both regions. They are commonly used to keep a feature or secondary * content visible on screen when content in main UI region is frequently scrolled or panned. * * ![Bottom sheet * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png) * * This component provides API to put together several material components to construct your screen, * by ensuring proper layout strategy for them and collecting necessary data so these components * will work together correctly. * * A simple example of a standard bottom sheet looks like this: * * @sample androidx.compose.material3.samples.SimpleBottomSheetScaffoldSample * @param sheetContent the content of the bottom sheet * @param modifier the [Modifier] to be applied to the root of the scaffold * @param scaffoldState the state of the bottom sheet scaffold * @param sheetPeekHeight the height of the bottom sheet when it is collapsed * @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take. Pass in * [Dp.Unspecified] for a sheet that spans the entire screen width. * @param sheetShape the shape of the bottom sheet * @param sheetContainerColor the background color of the bottom sheet * @param sheetContentColor the preferred content color provided by the bottom sheet to its * children. Defaults to the matching content color for [sheetContainerColor], or if that is not a * color from the theme, this will keep the same content color set above the bottom sheet. * @param sheetTonalElevation when [sheetContainerColor] is [ColorScheme.surface], a translucent * primary color overlay is applied on top of the container. A higher tonal elevation value will * result in a darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param sheetShadowElevation the shadow elevation of the bottom sheet * @param sheetDragHandle optional visual marker to pull the scaffold's bottom sheet * @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's * input * @param topBar top app bar of the screen, typically a [TopAppBar] * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via * [SnackbarHostState.showSnackbar], typically a [SnackbarHost] * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this scaffold. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param content content of the screen. The lambda receives a [PaddingValues] that should be * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to * the child of the scroll, and not on the scroll itself. */ @Composable @ExperimentalMaterial3Api fun BottomSheetScaffold( sheetContent: @Composable ColumnScope.() -> Unit, modifier: Modifier = Modifier, scaffoldState: BottomSheetScaffoldState = rememberBottomSheetScaffoldState(), sheetPeekHeight: Dp = BottomSheetDefaults.SheetPeekHeight, sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, sheetShape: Shape = BottomSheetDefaults.ExpandedShape, sheetContainerColor: Color = BottomSheetDefaults.ContainerColor, sheetContentColor: Color = contentColorFor(sheetContainerColor), sheetTonalElevation: Dp = 0.dp, sheetShadowElevation: Dp = BottomSheetDefaults.Elevation, sheetDragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, sheetSwipeEnabled: Boolean = true, topBar: @Composable (() -> Unit)? = null, snackbarHost: @Composable (SnackbarHostState) -> Unit = { SnackbarHost(it) }, containerColor: Color = MaterialTheme.colorScheme.surface, contentColor: Color = contentColorFor(containerColor), content: @Composable (PaddingValues) -> Unit, ) { Box(modifier.fillMaxSize().background(containerColor)) { // Using composition local provider instead of Surface as Surface implements .clip() which // intercepts touch events in testing. CompositionLocalProvider(LocalContentColor provides contentColor) { BottomSheetScaffoldLayout( topBar = topBar, body = { content(PaddingValues(bottom = sheetPeekHeight)) }, snackbarHost = { snackbarHost(scaffoldState.snackbarHostState) }, sheetOffset = { scaffoldState.bottomSheetState.requireOffset() }, sheetState = scaffoldState.bottomSheetState, bottomSheet = { StandardBottomSheet( state = scaffoldState.bottomSheetState, peekHeight = sheetPeekHeight, sheetMaxWidth = sheetMaxWidth, sheetSwipeEnabled = sheetSwipeEnabled, shape = sheetShape, containerColor = sheetContainerColor, contentColor = sheetContentColor, tonalElevation = sheetTonalElevation, shadowElevation = sheetShadowElevation, dragHandle = sheetDragHandle, content = sheetContent, ) }, ) } } } /** * State of the [BottomSheetScaffold] composable. * * @param bottomSheetState the state of the persistent bottom sheet * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold */ @ExperimentalMaterial3Api @Stable class BottomSheetScaffoldState( val bottomSheetState: SheetState, val snackbarHostState: SnackbarHostState, ) /** * Create and [remember] a [BottomSheetScaffoldState]. * * @param bottomSheetState the state of the standard bottom sheet. See * [rememberStandardBottomSheetState] * @param snackbarHostState the [SnackbarHostState] used to show snackbars inside the scaffold */ @Composable @ExperimentalMaterial3Api fun rememberBottomSheetScaffoldState( bottomSheetState: SheetState = rememberStandardBottomSheetState(), snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, ): BottomSheetScaffoldState { return remember(bottomSheetState, snackbarHostState) { BottomSheetScaffoldState( bottomSheetState = bottomSheetState, snackbarHostState = snackbarHostState, ) } } /** * Create and [remember] a [SheetState] for [BottomSheetScaffold]. * * @param initialValue the initial value of the state. Should be either [PartiallyExpanded] or * [Expanded] if [skipHiddenState] is true * @param confirmValueChange optional callback invoked to confirm or veto a pending state change * @param [skipHiddenState] whether Hidden state is skipped for [BottomSheetScaffold] */ @Composable @ExperimentalMaterial3Api fun rememberStandardBottomSheetState( initialValue: SheetValue = PartiallyExpanded, confirmValueChange: (SheetValue) -> Boolean = { true }, skipHiddenState: Boolean = true, ) = rememberSheetState( confirmValueChange = confirmValueChange, initialValue = initialValue, skipHiddenState = skipHiddenState, ) @OptIn(ExperimentalMaterial3Api::class) @Composable private fun StandardBottomSheet( state: SheetState, peekHeight: Dp, sheetMaxWidth: Dp, sheetSwipeEnabled: Boolean, shape: Shape, containerColor: Color, contentColor: Color, tonalElevation: Dp, shadowElevation: Dp, dragHandle: @Composable (() -> Unit)?, content: @Composable ColumnScope.() -> Unit, ) { // TODO Load the motionScheme tokens from the component tokens file val anchoredDraggableMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val showMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val hideMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.FastEffects.value() SideEffect { state.showMotionSpec = showMotion state.hideMotionSpec = hideMotion state.anchoredDraggableMotionSpec = anchoredDraggableMotion } val scope = rememberCoroutineScope() val orientation = Orientation.Vertical val peekHeightPx = with(LocalDensity.current) { peekHeight.toPx() } val anchoredDraggableFlingBehavior = AnchoredDraggableDefaults.flingBehavior( state = state.anchoredDraggableState, positionalThreshold = { _ -> state.positionalThreshold.invoke() }, animationSpec = BottomSheetAnimationSpec, ) val nestedScroll = if (sheetSwipeEnabled) { Modifier.nestedScroll( remember(state.anchoredDraggableState) { ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection( sheetState = state, orientation = orientation, flingBehavior = anchoredDraggableFlingBehavior, ) } ) } else { Modifier } Surface( modifier = Modifier.widthIn(max = sheetMaxWidth) .fillMaxWidth() .requiredHeightIn(min = peekHeight) .then(nestedScroll) .draggableAnchors(state.anchoredDraggableState, orientation) { sheetSize, constraints -> val layoutHeight = constraints.maxHeight.toFloat() val sheetHeight = sheetSize.height.toFloat() val newAnchors = DraggableAnchors { val isHiddenAnchorAvailable = sheetHeight == 0f || peekHeightPx == 0f || !state.skipHiddenState // We are preserving ambiguous anchor reconciliation for first layout pass. // This handles the use case where sheetPeekHeight is backed by a mutable // value which is backed by 0.dp before being recalculated. We can assume // the state is in its first pass by asserting anchor sizes are zero, as we // enforce at least 1 anchor below. We then settle at partial as this is // the anchor external users have access to via sheetPeekHeight API. val isInitialLayout = state.anchoredDraggableState.anchors.size == 0 val isStableAtPartial = state.currentValue == PartiallyExpanded && !state.isAnimationRunning val isAmbiguousPartialAllowed = peekHeightPx == 0f && (isInitialLayout || isStableAtPartial) val isPartiallyExpandedAnchorAvailable = !state.skipPartiallyExpanded && (peekHeightPx > 0f || isAmbiguousPartialAllowed) && peekHeightPx != sheetHeight val isExpandedAnchorAvailable = sheetHeight > 0f require( isHiddenAnchorAvailable || isPartiallyExpandedAnchorAvailable || isExpandedAnchorAvailable ) { "BottomSheetScaffold: Require at least 1 anchor to be initialized" } if (isPartiallyExpandedAnchorAvailable) { PartiallyExpanded at (layoutHeight - peekHeightPx) } if (isHiddenAnchorAvailable) { Hidden at layoutHeight } if (isExpandedAnchorAvailable) { Expanded at layoutHeight - sheetHeight } } val newTarget = when (val oldTarget = state.targetValue) { Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget PartiallyExpanded -> when { newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded newAnchors.hasPositionFor(Expanded) -> Expanded newAnchors.hasPositionFor(Hidden) -> Hidden else -> oldTarget } Expanded -> if (newAnchors.hasPositionFor(Expanded)) Expanded else Hidden } return@draggableAnchors newAnchors to newTarget } .anchoredDraggable( state = state.anchoredDraggableState, orientation = orientation, enabled = sheetSwipeEnabled, flingBehavior = anchoredDraggableFlingBehavior, ) // Scale up the Surface vertically in case the sheet's offset overflows below the // min anchor. This is done to avoid showing a gap when the sheet opens and bounces // when it's applied with a bouncy motion. Note that the content inside the Surface // is scaled back down to maintain its aspect ratio (see below). .verticalScaleUp(state), shape = shape, color = containerColor, contentColor = contentColor, tonalElevation = tonalElevation, shadowElevation = shadowElevation, ) { Column( Modifier.fillMaxWidth() // Scale the content down in case the sheet offset overflows below the min anchor. // The wrapping Surface is scaled up, so this is done to maintain the content's // aspect ratio. .verticalScaleDown(state) ) { if (dragHandle != null) { val partialExpandActionLabel = getString(Strings.BottomSheetPartialExpandDescription) val dismissActionLabel = getString(Strings.BottomSheetDismissDescription) val expandActionLabel = getString(Strings.BottomSheetExpandDescription) DragHandleWithTooltip( modifier = Modifier.clickable { when (state.currentValue) { Expanded -> scope.launch { if (!state.skipHiddenState) { state.hide() } else { state.partialExpand() } } PartiallyExpanded -> scope.launch { state.expand() } else -> scope.launch { state.show() } } } .semantics(mergeDescendants = true) { with(state) { // Provides semantics to interact with the bottomsheet if // there is more than one anchor to swipe to and swiping is // enabled. if ( anchoredDraggableState.anchors.size > 1 && sheetSwipeEnabled ) { if (currentValue == PartiallyExpanded) { expand(expandActionLabel) { val canExpand = confirmValueChange(Expanded) if (canExpand) { scope.launch { expand() } } return@expand canExpand } } else { collapse(partialExpandActionLabel) { val canPartiallyExpand = confirmValueChange(PartiallyExpanded) scope.launch { partialExpand() } return@collapse canPartiallyExpand } } if (!state.skipHiddenState) { dismiss(dismissActionLabel) { val canHide = confirmValueChange(Hidden) scope.launch { hide() } return@dismiss canHide } } } } }, content = dragHandle, ) } content() } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun BottomSheetScaffoldLayout( topBar: @Composable (() -> Unit)?, body: @Composable () -> Unit, bottomSheet: @Composable () -> Unit, snackbarHost: @Composable () -> Unit, sheetOffset: () -> Float, sheetState: SheetState, ) { Layout( contents = listOf<@Composable () -> Unit>(topBar ?: {}, body, bottomSheet, snackbarHost) ) { (topBarMeasurables, bodyMeasurables, bottomSheetMeasurables, snackbarHostMeasurables), constraints -> val layoutWidth = constraints.maxWidth val layoutHeight = constraints.maxHeight val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) val sheetPlaceables = bottomSheetMeasurables.fastMap { it.measure(looseConstraints) } val topBarPlaceables = topBarMeasurables.fastMap { it.measure(looseConstraints) } val topBarHeight = topBarPlaceables.fastMaxOfOrNull { it.height } ?: 0 val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight) val bodyPlaceables = bodyMeasurables.fastMap { it.measure(bodyConstraints) } val snackbarPlaceables = snackbarHostMeasurables.fastMap { it.measure(looseConstraints) } layout(layoutWidth, layoutHeight) { val sheetWidth = sheetPlaceables.fastMaxOfOrNull { it.width } ?: 0 val sheetOffsetX = max(0, (layoutWidth - sheetWidth) / 2) val snackbarWidth = snackbarPlaceables.fastMaxOfOrNull { it.width } ?: 0 val snackbarHeight = snackbarPlaceables.fastMaxOfOrNull { it.height } ?: 0 val snackbarOffsetX = (layoutWidth - snackbarWidth) / 2 val snackbarOffsetY = when (sheetState.currentValue) { PartiallyExpanded -> sheetOffset().roundToInt() - snackbarHeight Expanded, Hidden -> layoutHeight - snackbarHeight } // Placement order is important for elevation bodyPlaceables.fastForEach { it.placeRelative(0, topBarHeight) } topBarPlaceables.fastForEach { it.placeRelative(0, 0) } sheetPlaceables.fastForEach { it.placeRelative(sheetOffsetX, 0) } snackbarPlaceables.fastForEach { it.placeRelative(snackbarOffsetX, snackbarOffsetY) } } } } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/ModalBottomSheet.kt ```kotlin /* * Copyright 2024 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.imePadding import androidx.compose.material3.SheetValue.Expanded import androidx.compose.material3.SheetValue.Hidden import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.getString import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment.Companion.TopCenter import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.semantics.isTraversalGroup import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch /** * [Material Design modal bottom sheet](https://m3.material.io/components/bottom-sheets/overview) * * Modal bottom sheets are used as an alternative to inline menus or simple dialogs on mobile, * especially when offering a long list of action items, or when items require longer descriptions * and icons. Like dialogs, modal bottom sheets appear in front of app content, disabling all other * app functionality when they appear, and remaining on screen until confirmed, dismissed, or a * required action has been taken. * * ![Bottom sheet * image](https://developer.android.com/images/reference/androidx/compose/material3/bottom_sheet.png) * * A simple example of a modal bottom sheet looks like this: * * @sample androidx.compose.material3.samples.ModalBottomSheetSample * @param onDismissRequest Executes when the user clicks outside of the bottom sheet, after sheet * animates to [Hidden]. * @param modifier Optional [Modifier] for the bottom sheet. * @param sheetState The state of the bottom sheet. * @param sheetMaxWidth [Dp] that defines what the maximum width the sheet will take. Pass in * [Dp.Unspecified] for a sheet that spans the entire screen width. * @param sheetGesturesEnabled Whether the bottom sheet can be interacted with by gestures. * @param shape The shape of the bottom sheet. * @param containerColor The color used for the background of this bottom sheet * @param contentColor The preferred color for content inside this bottom sheet. Defaults to either * the matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color * overlay is applied on top of the container. A higher tonal elevation value will result in a * darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param scrimColor Color of the scrim that obscures content when the bottom sheet is open. * @param dragHandle Optional visual marker to swipe the bottom sheet. * @param contentWindowInsets callback which provides window insets to be passed to the bottom sheet * content via [Modifier.windowInsetsPadding]. [ModalBottomSheet] will pre-emptively consume top * insets based on it's current offset. This keeps content outside of the expected window insets * at any position. * @param properties [ModalBottomSheetProperties] for further customization of this modal bottom * sheet's window behavior. * @param content The content to be displayed inside the bottom sheet. */ @Composable @ExperimentalMaterial3Api fun ModalBottomSheet( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, sheetState: SheetState = rememberModalBottomSheetState(), sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth, sheetGesturesEnabled: Boolean = true, shape: Shape = BottomSheetDefaults.ExpandedShape, containerColor: Color = BottomSheetDefaults.ContainerColor, contentColor: Color = contentColorFor(containerColor), tonalElevation: Dp = 0.dp, scrimColor: Color = BottomSheetDefaults.ScrimColor, dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets }, properties: ModalBottomSheetProperties = ModalBottomSheetProperties(), content: @Composable ColumnScope.() -> Unit, ) { // TODO Load the motionScheme tokens from the component tokens file val anchoredDraggableMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val showMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val hideMotion: FiniteAnimationSpec = MotionSchemeKeyTokens.FastEffects.value() SideEffect { sheetState.showMotionSpec = showMotion sheetState.hideMotionSpec = hideMotion sheetState.anchoredDraggableMotionSpec = anchoredDraggableMotion } val scope = rememberCoroutineScope() val animateToDismiss: () -> Unit = { if (sheetState.confirmValueChange(Hidden)) { scope .launch { sheetState.hide() } .invokeOnCompletion { if (!sheetState.isVisible) { onDismissRequest() } } } } val settleToDismiss: () -> Unit = { if (sheetState.currentValue == Expanded && sheetState.hasPartiallyExpandedState) { // Smoothly animate away predictive back transformations since we are not fully // dismissing. We don't need to do this in the else below because we want to // preserve the predictive back transformations (scale) during the hide animation. scope.launch { sheetState.partialExpand() } } else { // Is expanded without collapsed state or is collapsed. scope.launch { sheetState.hide() }.invokeOnCompletion { onDismissRequest() } } } ModalBottomSheetDialog( properties = properties, contentColor = contentColor, onDismissRequest = settleToDismiss, ) { Box(modifier = Modifier.fillMaxSize().imePadding().semantics { isTraversalGroup = true }) { val sheetWindowInsets = remember(sheetState) { SheetWindowInsets(sheetState) } val isScrimVisible: Boolean by remember { derivedStateOf { sheetState.targetValue != Hidden } } val scrimAlpha by animateFloatAsState( targetValue = if (isScrimVisible) 1f else 0f, animationSpec = MotionSchemeKeyTokens.DefaultEffects.value(), label = "ScrimAlphaAnimation", ) Scrim( contentDescription = getString(Strings.CloseSheet), onClick = if (properties.shouldDismissOnClickOutside) animateToDismiss else null, alpha = { scrimAlpha }, color = scrimColor, ) BottomSheet( modifier = modifier.align(TopCenter).consumeWindowInsets(sheetWindowInsets), state = sheetState, onDismissRequest = onDismissRequest, maxWidth = sheetMaxWidth, gesturesEnabled = sheetGesturesEnabled, backHandlerEnabled = properties.shouldDismissOnBackPress, shape = shape, containerColor = containerColor, contentColor = contentColor, tonalElevation = tonalElevation, dragHandle = dragHandle, contentWindowInsets = contentWindowInsets, content = content, ) } } if (sheetState.hasExpandedState) { LaunchedEffect(sheetState) { sheetState.show() } } } /** * Properties used to customize the behavior of a [ModalBottomSheet]. * * @param shouldDismissOnBackPress Whether the modal bottom sheet can be dismissed by pressing the * back button. If true, pressing the back button will call onDismissRequest. * @param shouldDismissOnClickOutside Whether the modal bottom sheet can be dismissed by clicking on * the scrim. */ @Immutable @ExperimentalMaterial3Api expect class ModalBottomSheetProperties( shouldDismissOnBackPress: Boolean = true, shouldDismissOnClickOutside: Boolean = true, ) { val shouldDismissOnBackPress: Boolean val shouldDismissOnClickOutside: Boolean } /** Default values for [ModalBottomSheet] */ @Immutable @ExperimentalMaterial3Api expect object ModalBottomSheetDefaults { /** Properties used to customize the behavior of a [ModalBottomSheet]. */ val properties: ModalBottomSheetProperties } /** * Create and [remember] a [SheetState] for [ModalBottomSheet]. * * @param skipPartiallyExpanded Whether the partially expanded state, if the sheet is tall enough, * should be skipped. If true, the sheet will always expand to the [Expanded] state and move to * the [Hidden] state when hiding the sheet, either programmatically or by user interaction. * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change. */ @Composable @ExperimentalMaterial3Api fun rememberModalBottomSheetState( skipPartiallyExpanded: Boolean = false, confirmValueChange: (SheetValue) -> Boolean = { true }, ) = rememberSheetState( skipPartiallyExpanded = skipPartiallyExpanded, confirmValueChange = confirmValueChange, initialValue = Hidden, ) @Stable @OptIn(ExperimentalMaterial3Api::class) internal class SheetWindowInsets(private val state: SheetState) : WindowInsets { override fun getLeft(density: Density, layoutDirection: LayoutDirection): Int = 0 override fun getTop(density: Density): Int { val offset = state.anchoredDraggableState.offset return if (offset.isNaN()) 0 else offset.toInt().coerceAtLeast(0) } override fun getRight(density: Density, layoutDirection: LayoutDirection): Int = 0 override fun getBottom(density: Density): Int = 0 override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is SheetWindowInsets) return false return state == other.state } override fun hashCode(): Int = state.hashCode() } /** * [Dialog]-like component providing default window behavior for [BottomSheet]. This implementation * explicitly provides a full-screen edge to edge layout. * * The dialog is visible as long as it is part of the composition hierarchy. In order to let the * user dismiss the Dialog, the implementation of onDismissRequest should contain a way to remove * the dialog from the composition hierarchy. * * You can add implement a custom [ModalBottomSheet] by leveraging this API alongside [BottomSheet], * [draggableAnchoredSheet], and [Scrim]: * * @sample androidx.compose.material3.samples.ManualModalBottomSheetSample * @param onDismissRequest Callback which executes when user tries to dismiss * [ModalBottomSheetDialog]. * @param contentColor The content color of this dialog. Used to inform the default behavior of the * windows' system bars and content. * @param properties [ModalBottomSheetProperties] for further customization of this dialog. * @param content The content displayed in this [ModalBottomSheetDialog]. Usually [BottomSheet]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable internal expect fun ModalBottomSheetDialog( onDismissRequest: () -> Unit = {}, contentColor: Color = contentColorFor(BottomSheetDefaults.ContainerColor), properties: ModalBottomSheetProperties = ModalBottomSheetProperties(), content: @Composable () -> Unit, ) ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AlertDialog.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.material3.internal.ProvideContentColorTextStyle import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.getString import androidx.compose.material3.tokens.DialogTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.paneTitle import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.takeOrElse import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties /** * [Material Design basic dialog](https://m3.material.io/components/dialogs/overview) * * Dialogs provide important prompts in a user flow. They can require an action, communicate * information, or help users accomplish a task. * * ![Basic dialog * image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png) * * The dialog will position its buttons, typically [TextButton]s, based on the available space. By * default it will try to place them horizontally next to each other and fallback to horizontal * placement if not enough space is available. * * Simple usage: * * @sample androidx.compose.material3.samples.AlertDialogSample * * Usage with a "Hero" icon: * * @sample androidx.compose.material3.samples.AlertDialogWithIconSample * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or * pressing the back button. This is not called when the dismiss button is clicked. * @param confirmButton button which is meant to confirm a proposed action, thus resolving what * triggered the dialog. The dialog does not set up any events for this button so they need to be * set up by the caller. * @param modifier the [Modifier] to be applied to this dialog * @param dismissButton button which is meant to dismiss the dialog. The dialog does not set up any * events for this button so they need to be set up by the caller. * @param icon optional icon that will appear above the [title] or above the [text], in case a title * was not provided. * @param title title which should specify the purpose of the dialog. The title is not mandatory, * because there may be sufficient information inside the [text]. * @param text text which presents the details regarding the dialog's purpose. * @param shape defines the shape of this dialog's container * @param containerColor the color used for the background of this dialog. Use [Color.Transparent] * to have no color. * @param iconContentColor the content color used for the icon. * @param titleContentColor the content color used for the title. * @param textContentColor the content color used for the text. * @param tonalElevation when [containerColor] is [ColorScheme.surface], a translucent primary color * overlay is applied on top of the container. A higher tonal elevation value will result in a * darker color in light theme and lighter color in dark theme. See also: [Surface]. * @param properties typically platform specific properties to further configure the dialog. * @see BasicAlertDialog */ @Composable expect fun AlertDialog( onDismissRequest: () -> Unit, confirmButton: @Composable () -> Unit, modifier: Modifier = Modifier, dismissButton: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null, title: @Composable (() -> Unit)? = null, text: @Composable (() -> Unit)? = null, shape: Shape = AlertDialogDefaults.shape, containerColor: Color = AlertDialogDefaults.containerColor, iconContentColor: Color = AlertDialogDefaults.iconContentColor, titleContentColor: Color = AlertDialogDefaults.titleContentColor, textContentColor: Color = AlertDialogDefaults.textContentColor, tonalElevation: Dp = AlertDialogDefaults.TonalElevation, properties: DialogProperties = DialogProperties(), ) /** * [Basic alert dialog dialog](https://m3.material.io/components/dialogs/overview) * * Dialogs provide important prompts in a user flow. They can require an action, communicate * information, or help users accomplish a task. * * ![Basic dialog * image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png) * * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that * your content will need to define its own styling. * * By default, the displayed dialog has the minimum height and width that the Material Design spec * defines. If required, these constraints can be overwritten by providing a `width` or `height` * [Modifier]s. * * Basic alert dialog usage with custom content: * * @sample androidx.compose.material3.samples.BasicAlertDialogSample * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or * pressing the back button. This is not called when the dismiss button is clicked. * @param modifier the [Modifier] to be applied to this dialog's content. * @param properties typically platform specific properties to further configure the dialog. * @param content the content of the dialog */ @OptIn(ExperimentalMaterial3ComponentOverrideApi::class) @ExperimentalMaterial3Api @Composable fun BasicAlertDialog( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, properties: DialogProperties = DialogProperties(), content: @Composable () -> Unit, ) { with(LocalBasicAlertDialogOverride.current) { BasicAlertDialogOverrideScope( onDismissRequest = onDismissRequest, modifier = modifier, properties = properties, content = content, ) .BasicAlertDialog() } } /** * This override provides the default behavior of the [BasicAlertDialog] component. * * [BasicAlertDialogOverride] used when no override is specified. */ @OptIn(ExperimentalMaterial3Api::class) @ExperimentalMaterial3ComponentOverrideApi object DefaultBasicAlertDialogOverride : BasicAlertDialogOverride { @Composable override fun BasicAlertDialogOverrideScope.BasicAlertDialog() { Dialog(onDismissRequest = onDismissRequest, properties = properties) { val dialogPaneDescription = getString(Strings.Dialog) Box( modifier = modifier .sizeIn(minWidth = DialogMinWidth, maxWidth = DialogMaxWidth) .then(Modifier.semantics { paneTitle = dialogPaneDescription }), propagateMinConstraints = true, ) { content() } } } } /** * [Basic alert dialog dialog](https://m3.material.io/components/dialogs/overview) * * Dialogs provide important prompts in a user flow. They can require an action, communicate * information, or help users accomplish a task. * * ![Basic dialog * image](https://developer.android.com/images/reference/androidx/compose/material3/basic-dialog.png) * * This basic alert dialog expects an arbitrary content that is defined by the caller. Note that * your content will need to define its own styling. * * By default, the displayed dialog has the minimum height and width that the Material Design spec * defines. If required, these constraints can be overwritten by providing a `width` or `height` * [Modifier]s. * * Basic alert dialog usage with custom content: * * @sample androidx.compose.material3.samples.BasicAlertDialogSample * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or * pressing the back button. This is not called when the dismiss button is clicked. * @param modifier the [Modifier] to be applied to this dialog's content. * @param properties typically platform specific properties to further configure the dialog. * @param content the content of the dialog */ @Deprecated( "Use BasicAlertDialog instead", replaceWith = ReplaceWith("BasicAlertDialog(onDismissRequest, modifier, properties, content)"), ) @ExperimentalMaterial3Api @Composable fun AlertDialog( onDismissRequest: () -> Unit, modifier: Modifier = Modifier, properties: DialogProperties = DialogProperties(), content: @Composable () -> Unit, ) = BasicAlertDialog(onDismissRequest, modifier, properties, content) /** Contains default values used for [AlertDialog] and [BasicAlertDialog]. */ object AlertDialogDefaults { /** The default shape for alert dialogs */ val shape: Shape @Composable get() = DialogTokens.ContainerShape.value /** The default container color for alert dialogs */ val containerColor: Color @Composable get() = DialogTokens.ContainerColor.value /** The default icon color for alert dialogs */ val iconContentColor: Color @Composable get() = DialogTokens.IconColor.value /** The default title color for alert dialogs */ val titleContentColor: Color @Composable get() = DialogTokens.HeadlineColor.value /** The default text color for alert dialogs */ val textContentColor: Color @Composable get() = DialogTokens.SupportingTextColor.value /** The default tonal elevation for alert dialogs */ val TonalElevation: Dp = 0.dp } @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun AlertDialogImpl( onDismissRequest: () -> Unit, confirmButton: @Composable () -> Unit, modifier: Modifier, dismissButton: @Composable (() -> Unit)?, icon: @Composable (() -> Unit)?, title: @Composable (() -> Unit)?, text: @Composable (() -> Unit)?, shape: Shape, containerColor: Color, iconContentColor: Color, titleContentColor: Color, textContentColor: Color, tonalElevation: Dp, properties: DialogProperties, ) { BasicAlertDialog( onDismissRequest = onDismissRequest, modifier = modifier, properties = properties, ) { AlertDialogContent( buttons = { val buttonPaddingFromMICS = LocalMinimumInteractiveComponentSize.current.takeOrElse { 0.dp } - ButtonDefaults.MinHeight AlertDialogFlowRow( mainAxisSpacing = ButtonsMainAxisSpacing, crossAxisSpacing = (ButtonsCrossAxisSpacing - buttonPaddingFromMICS).coerceIn( 0.dp, ButtonsCrossAxisSpacing, ), ) { confirmButton() dismissButton?.invoke() } }, icon = icon, title = title, text = text, shape = shape, containerColor = containerColor, tonalElevation = tonalElevation, // Note that a button content color is provided here from the dialog's token, but in // most cases, TextButtons should be used for dismiss and confirm buttons. TextButtons // will not consume this provided content color value, and will used their own defined // or default colors. buttonContentColor = DialogTokens.ActionLabelTextColor.value, iconContentColor = iconContentColor, titleContentColor = titleContentColor, textContentColor = textContentColor, ) } } @Composable internal fun AlertDialogContent( buttons: @Composable () -> Unit, modifier: Modifier = Modifier, icon: (@Composable () -> Unit)?, title: (@Composable () -> Unit)?, text: @Composable (() -> Unit)?, shape: Shape, containerColor: Color, tonalElevation: Dp, buttonContentColor: Color, iconContentColor: Color, titleContentColor: Color, textContentColor: Color, ) { Surface( modifier = modifier, shape = shape, color = containerColor, tonalElevation = tonalElevation, ) { Column(modifier = Modifier.padding(DialogPadding)) { icon?.let { CompositionLocalProvider(LocalContentColor provides iconContentColor) { Box(Modifier.padding(IconPadding).align(Alignment.CenterHorizontally)) { icon() } } } title?.let { ProvideContentColorTextStyle( contentColor = titleContentColor, textStyle = DialogTokens.HeadlineFont.value, ) { Box( // Align the title to the center when an icon is present. Modifier.padding(TitlePadding) .align( if (icon == null) { Alignment.Start } else { Alignment.CenterHorizontally } ) ) { title() } } } text?.let { val textStyle = DialogTokens.SupportingTextFont.value ProvideContentColorTextStyle( contentColor = textContentColor, textStyle = textStyle, ) { Box( Modifier.weight(weight = 1f, fill = false) .padding(TextPadding) .align(Alignment.Start) ) { text() } } } Box(modifier = Modifier.align(Alignment.End)) { val textStyle = DialogTokens.ActionLabelTextFont.value ProvideContentColorTextStyle( contentColor = buttonContentColor, textStyle = textStyle, content = buttons, ) } } } } /** * [FlowRow] for dialog buttons. The confirm button is expected to be the first child of [content]. */ @Composable internal fun AlertDialogFlowRow( mainAxisSpacing: Dp, crossAxisSpacing: Dp, content: @Composable () -> Unit, ) { val originalLayoutDirection = LocalLayoutDirection.current // The confirm button comes BEFORE the dismiss button when stacked vertically, // but AFTER the dismiss button when stacked horizontally. CompositionLocalProvider(LocalLayoutDirection provides originalLayoutDirection.flip()) { FlowRow( horizontalArrangement = Arrangement.spacedBy(mainAxisSpacing), verticalArrangement = Arrangement.spacedBy(crossAxisSpacing), ) { CompositionLocalProvider( LocalLayoutDirection provides originalLayoutDirection, content = content, ) } } } private fun LayoutDirection.flip(): LayoutDirection = when (this) { LayoutDirection.Ltr -> LayoutDirection.Rtl LayoutDirection.Rtl -> LayoutDirection.Ltr } internal val DialogMinWidth = 280.dp internal val DialogMaxWidth = 560.dp private val ButtonsMainAxisSpacing = 8.dp private val ButtonsCrossAxisSpacing = 8.dp // Paddings for each of the dialog's parts. private val DialogPadding = PaddingValues(all = 24.dp) private val IconPadding = PaddingValues(bottom = 16.dp) private val TitlePadding = PaddingValues(bottom = 16.dp) private val TextPadding = PaddingValues(bottom = 24.dp) /** * Interface that allows libraries to override the behavior of the [BasicAlertDialog] component. * * To override this component, implement the member function of this interface, then provide the * implementation to [LocalBasicAlertDialogOverride] in the Compose hierarchy. */ @ExperimentalMaterial3ComponentOverrideApi interface BasicAlertDialogOverride { /** Behavior function that is called by the [BasicAlertDialog] component. */ @Composable fun BasicAlertDialogOverrideScope.BasicAlertDialog() } /** * Parameters available to [BasicAlertDialog]. * * @param onDismissRequest called when the user tries to dismiss the Dialog by clicking outside or * pressing the back button. This is not called when the dismiss button is clicked. * @param modifier the [Modifier] to be applied to this dialog's content. * @param properties typically platform specific properties to further configure the dialog. * @param content the content of the dialog */ @ExperimentalMaterial3ComponentOverrideApi class BasicAlertDialogOverrideScope internal constructor( val onDismissRequest: () -> Unit, val modifier: Modifier = Modifier, val properties: DialogProperties = DialogProperties(), val content: @Composable () -> Unit, ) /** CompositionLocal containing the currently-selected [BasicAlertDialogOverride]. */ @ExperimentalMaterial3ComponentOverrideApi val LocalBasicAlertDialogOverride: ProvidableCompositionLocal = compositionLocalOf { DefaultBasicAlertDialogOverride } ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Snackbar.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.paddingFromBaseline import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.internal.Icons import androidx.compose.material3.internal.Strings import androidx.compose.material3.internal.getString import androidx.compose.material3.tokens.SnackbarTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.AlignmentLine import androidx.compose.ui.layout.FirstBaseline import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layoutId import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFirst import androidx.compose.ui.util.fastFirstOrNull import kotlin.math.max import kotlin.math.min /** * [Material Design snackbar](https://m3.material.io/components/snackbar/overview) * * Snackbars provide brief messages about app processes at the bottom of the screen. * * ![Snackbar * image](https://developer.android.com/images/reference/androidx/compose/material3/snackbar.png) * * Snackbars inform users of a process that an app has performed or will perform. They appear * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and * they don’t require user input to disappear. * * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional. * * Snackbars with an action should not timeout or self-dismiss until the user performs another * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a * page is not considered an action. * * This component provides only the visuals of the Snackbar. If you need to show a Snackbar with * defaults on the screen, use [SnackbarHostState.showSnackbar]: * * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar * * If you want to customize appearance of the Snackbar, you can pass your own version as a child of * the [SnackbarHost] to the [Scaffold]: * * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar * * For a multiline sample following the Material recommended spec of a maximum of 2 lines, see: * * @sample androidx.compose.material3.samples.ScaffoldWithMultilineSnackbar * @param modifier the [Modifier] to be applied to this snackbar * @param action action / button component to add as an action to the snackbar. Consider using * [ColorScheme.inversePrimary] as the color for the action, if you do not have a predefined color * you wish to use instead. * @param dismissAction action / button component to add as an additional close affordance action * when a snackbar is non self-dismissive. Consider using [ColorScheme.inverseOnSurface] as the * color for the action, if you do not have a predefined color you wish to use instead. * @param actionOnNewLine whether or not action should be put on a separate line. Recommended for * action with long action text. * @param shape defines the shape of this snackbar's container * @param containerColor the color used for the background of this snackbar. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this snackbar * @param actionContentColor the preferred content color for the optional [action] inside this * snackbar * @param dismissActionContentColor the preferred content color for the optional [dismissAction] * inside this snackbar * @param content content to show information about a process that an app has performed or will * perform */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun Snackbar( modifier: Modifier = Modifier, action: @Composable (() -> Unit)? = null, dismissAction: @Composable (() -> Unit)? = null, actionOnNewLine: Boolean = false, shape: Shape = SnackbarDefaults.shape, containerColor: Color = SnackbarDefaults.color, contentColor: Color = SnackbarDefaults.contentColor, actionContentColor: Color = SnackbarDefaults.actionContentColor, dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor, content: @Composable () -> Unit, ) { Surface( modifier = modifier, shape = shape, color = containerColor, contentColor = contentColor, shadowElevation = SnackbarTokens.ContainerElevation, ) { val textStyle = SnackbarTokens.SupportingTextFont.value val actionTextStyle = SnackbarTokens.ActionLabelTextFont.value CompositionLocalProvider(LocalTextStyle provides textStyle) { when { actionOnNewLine && action != null -> if (ComposeMaterial3Flags.isSnackbarStylingFixEnabled) { NewLineButtonSnackbar( text = content, action = action, dismissAction = dismissAction, actionTextStyle = actionTextStyle, actionContentColor = actionContentColor, dismissActionContentColor = dismissActionContentColor, ) } else { LegacyNewLineButtonSnackbar( text = content, action = action, dismissAction = dismissAction, actionTextStyle = actionTextStyle, actionContentColor = actionContentColor, dismissActionContentColor = dismissActionContentColor, ) } else -> if (ComposeMaterial3Flags.isSnackbarStylingFixEnabled) { OneRowSnackbar( text = content, action = action, dismissAction = dismissAction, actionTextStyle = actionTextStyle, actionTextColor = actionContentColor, dismissActionColor = dismissActionContentColor, ) } else { LegacyOneRowSnackbar( text = content, action = action, dismissAction = dismissAction, actionTextStyle = actionTextStyle, actionTextColor = actionContentColor, dismissActionColor = dismissActionContentColor, ) } } } } } /** * [Material Design snackbar](https://m3.material.io/components/snackbar/overview) * * Snackbars provide brief messages about app processes at the bottom of the screen. * * ![Snackbar * image](https://developer.android.com/images/reference/androidx/compose/material3/snackbar.png) * * Snackbars inform users of a process that an app has performed or will perform. They appear * temporarily, towards the bottom of the screen. They shouldn’t interrupt the user experience, and * they don’t require user input to disappear. * * A Snackbar can contain a single action. "Dismiss" or "cancel" actions are optional. * * Snackbars with an action should not timeout or self-dismiss until the user performs another * action. Here, moving the keyboard focus indicator to navigate through interactive elements in a * page is not considered an action. * * This version of snackbar is designed to work with [SnackbarData] provided by the [SnackbarHost], * which is usually used inside of the [Scaffold]. * * This components provides only the visuals of the Snackbar. If you need to show a Snackbar with * defaults on the screen, use [SnackbarHostState.showSnackbar]: * * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar * * If you want to customize appearance of the Snackbar, you can pass your own version as a child of * the [SnackbarHost] to the [Scaffold]: * * @sample androidx.compose.material3.samples.ScaffoldWithCustomSnackbar * * When a [SnackbarData.visuals] sets the Snackbar's duration as [SnackbarDuration.Indefinite], it's * recommended to display an additional close affordance action. See * [SnackbarVisuals.withDismissAction]: * * @sample androidx.compose.material3.samples.ScaffoldWithIndefiniteSnackbar * @param snackbarData data about the current snackbar showing via [SnackbarHostState] * @param modifier the [Modifier] to be applied to this snackbar * @param actionOnNewLine whether or not action should be put on a separate line. Recommended for * action with long action text. * @param shape defines the shape of this snackbar's container * @param containerColor the color used for the background of this snackbar. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this snackbar * @param actionColor the color of the snackbar's action * @param actionContentColor the preferred content color for the optional action inside this * snackbar. See [SnackbarVisuals.actionLabel]. * @param dismissActionContentColor the preferred content color for the optional dismiss action * inside this snackbar. See [SnackbarVisuals.withDismissAction]. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun Snackbar( snackbarData: SnackbarData, modifier: Modifier = Modifier, actionOnNewLine: Boolean = false, shape: Shape = SnackbarDefaults.shape, containerColor: Color = SnackbarDefaults.color, contentColor: Color = SnackbarDefaults.contentColor, actionColor: Color = SnackbarDefaults.actionColor, actionContentColor: Color = SnackbarDefaults.actionContentColor, dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor, ) { val actionLabel = snackbarData.visuals.actionLabel val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) { @Composable { TextButton( colors = ButtonDefaults.textButtonColors(contentColor = actionColor), onClick = { snackbarData.performAction() }, content = { Text(actionLabel) }, ) } } else { null } val dismissActionComposable: (@Composable () -> Unit)? = if (snackbarData.visuals.withDismissAction) { @Composable { val contentDescription = getString(Strings.SnackbarDismiss) TooltipBox( positionProvider = TooltipDefaults.rememberTooltipPositionProvider( TooltipAnchorPosition.Above ), tooltip = { PlainTooltip { Text(contentDescription) } }, state = rememberTooltipState(), ) { IconButton( onClick = { snackbarData.dismiss() }, content = { Icon(Icons.Filled.Close, contentDescription = contentDescription) }, ) } } } else { null } Snackbar( modifier = modifier.padding(12.dp), action = actionComposable, dismissAction = dismissActionComposable, actionOnNewLine = actionOnNewLine, shape = shape, containerColor = containerColor, contentColor = contentColor, actionContentColor = actionContentColor, dismissActionContentColor = dismissActionContentColor, content = { Text(snackbarData.visuals.message) }, ) } @Composable private fun NewLineButtonSnackbar( text: @Composable () -> Unit, action: @Composable () -> Unit, dismissAction: @Composable (() -> Unit)?, actionTextStyle: TextStyle, actionContentColor: Color, dismissActionContentColor: Color, ) { Column( modifier = Modifier.widthIn(max = ContainerMaxWidth) .fillMaxWidth() .padding(start = HorizontalSpacing) ) { Box( modifier = Modifier.fillMaxWidth() .padding(vertical = SnackbarVerticalPadding) .padding(end = HorizontalSpacing) ) { text() } Row( modifier = Modifier.align(Alignment.End) .padding( bottom = ActionButtonBottomPadding, end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp, ), verticalAlignment = Alignment.CenterVertically, ) { CompositionLocalProvider( LocalContentColor provides actionContentColor, LocalTextStyle provides actionTextStyle, content = action, ) if (dismissAction != null) { CompositionLocalProvider( LocalContentColor provides dismissActionContentColor, content = dismissAction, ) } } } } @Composable private fun LegacyNewLineButtonSnackbar( text: @Composable () -> Unit, action: @Composable () -> Unit, dismissAction: @Composable (() -> Unit)?, actionTextStyle: TextStyle, actionContentColor: Color, dismissActionContentColor: Color, ) { Column( modifier = Modifier // Fill max width, up to ContainerMaxWidth. .widthIn(max = ContainerMaxWidth) .fillMaxWidth() .padding(start = HorizontalSpacing, bottom = SeparateButtonExtraY) ) { Box( Modifier.paddingFromBaseline(HeightToFirstLine, LongButtonVerticalOffset) .padding(end = HorizontalSpacingButtonSide) ) { text() } Box( Modifier.align(Alignment.End) .padding(end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp) ) { Row { CompositionLocalProvider( LocalContentColor provides actionContentColor, LocalTextStyle provides actionTextStyle, content = action, ) if (dismissAction != null) { CompositionLocalProvider( LocalContentColor provides dismissActionContentColor, content = dismissAction, ) } } } } } @Composable private fun LegacyOneRowSnackbar( text: @Composable () -> Unit, action: @Composable (() -> Unit)?, dismissAction: @Composable (() -> Unit)?, actionTextStyle: TextStyle, actionTextColor: Color, dismissActionColor: Color, ) { val textTag = "text" val actionTag = "action" val dismissActionTag = "dismissAction" Layout( { Box(Modifier.layoutId(textTag).padding(vertical = LegacySnackbarVerticalPadding)) { text() } if (action != null) { Box(Modifier.layoutId(actionTag)) { CompositionLocalProvider( LocalContentColor provides actionTextColor, LocalTextStyle provides actionTextStyle, content = action, ) } } if (dismissAction != null) { Box(Modifier.layoutId(dismissActionTag)) { CompositionLocalProvider( LocalContentColor provides dismissActionColor, content = dismissAction, ) } } }, modifier = Modifier.padding( start = HorizontalSpacing, end = if (dismissAction == null) HorizontalSpacingButtonSide else 0.dp, ), ) { measurables, constraints -> val containerWidth = min(constraints.maxWidth, ContainerMaxWidth.roundToPx()) val actionButtonPlaceable = measurables.fastFirstOrNull { it.layoutId == actionTag }?.measure(constraints) val dismissButtonPlaceable = measurables.fastFirstOrNull { it.layoutId == dismissActionTag }?.measure(constraints) val actionButtonWidth = actionButtonPlaceable?.width ?: 0 val actionButtonHeight = actionButtonPlaceable?.height ?: 0 val dismissButtonWidth = dismissButtonPlaceable?.width ?: 0 val dismissButtonHeight = dismissButtonPlaceable?.height ?: 0 val extraSpacingWidth = if (dismissButtonWidth == 0) TextEndExtraSpacing.roundToPx() else 0 val textMaxWidth = (containerWidth - actionButtonWidth - dismissButtonWidth - extraSpacingWidth) .coerceAtLeast(constraints.minWidth) val textPlaceable = measurables .fastFirst { it.layoutId == textTag } .measure(constraints.copy(minHeight = 0, maxWidth = textMaxWidth)) val firstTextBaseline = textPlaceable[FirstBaseline] val lastTextBaseline = textPlaceable[LastBaseline] val hasText = firstTextBaseline != AlignmentLine.Unspecified && lastTextBaseline != AlignmentLine.Unspecified val isOneLine = firstTextBaseline == lastTextBaseline || !hasText val dismissButtonPlaceX = containerWidth - dismissButtonWidth val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth val textPlaceY: Int val containerHeight: Int val actionButtonPlaceY: Int if (isOneLine) { val minContainerHeight = SnackbarTokens.SingleLineContainerHeight.roundToPx() val contentHeight = max(actionButtonHeight, dismissButtonHeight) containerHeight = max(minContainerHeight, contentHeight) textPlaceY = (containerHeight - textPlaceable.height) / 2 actionButtonPlaceY = if (actionButtonPlaceable != null) { actionButtonPlaceable[FirstBaseline].let { if (it != AlignmentLine.Unspecified) { textPlaceY + firstTextBaseline - it } else { 0 } } } else { 0 } } else { val baselineOffset = HeightToFirstLine.roundToPx() textPlaceY = baselineOffset - firstTextBaseline val minContainerHeight = SnackbarTokens.TwoLinesContainerHeight.roundToPx() val contentHeight = textPlaceY + textPlaceable.height containerHeight = max(minContainerHeight, contentHeight) actionButtonPlaceY = if (actionButtonPlaceable != null) { (containerHeight - actionButtonPlaceable.height) / 2 } else { 0 } } val dismissButtonPlaceY = if (dismissButtonPlaceable != null) { (containerHeight - dismissButtonPlaceable.height) / 2 } else { 0 } layout(containerWidth, containerHeight) { textPlaceable.placeRelative(0, textPlaceY) actionButtonPlaceable?.placeRelative(actionButtonPlaceX, actionButtonPlaceY) dismissButtonPlaceable?.placeRelative(dismissButtonPlaceX, dismissButtonPlaceY) } } } @Composable private fun OneRowSnackbar( text: @Composable () -> Unit, action: @Composable (() -> Unit)?, dismissAction: @Composable (() -> Unit)?, actionTextStyle: TextStyle, actionTextColor: Color, dismissActionColor: Color, ) { val textTag = "text" val actionTag = "action" val dismissActionTag = "dismissAction" Layout( { Box(Modifier.layoutId(textTag).padding(vertical = SnackbarVerticalPadding)) { text() } if (action != null) { Box(Modifier.layoutId(actionTag)) { CompositionLocalProvider( LocalContentColor provides actionTextColor, LocalTextStyle provides actionTextStyle, content = action, ) } } if (dismissAction != null) { Box(Modifier.layoutId(dismissActionTag)) { CompositionLocalProvider( LocalContentColor provides dismissActionColor, content = dismissAction, ) } } }, modifier = Modifier.padding( start = HorizontalSpacing, end = if (dismissAction == null) TextEndExtraSpacing else 0.dp, ), ) { measurables, constraints -> val minContainerHeight = SnackbarTokens.SingleLineContainerHeight.roundToPx() val containerWidth = min(constraints.maxWidth, ContainerMaxWidth.roundToPx()) val actionButtonPlaceable = measurables.fastFirstOrNull { it.layoutId == actionTag }?.measure(constraints) val dismissButtonPlaceable = measurables.fastFirstOrNull { it.layoutId == dismissActionTag }?.measure(constraints) val actionButtonWidth = actionButtonPlaceable?.width ?: 0 val dismissButtonWidth = dismissButtonPlaceable?.width ?: 0 val extraSpacingWidth = if (dismissButtonWidth == 0) TextEndExtraSpacing.roundToPx() else 0 val textMaxWidth = (containerWidth - actionButtonWidth - dismissButtonWidth - extraSpacingWidth) .coerceAtLeast(constraints.minWidth) val textPlaceable = measurables .fastFirst { it.layoutId == textTag } .measure(constraints.copy(minHeight = 0, maxWidth = textMaxWidth)) val containerHeight = maxOf( minContainerHeight, textPlaceable.height, actionButtonPlaceable?.height ?: 0, dismissButtonPlaceable?.height ?: 0, ) val dismissButtonPlaceX = containerWidth - dismissButtonWidth val actionButtonPlaceX = dismissButtonPlaceX - actionButtonWidth layout(containerWidth, containerHeight) { textPlaceable.placeRelative(0, (containerHeight - textPlaceable.height) / 2) actionButtonPlaceable?.placeRelative( actionButtonPlaceX, (containerHeight - actionButtonPlaceable.height) / 2, ) dismissButtonPlaceable?.placeRelative( dismissButtonPlaceX, (containerHeight - dismissButtonPlaceable.height) / 2, ) } } } /** Contains the default values used for [Snackbar]. */ object SnackbarDefaults { /** Default shape of a snackbar. */ val shape: Shape @Composable get() = SnackbarTokens.ContainerShape.value /** Default color of a snackbar. */ val color: Color @Composable get() = SnackbarTokens.ContainerColor.value /** Default content color of a snackbar. */ val contentColor: Color @Composable get() = SnackbarTokens.SupportingTextColor.value /** Default action color of a snackbar. */ val actionColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.value /** Default action content color of a snackbar. */ val actionContentColor: Color @Composable get() = SnackbarTokens.ActionLabelTextColor.value /** Default dismiss action content color of a snackbar. */ val dismissActionContentColor: Color @Composable get() = SnackbarTokens.IconColor.value } private val ContainerMaxWidth = 600.dp private val HeightToFirstLine = 30.dp private val HorizontalSpacing = 16.dp private val HorizontalSpacingButtonSide = 8.dp private val SeparateButtonExtraY = 2.dp private val LegacySnackbarVerticalPadding = 6.dp private val TextEndExtraSpacing = 8.dp private val LongButtonVerticalOffset = 12.dp private val SnackbarVerticalPadding = 14.dp private val ActionButtonBottomPadding = 4.dp ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/FloatingActionButton.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition import androidx.compose.animation.expandHorizontally import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkHorizontally import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.HoverInteraction import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme.LocalMaterialTheme import androidx.compose.material3.internal.ProvideContentColorTextStyle import androidx.compose.material3.internal.animateElevation import androidx.compose.material3.tokens.ElevationTokens import androidx.compose.material3.tokens.ExtendedFabLargeTokens import androidx.compose.material3.tokens.ExtendedFabMediumTokens import androidx.compose.material3.tokens.ExtendedFabPrimaryTokens import androidx.compose.material3.tokens.ExtendedFabSmallTokens import androidx.compose.material3.tokens.FabBaselineTokens import androidx.compose.material3.tokens.FabLargeTokens import androidx.compose.material3.tokens.FabMediumTokens import androidx.compose.material3.tokens.FabPrimaryContainerTokens import androidx.compose.material3.tokens.FabSmallTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.TypographyKeyTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.State import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.CacheDrawModifierNode import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.drawscope.inset import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.layout import androidx.compose.ui.node.CompositionLocalConsumerModifierNode import androidx.compose.ui.node.DelegatingNode import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.currentValueOf import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.toIntSize import androidx.compose.ui.unit.toOffset import androidx.compose.ui.util.lerp import kotlin.math.roundToInt import kotlinx.coroutines.launch /** * [Material Design floating action * button](https://m3.material.io/components/floating-action-button/overview) * * The FAB represents the most important action on a screen. It puts key actions within reach. * * ![FAB image](https://developer.android.com/images/reference/androidx/compose/material3/fab.png) * * FAB typically contains an icon, for a FAB with text and an icon, see * [ExtendedFloatingActionButton]. * * @sample androidx.compose.material3.samples.FloatingActionButtonSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically an [Icon] */ @Composable fun FloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.shape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) = FloatingActionButton( onClick, ExtendedFabPrimaryTokens.LabelTextFont.value, FabBaselineTokens.ContainerWidth, FabBaselineTokens.ContainerHeight, modifier, shape, containerColor, contentColor, elevation, interactionSource, content, ) @Composable private fun FloatingActionButton( onClick: () -> Unit, textStyle: TextStyle, minWidth: Dp, minHeight: Dp, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.shape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } Surface( onClick = onClick, modifier = modifier.semantics { role = Role.Button }, shape = shape, color = containerColor, contentColor = contentColor, tonalElevation = elevation.tonalElevation(), shadowElevation = elevation.shadowElevation(interactionSource = interactionSource).value, interactionSource = interactionSource, ) { ProvideContentColorTextStyle(contentColor = contentColor, textStyle = textStyle) { Box( modifier = Modifier.defaultMinSize(minWidth = minWidth, minHeight = minHeight), contentAlignment = Alignment.Center, ) { content() } } } } /** * [Material Design small floating action * button](https://m3.material.io/components/floating-action-button/overview) * * The FAB represents the most important action on a screen. It puts key actions within reach. * * ![Small FAB * image](https://developer.android.com/images/reference/androidx/compose/material3/small-fab.png) * * @sample androidx.compose.material3.samples.SmallFloatingActionButtonSample * * FABs can also be shown and hidden with an animation when the main content is scrolled: * * @sample androidx.compose.material3.samples.AnimatedFloatingActionButtonSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically an [Icon] */ @Composable fun SmallFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.smallShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier.sizeIn( minWidth = FabSmallTokens.ContainerWidth, minHeight = FabSmallTokens.ContainerHeight, ), shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, content = content, ) } /** * [Material Design medium floating action * button](https://m3.material.io/components/floating-action-button/overview) * * The FAB represents the most important action on a screen. It puts key actions within reach. * * @sample androidx.compose.material3.samples.MediumFloatingActionButtonSample * * FABs can also be shown and hidden with an animation when the main content is scrolled: * * @sample androidx.compose.material3.samples.AnimatedFloatingActionButtonSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically an [Icon] */ @ExperimentalMaterial3ExpressiveApi @Composable fun MediumFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.mediumShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier.sizeIn( minWidth = FabMediumTokens.ContainerWidth, minHeight = FabMediumTokens.ContainerHeight, ), shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, content = content, ) } /** * [Material Design large floating action * button](https://m3.material.io/components/floating-action-button/overview) * * The FAB represents the most important action on a screen. It puts key actions within reach. * * ![Large FAB * image](https://developer.android.com/images/reference/androidx/compose/material3/large-fab.png) * * @sample androidx.compose.material3.samples.LargeFloatingActionButtonSample * * FABs can also be shown and hidden with an animation when the main content is scrolled: * * @sample androidx.compose.material3.samples.AnimatedFloatingActionButtonSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically an [Icon] */ @Composable fun LargeFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.largeShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable () -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier.sizeIn( minWidth = FabLargeTokens.ContainerWidth, minHeight = FabLargeTokens.ContainerHeight, ), shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, content = content, ) } // TODO link to image /** * [Material Design small extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * The other small extended floating action button overload supports a text label and icon. * * @sample androidx.compose.material3.samples.SmallExtendedFloatingActionButtonTextSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically a [Text] label */ @ExperimentalMaterial3ExpressiveApi @Composable fun SmallExtendedFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.smallExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { FloatingActionButton( onClick = onClick, textStyle = SmallExtendedFabTextStyle.value, minWidth = SmallExtendedFabMinimumWidth, minHeight = SmallExtendedFabMinimumHeight, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { Row( modifier = Modifier.padding( start = SmallExtendedFabPaddingStart, end = SmallExtendedFabPaddingEnd, ), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } // TODO link to image /** * [Material Design medium extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * The other medium extended floating action button overload supports a text label and icon. * * @sample androidx.compose.material3.samples.MediumExtendedFloatingActionButtonTextSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically a [Text] label */ @ExperimentalMaterial3ExpressiveApi @Composable fun MediumExtendedFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.mediumExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { FloatingActionButton( onClick = onClick, textStyle = MediumExtendedFabTextStyle.value, minWidth = MediumExtendedFabMinimumWidth, minHeight = MediumExtendedFabMinimumHeight, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { Row( modifier = Modifier.padding( start = MediumExtendedFabPaddingStart, end = MediumExtendedFabPaddingEnd, ), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } // TODO link to image /** * [Material Design large extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * The other large extended floating action button overload supports a text label and icon. * * @sample androidx.compose.material3.samples.LargeExtendedFloatingActionButtonTextSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically a [Text] label */ @ExperimentalMaterial3ExpressiveApi @Composable fun LargeExtendedFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.largeExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { FloatingActionButton( onClick = onClick, textStyle = LargeExtendedFabTextStyle.value, minWidth = LargeExtendedFabMinimumWidth, minHeight = LargeExtendedFabMinimumHeight, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { Row( modifier = Modifier.padding( start = LargeExtendedFabPaddingStart, end = LargeExtendedFabPaddingEnd, ), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } /** * [Material Design extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * ![Extended FAB * image](https://developer.android.com/images/reference/androidx/compose/material3/extended-fab.png) * * The other extended floating action button overload supports a text label and icon. * * @sample androidx.compose.material3.samples.ExtendedFloatingActionButtonTextSample * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this FAB, typically a [Text] label */ @Composable fun ExtendedFloatingActionButton( onClick: () -> Unit, modifier: Modifier = Modifier, shape: Shape = FloatingActionButtonDefaults.extendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { FloatingActionButton( onClick = onClick, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { Row( modifier = Modifier.sizeIn(minWidth = ExtendedFabMinimumWidth) .padding(horizontal = ExtendedFabTextPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } /** * [Material Design small extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * The other small extended floating action button overload is for FABs without an icon. * * Default content description for accessibility is extended from the extended fabs icon. For custom * behavior, you can provide your own via [Modifier.semantics]. * * @sample androidx.compose.material3.samples.SmallExtendedFloatingActionButtonSample * @sample androidx.compose.material3.samples.SmallAnimatedExtendedFloatingActionButtonSample * @param text label displayed inside this FAB * @param icon icon for this FAB, typically an [Icon] * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param expanded controls the expansion state of this FAB. In an expanded state, the FAB will show * both the [icon] and [text]. In a collapsed state, the FAB will show only the [icon]. * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. */ @ExperimentalMaterial3ExpressiveApi @Composable fun SmallExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, expanded: Boolean = true, shape: Shape = FloatingActionButtonDefaults.smallExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, ) = ExtendedFloatingActionButton( text = text, icon = icon, onClick = onClick, textStyle = SmallExtendedFabTextStyle.value, minWidth = SmallExtendedFabMinimumWidth, minHeight = SmallExtendedFabMinimumHeight, startPadding = SmallExtendedFabPaddingStart, endPadding = SmallExtendedFabPaddingEnd, iconPadding = SmallExtendedFabIconPadding, modifier = modifier, expanded = expanded, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) /** * [Material Design medium extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * The other medium extended floating action button overload is for FABs without an icon. * * Default content description for accessibility is extended from the extended fabs icon. For custom * behavior, you can provide your own via [Modifier.semantics]. * * @sample androidx.compose.material3.samples.MediumExtendedFloatingActionButtonSample * @sample androidx.compose.material3.samples.MediumAnimatedExtendedFloatingActionButtonSample * @param text label displayed inside this FAB * @param icon icon for this FAB, typically an [Icon] * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param expanded controls the expansion state of this FAB. In an expanded state, the FAB will show * both the [icon] and [text]. In a collapsed state, the FAB will show only the [icon]. * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. */ @ExperimentalMaterial3ExpressiveApi @Composable fun MediumExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, expanded: Boolean = true, shape: Shape = FloatingActionButtonDefaults.mediumExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, ) = ExtendedFloatingActionButton( text = text, icon = icon, onClick = onClick, textStyle = MediumExtendedFabTextStyle.value, minWidth = MediumExtendedFabMinimumWidth, minHeight = MediumExtendedFabMinimumHeight, startPadding = MediumExtendedFabPaddingStart, endPadding = MediumExtendedFabPaddingEnd, iconPadding = MediumExtendedFabIconPadding, modifier = modifier, expanded = expanded, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) /** * [Material Design large extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * The other large extended floating action button overload is for FABs without an icon. * * Default content description for accessibility is extended from the extended fabs icon. For custom * behavior, you can provide your own via [Modifier.semantics]. * * @sample androidx.compose.material3.samples.LargeExtendedFloatingActionButtonSample * @sample androidx.compose.material3.samples.LargeAnimatedExtendedFloatingActionButtonSample * @param text label displayed inside this FAB * @param icon icon for this FAB, typically an [Icon] * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param expanded controls the expansion state of this FAB. In an expanded state, the FAB will show * both the [icon] and [text]. In a collapsed state, the FAB will show only the [icon]. * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. */ @ExperimentalMaterial3ExpressiveApi @Composable fun LargeExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, expanded: Boolean = true, shape: Shape = FloatingActionButtonDefaults.largeExtendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, ) = ExtendedFloatingActionButton( text = text, icon = icon, onClick = onClick, textStyle = LargeExtendedFabTextStyle.value, minWidth = LargeExtendedFabMinimumWidth, minHeight = LargeExtendedFabMinimumHeight, startPadding = LargeExtendedFabPaddingStart, endPadding = LargeExtendedFabPaddingEnd, iconPadding = LargeExtendedFabIconPadding, modifier = modifier, expanded = expanded, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) /** * [Material Design extended floating action * button](https://m3.material.io/components/extended-fab/overview) * * Extended FABs help people take primary actions. They're wider than FABs to accommodate a text * label and larger target area. * * ![Extended FAB * image](https://developer.android.com/images/reference/androidx/compose/material3/extended-fab.png) * * The other extended floating action button overload is for FABs without an icon. * * Default content description for accessibility is extended from the extended fabs icon. For custom * behavior, you can provide your own via [Modifier.semantics]. * * @sample androidx.compose.material3.samples.ExtendedFloatingActionButtonSample * @sample androidx.compose.material3.samples.AnimatedExtendedFloatingActionButtonSample * @param text label displayed inside this FAB * @param icon icon for this FAB, typically an [Icon] * @param onClick called when this FAB is clicked * @param modifier the [Modifier] to be applied to this FAB * @param expanded controls the expansion state of this FAB. In an expanded state, the FAB will show * both the [icon] and [text]. In a collapsed state, the FAB will show only the [icon]. * @param shape defines the shape of this FAB's container and shadow (when using [elevation]) * @param containerColor the color used for the background of this FAB. Use [Color.Transparent] to * have no color. * @param contentColor the preferred color for content inside this FAB. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param elevation [FloatingActionButtonElevation] used to resolve the elevation for this FAB in * different states. This controls the size of the shadow below the FAB. Additionally, when the * container color is [ColorScheme.surface], this controls the amount of primary color applied as * an overlay. See also: [Surface]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this FAB. You can use this to change the FAB's appearance or * preview the FAB in different states. Note that if `null` is provided, interactions will still * happen internally. */ @Composable fun ExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier, expanded: Boolean = true, shape: Shape = FloatingActionButtonDefaults.extendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, ) { FloatingActionButton( onClick = onClick, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { val startPadding = if (expanded) ExtendedFabStartIconPadding else 0.dp val endPadding = if (expanded) ExtendedFabTextPadding else 0.dp Row( modifier = Modifier.sizeIn( minWidth = if (expanded) { ExtendedFabMinimumWidth } else { FabBaselineTokens.ContainerWidth } ) .padding(start = startPadding, end = endPadding), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = if (expanded) Arrangement.Start else Arrangement.Center, ) { icon() AnimatedVisibility( visible = expanded, enter = extendedFabExpandAnimation(), exit = extendedFabCollapseAnimation(), ) { Row(Modifier.clearAndSetSemantics {}) { Spacer(Modifier.width(ExtendedFabEndIconPadding)) text() } } } } } @Composable private fun ExtendedFloatingActionButton( text: @Composable () -> Unit, icon: @Composable () -> Unit, onClick: () -> Unit, textStyle: TextStyle, minWidth: Dp, minHeight: Dp, startPadding: Dp, endPadding: Dp, iconPadding: Dp, modifier: Modifier = Modifier, expanded: Boolean = true, shape: Shape = FloatingActionButtonDefaults.extendedFabShape, containerColor: Color = FloatingActionButtonDefaults.containerColor, contentColor: Color = contentColorFor(containerColor), elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(), interactionSource: MutableInteractionSource? = null, ) { FloatingActionButton( onClick = onClick, textStyle = textStyle, minWidth = Dp.Unspecified, minHeight = Dp.Unspecified, modifier = modifier, shape = shape, containerColor = containerColor, contentColor = contentColor, elevation = elevation, interactionSource = interactionSource, ) { val expandTransition = updateTransition(if (expanded) 1f else 0f, label = "expanded state") // TODO Load the motionScheme tokens from the component tokens file val sizeAnimationSpec = MotionSchemeKeyTokens.FastSpatial.value() val opacityAnimationSpec = MotionSchemeKeyTokens.FastEffects.value() val expandedWidthProgress = expandTransition.animateFloat(transitionSpec = { sizeAnimationSpec }) { it } val expandedAlphaProgress = expandTransition.animateFloat(transitionSpec = { opacityAnimationSpec }) { it } Row( modifier = Modifier.layout { measurable, constraints -> val expandedWidth = measurable.maxIntrinsicWidth(constraints.maxHeight) val width = lerp(minWidth.roundToPx(), expandedWidth, expandedWidthProgress.value) val placeable = measurable.measure(constraints) layout(width, placeable.height) { placeable.place(0, 0) } } .sizeIn(minWidth = minWidth, minHeight = minHeight) .padding(start = startPadding, end = endPadding), verticalAlignment = Alignment.CenterVertically, ) { icon() val fullyCollapsed = remember(expandTransition) { derivedStateOf { expandTransition.currentState == 0f && !expandTransition.isRunning } } if (!fullyCollapsed.value) { Row( Modifier.clearAndSetSemantics {} .graphicsLayer { alpha = expandedAlphaProgress.value } ) { Spacer(Modifier.width(iconPadding)) text() } } } } } /** Contains the default values used by [FloatingActionButton] */ object FloatingActionButtonDefaults { internal val ShowHideTargetScale = 0.2f /** The recommended size of the icon inside a [MediumFloatingActionButton]. */ @ExperimentalMaterial3ExpressiveApi val MediumIconSize = FabMediumTokens.IconSize /** The recommended size of the icon inside a [LargeFloatingActionButton]. */ val LargeIconSize = 36.dp // TODO: FabLargeTokens.IconSize is incorrect /** Default shape for a floating action button. */ val shape: Shape @Composable get() = FabBaselineTokens.ContainerShape.value /** Default shape for a small floating action button. */ val smallShape: Shape @Composable get() = FabSmallTokens.ContainerShape.value /** Default shape for a medium floating action button. */ @ExperimentalMaterial3ExpressiveApi val mediumShape: Shape @Composable get() = ShapeDefaults.LargeIncreased // TODO: update to use token /** Default shape for a large floating action button. */ val largeShape: Shape @Composable get() = FabLargeTokens.ContainerShape.value /** Default shape for an extended floating action button. */ val extendedFabShape: Shape @Composable get() = ExtendedFabPrimaryTokens.ContainerShape.value /** Default shape for a small extended floating action button. */ @ExperimentalMaterial3ExpressiveApi val smallExtendedFabShape: Shape @Composable get() = ExtendedFabSmallTokens.ContainerShape.value /** Default shape for a medium extended floating action button. */ @ExperimentalMaterial3ExpressiveApi val mediumExtendedFabShape: Shape @Composable get() = ShapeDefaults.LargeIncreased // TODO: update to use token /** Default shape for a large extended floating action button. */ @ExperimentalMaterial3ExpressiveApi val largeExtendedFabShape: Shape @Composable get() = ExtendedFabLargeTokens.ContainerShape.value /** Default container color for a floating action button. */ val containerColor: Color @Composable get() = FabPrimaryContainerTokens.ContainerColor.value /** * Creates a [FloatingActionButtonElevation] that represents the elevation of a * [FloatingActionButton] in different states. For use cases in which a less prominent * [FloatingActionButton] is possible consider the [loweredElevation]. * * @param defaultElevation the elevation used when the [FloatingActionButton] has no other * [Interaction]s. * @param pressedElevation the elevation used when the [FloatingActionButton] is pressed. * @param focusedElevation the elevation used when the [FloatingActionButton] is focused. * @param hoveredElevation the elevation used when the [FloatingActionButton] is hovered. */ @Composable fun elevation( defaultElevation: Dp = FabPrimaryContainerTokens.ContainerElevation, pressedElevation: Dp = FabPrimaryContainerTokens.PressedContainerElevation, focusedElevation: Dp = FabPrimaryContainerTokens.FocusedContainerElevation, hoveredElevation: Dp = FabPrimaryContainerTokens.HoveredContainerElevation, ): FloatingActionButtonElevation = FloatingActionButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, ) /** * Use this to create a [FloatingActionButton] with a lowered elevation for less emphasis. Use * [elevation] to get a normal [FloatingActionButton]. * * @param defaultElevation the elevation used when the [FloatingActionButton] has no other * [Interaction]s. * @param pressedElevation the elevation used when the [FloatingActionButton] is pressed. * @param focusedElevation the elevation used when the [FloatingActionButton] is focused. * @param hoveredElevation the elevation used when the [FloatingActionButton] is hovered. */ @Composable fun loweredElevation( defaultElevation: Dp = ElevationTokens.Level1, pressedElevation: Dp = ElevationTokens.Level1, focusedElevation: Dp = ElevationTokens.Level1, hoveredElevation: Dp = ElevationTokens.Level2, ): FloatingActionButtonElevation = FloatingActionButtonElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, focusedElevation = focusedElevation, hoveredElevation = hoveredElevation, ) /** * Use this to create a [FloatingActionButton] that represents the default elevation of a * [FloatingActionButton] used for [BottomAppBar] in different states. * * @param defaultElevation the elevation used when the [FloatingActionButton] has no other * [Interaction]s. * @param pressedElevation the elevation used when the [FloatingActionButton] is pressed. * @param focusedElevation the elevation used when the [FloatingActionButton] is focused. * @param hoveredElevation the elevation used when the [FloatingActionButton] is hovered. */ fun bottomAppBarFabElevation( defaultElevation: Dp = 0.dp, pressedElevation: Dp = 0.dp, focusedElevation: Dp = 0.dp, hoveredElevation: Dp = 0.dp, ): FloatingActionButtonElevation = FloatingActionButtonElevation( defaultElevation, pressedElevation, focusedElevation, hoveredElevation, ) } /** * Apply this modifier to a [FloatingActionButton] to show or hide it with an animation, typically * based on the app's main content scrolling. * * @param visible whether the FAB should be shown or hidden with an animation * @param alignment the direction towards which the FAB should be scaled to and from * @param targetScale the initial scale value when showing the FAB and the final scale value when * hiding the FAB * @param scaleAnimationSpec the [AnimationSpec] to use for the scale part of the animation, if null * the Fast Spatial spring spec from the [MotionScheme] will be used * @param alphaAnimationSpec the [AnimationSpec] to use for the alpha part of the animation, if null * the Fast Effects spring spec from the [MotionScheme] will be used * @sample androidx.compose.material3.samples.AnimatedFloatingActionButtonSample */ @ExperimentalMaterial3ExpressiveApi fun Modifier.animateFloatingActionButton( visible: Boolean, alignment: Alignment, targetScale: Float = FloatingActionButtonDefaults.ShowHideTargetScale, scaleAnimationSpec: AnimationSpec? = null, alphaAnimationSpec: AnimationSpec? = null, ): Modifier { return this.then( FabVisibleModifier( visible = visible, alignment = alignment, targetScale = targetScale, scaleAnimationSpec = scaleAnimationSpec, alphaAnimationSpec = alphaAnimationSpec, ) ) } internal data class FabVisibleModifier( private val visible: Boolean, private val alignment: Alignment, private val targetScale: Float, private val scaleAnimationSpec: AnimationSpec? = null, private val alphaAnimationSpec: AnimationSpec? = null, ) : ModifierNodeElement() { override fun create(): FabVisibleNode = FabVisibleNode( visible = visible, alignment = alignment, targetScale = targetScale, scaleAnimationSpec = scaleAnimationSpec, alphaAnimationSpec = alphaAnimationSpec, ) override fun update(node: FabVisibleNode) { node.updateNode( visible = visible, alignment = alignment, targetScale = targetScale, scaleAnimationSpec = scaleAnimationSpec, alphaAnimationSpec = alphaAnimationSpec, ) } override fun InspectorInfo.inspectableProperties() { // Show nothing in the inspector. } } internal class FabVisibleNode( visible: Boolean, private var alignment: Alignment, private var targetScale: Float, private var scaleAnimationSpec: AnimationSpec? = null, private var alphaAnimationSpec: AnimationSpec? = null, ) : DelegatingNode(), LayoutModifierNode, CompositionLocalConsumerModifierNode { private val scaleAnimatable = Animatable(if (visible) 1f else 0f) private val alphaAnimatable = Animatable(if (visible) 1f else 0f) init { delegate( CacheDrawModifierNode { val layer = obtainGraphicsLayer() // Use a larger layer size to make sure the elevation shadow doesn't get clipped // and offset via layer.topLeft and DrawScope.inset to preserve the visual // position of the FAB. val layerInsetSize = 16.dp.toPx() val layerSize = Size(size.width + layerInsetSize * 2f, size.height + layerInsetSize * 2f) .toIntSize() val nodeSize = size.toIntSize() layer.apply { topLeft = IntOffset(-layerInsetSize.roundToInt(), -layerInsetSize.roundToInt()) alpha = alphaAnimatable.value // Scale towards the direction of the provided alignment val alignOffset = alignment.align(IntSize(1, 1), nodeSize, layoutDirection) pivotOffset = alignOffset.toOffset() + Offset(layerInsetSize, layerInsetSize) scaleX = lerp(targetScale, 1f, scaleAnimatable.value) scaleY = lerp(targetScale, 1f, scaleAnimatable.value) record(size = layerSize) { inset(layerInsetSize, layerInsetSize) { this@record.drawContent() } } } onDrawWithContent { drawLayer(layer) } } ) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun updateNode( visible: Boolean, alignment: Alignment, targetScale: Float, scaleAnimationSpec: AnimationSpec?, alphaAnimationSpec: AnimationSpec?, ) { this.alignment = alignment this.targetScale = targetScale this.scaleAnimationSpec = scaleAnimationSpec this.alphaAnimationSpec = alphaAnimationSpec coroutineScope.launch { // TODO Load the motionScheme tokens from the component tokens file scaleAnimatable.animateTo( targetValue = if (visible) 1f else 0f, animationSpec = scaleAnimationSpec ?: currentValueOf(LocalMaterialTheme).motionScheme.fastSpatialSpec(), ) } coroutineScope.launch { // TODO Load the motionScheme tokens from the component tokens file alphaAnimatable.animateTo( targetValue = if (visible) 1f else 0f, animationSpec = alphaAnimationSpec ?: currentValueOf(LocalMaterialTheme).motionScheme.fastEffectsSpec(), ) } } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { if (alphaAnimatable.value == 0f) { return layout(0, 0) {} } val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { placeable.place(0, 0) } } } /** * Represents the tonal and shadow elevation for a floating action button in different states. * * See [FloatingActionButtonDefaults.elevation] for the default elevation used in a * [FloatingActionButton] and [ExtendedFloatingActionButton]. */ @Stable open class FloatingActionButtonElevation internal constructor( private val defaultElevation: Dp, private val pressedElevation: Dp, private val focusedElevation: Dp, private val hoveredElevation: Dp, ) { @Composable internal fun shadowElevation(interactionSource: InteractionSource): State { return animateElevation(interactionSource = interactionSource) } internal fun tonalElevation(): Dp { return defaultElevation } @Composable private fun animateElevation(interactionSource: InteractionSource): State { val animatable = remember(interactionSource) { FloatingActionButtonElevationAnimatable( defaultElevation = defaultElevation, pressedElevation = pressedElevation, hoveredElevation = hoveredElevation, focusedElevation = focusedElevation, ) } LaunchedEffect(this) { animatable.updateElevation( defaultElevation = defaultElevation, pressedElevation = pressedElevation, hoveredElevation = hoveredElevation, focusedElevation = focusedElevation, ) } LaunchedEffect(interactionSource) { val interactions = mutableListOf() interactionSource.interactions.collect { interaction -> when (interaction) { is HoverInteraction.Enter -> { interactions.add(interaction) } is HoverInteraction.Exit -> { interactions.remove(interaction.enter) } is FocusInteraction.Focus -> { interactions.add(interaction) } is FocusInteraction.Unfocus -> { interactions.remove(interaction.focus) } is PressInteraction.Press -> { interactions.add(interaction) } is PressInteraction.Release -> { interactions.remove(interaction.press) } is PressInteraction.Cancel -> { interactions.remove(interaction.press) } } val targetInteraction = interactions.lastOrNull() launch { animatable.animateElevation(to = targetInteraction) } } } return animatable.asState() } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is FloatingActionButtonElevation) return false if (defaultElevation != other.defaultElevation) return false if (pressedElevation != other.pressedElevation) return false if (focusedElevation != other.focusedElevation) return false return hoveredElevation == other.hoveredElevation } override fun hashCode(): Int { var result = defaultElevation.hashCode() result = 31 * result + pressedElevation.hashCode() result = 31 * result + focusedElevation.hashCode() result = 31 * result + hoveredElevation.hashCode() return result } } private class FloatingActionButtonElevationAnimatable( private var defaultElevation: Dp, private var pressedElevation: Dp, private var hoveredElevation: Dp, private var focusedElevation: Dp, ) { private val animatable = Animatable(defaultElevation, Dp.VectorConverter) private var lastTargetInteraction: Interaction? = null private var targetInteraction: Interaction? = null private fun Interaction?.calculateTarget(): Dp { return when (this) { is PressInteraction.Press -> pressedElevation is HoverInteraction.Enter -> hoveredElevation is FocusInteraction.Focus -> focusedElevation else -> defaultElevation } } suspend fun updateElevation( defaultElevation: Dp, pressedElevation: Dp, hoveredElevation: Dp, focusedElevation: Dp, ) { this.defaultElevation = defaultElevation this.pressedElevation = pressedElevation this.hoveredElevation = hoveredElevation this.focusedElevation = focusedElevation snapElevation() } private suspend fun snapElevation() { val target = targetInteraction.calculateTarget() if (animatable.targetValue != target) { try { animatable.snapTo(target) } finally { lastTargetInteraction = targetInteraction } } } suspend fun animateElevation(to: Interaction?) { val target = to.calculateTarget() // Update the interaction even if the values are the same, for when we change to another // interaction later targetInteraction = to try { if (animatable.targetValue != target) { animatable.animateElevation(target = target, from = lastTargetInteraction, to = to) } } finally { lastTargetInteraction = to } } fun asState(): State = animatable.asState() } private val SmallExtendedFabMinimumWidth = ExtendedFabSmallTokens.ContainerHeight private val SmallExtendedFabMinimumHeight = ExtendedFabSmallTokens.ContainerHeight private val SmallExtendedFabPaddingStart = ExtendedFabSmallTokens.LeadingSpace private val SmallExtendedFabPaddingEnd = ExtendedFabSmallTokens.TrailingSpace private val SmallExtendedFabIconPadding = ExtendedFabSmallTokens.IconLabelSpace private val SmallExtendedFabTextStyle = TypographyKeyTokens.TitleMedium private val MediumExtendedFabMinimumWidth = ExtendedFabMediumTokens.ContainerHeight private val MediumExtendedFabMinimumHeight = ExtendedFabMediumTokens.ContainerHeight private val MediumExtendedFabPaddingStart = ExtendedFabMediumTokens.LeadingSpace private val MediumExtendedFabPaddingEnd = ExtendedFabMediumTokens.TrailingSpace // TODO: ExtendedFabMediumTokens.IconLabelSpace is incorrect private val MediumExtendedFabIconPadding = 12.dp private val MediumExtendedFabTextStyle = TypographyKeyTokens.TitleLarge private val LargeExtendedFabMinimumWidth = ExtendedFabLargeTokens.ContainerHeight private val LargeExtendedFabMinimumHeight = ExtendedFabLargeTokens.ContainerHeight private val LargeExtendedFabPaddingStart = ExtendedFabLargeTokens.LeadingSpace private val LargeExtendedFabPaddingEnd = ExtendedFabLargeTokens.TrailingSpace // TODO: ExtendedFabLargeTokens.IconLabelSpace is incorrect private val LargeExtendedFabIconPadding = 16.dp private val LargeExtendedFabTextStyle = TypographyKeyTokens.HeadlineSmall private val ExtendedFabStartIconPadding = 16.dp private val ExtendedFabEndIconPadding = 12.dp private val ExtendedFabTextPadding = 20.dp private val ExtendedFabMinimumWidth = 80.dp @Composable private fun extendedFabCollapseAnimation() = fadeOut( // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.FastEffects.value() ) + shrinkHorizontally( animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value(), shrinkTowards = Alignment.Start, ) @Composable private fun extendedFabExpandAnimation() = fadeIn( // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.DefaultEffects.value() ) + expandHorizontally( animationSpec = MotionSchemeKeyTokens.FastSpatial.value(), expandFrom = Alignment.Start, ) ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Checkbox.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.snap import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.Canvas import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.triStateToggleable import androidx.compose.material3.tokens.CheckboxTokens import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.PathMeasure import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import kotlin.math.floor import kotlin.math.max /** * [Material Design checkbox](https://m3.material.io/components/checkbox/overview) * * Checkboxes allow users to select one or more items from a set. Checkboxes can turn an option on * or off. * * ![Checkbox * image](https://developer.android.com/images/reference/androidx/compose/material3/checkbox.png) * * Simple Checkbox sample: * * @sample androidx.compose.material3.samples.CheckboxSample * * Combined Checkbox with Text sample: * * @sample androidx.compose.material3.samples.CheckboxWithTextSample * @param checked whether this checkbox is checked or unchecked * @param onCheckedChange called when this checkbox is clicked. If `null`, then this checkbox will * not be interactable, unless something else handles its input events and updates its state. * @param modifier the [Modifier] to be applied to this checkbox * @param enabled controls the enabled state of this checkbox. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [CheckboxColors] that will be used to resolve the colors used for this checkbox in * different states. See [CheckboxDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this checkbox. You can use this to change the checkbox's appearance * or preview the checkbox in different states. Note that if `null` is provided, interactions will * still happen internally. * @see [TriStateCheckbox] if you require support for an indeterminate state. */ @Composable fun Checkbox( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, colors: CheckboxColors = CheckboxDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { val strokeWidthPx = with(LocalDensity.current) { floor(CheckboxDefaults.StrokeWidth.toPx()) } TriStateCheckbox( state = ToggleableState(checked), onClick = if (onCheckedChange != null) { { onCheckedChange(!checked) } } else { null }, checkmarkStroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Square), outlineStroke = Stroke(width = strokeWidthPx), modifier = modifier, enabled = enabled, colors = colors, interactionSource = interactionSource, ) } /** * [Material Design checkbox](https://m3.material.io/components/checkbox/overview) * * Checkboxes allow users to select one or more items from a set. Checkboxes can turn an option on * or off. * * ![Checkbox * image](https://developer.android.com/images/reference/androidx/compose/material3/checkbox.png) * * This Checkbox function offers greater flexibility in visual customization. Using the [Stroke] * parameters, you can control the appearance of both the checkmark and the box that surrounds it. * * A sample of a `Checkbox` that uses a [Stroke] with rounded [StrokeCap] and * [androidx.compose.ui.graphics.StrokeJoin]: * * @sample androidx.compose.material3.samples.CheckboxRoundedStrokesSample * @param checked whether this checkbox is checked or unchecked * @param onCheckedChange called when this checkbox is clicked. If `null`, then this checkbox will * not be interactable, unless something else handles its input events and updates its state. * @param checkmarkStroke stroke for the checkmark. * @param outlineStroke stroke for the checkmark's box outline. Note that this stroke is applied * when drawing the outline's rounded rectangle, so attributions such as * [androidx.compose.ui.graphics.StrokeJoin] will be ignored. * @param modifier the [Modifier] to be applied to this checkbox * @param enabled controls the enabled state of this checkbox. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [CheckboxColors] that will be used to resolve the colors used for this checkbox in * different states. See [CheckboxDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this checkbox. You can use this to change the checkbox's appearance * or preview the checkbox in different states. Note that if `null` is provided, interactions will * still happen internally. * @see [TriStateCheckbox] if you require support for an indeterminate state. */ @Composable fun Checkbox( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, checkmarkStroke: Stroke, outlineStroke: Stroke, modifier: Modifier = Modifier, enabled: Boolean = true, colors: CheckboxColors = CheckboxDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { TriStateCheckbox( state = ToggleableState(checked), onClick = if (onCheckedChange != null) { { onCheckedChange(!checked) } } else { null }, checkmarkStroke = checkmarkStroke, outlineStroke = outlineStroke, modifier = modifier, enabled = enabled, colors = colors, interactionSource = interactionSource, ) } /** * [Material Design checkbox](https://m3.material.io/components/checkbox/guidelines) * * Checkboxes can have a parent-child relationship with other checkboxes. When the parent checkbox * is checked, all child checkboxes are checked. If a parent checkbox is unchecked, all child * checkboxes are unchecked. If some, but not all, child checkboxes are checked, the parent checkbox * becomes an indeterminate checkbox. * * ![Checkbox * image](https://developer.android.com/images/reference/androidx/compose/material3/indeterminate-checkbox.png) * * @sample androidx.compose.material3.samples.TriStateCheckboxSample * @param state whether this checkbox is checked, unchecked, or in an indeterminate state * @param onClick called when this checkbox is clicked. If `null`, then this checkbox will not be * interactable, unless something else handles its input events and updates its [state]. * @param modifier the [Modifier] to be applied to this checkbox * @param enabled controls the enabled state of this checkbox. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [CheckboxColors] that will be used to resolve the colors used for this checkbox in * different states. See [CheckboxDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this checkbox. You can use this to change the checkbox's appearance * or preview the checkbox in different states. Note that if `null` is provided, interactions will * still happen internally. * @see [Checkbox] if you want a simple component that represents Boolean state */ @Composable fun TriStateCheckbox( state: ToggleableState, onClick: (() -> Unit)?, modifier: Modifier = Modifier, enabled: Boolean = true, colors: CheckboxColors = CheckboxDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { val strokeWidthPx = with(LocalDensity.current) { floor(CheckboxDefaults.StrokeWidth.toPx()) } TriStateCheckbox( state = state, onClick = onClick, checkmarkStroke = Stroke(width = strokeWidthPx, cap = StrokeCap.Square), outlineStroke = Stroke(width = strokeWidthPx), modifier = modifier, enabled = enabled, colors = colors, interactionSource = interactionSource, ) } /** * [Material Design checkbox](https://m3.material.io/components/checkbox/guidelines) * * Checkboxes can have a parent-child relationship with other checkboxes. When the parent checkbox * is checked, all child checkboxes are checked. If a parent checkbox is unchecked, all child * checkboxes are unchecked. If some, but not all, child checkboxes are checked, the parent checkbox * becomes an indeterminate checkbox. * * ![Checkbox * image](https://developer.android.com/images/reference/androidx/compose/material3/indeterminate-checkbox.png) * * This Checkbox function offers greater flexibility in visual customization. Using the [Stroke] * parameters, you can control the appearance of both the checkmark and the box that surrounds it. * * A sample of a `TriStateCheckbox` that uses a [Stroke] with rounded [StrokeCap] and * [androidx.compose.ui.graphics.StrokeJoin]: * * @sample androidx.compose.material3.samples.TriStateCheckboxRoundedStrokesSample * @param state whether this checkbox is checked, unchecked, or in an indeterminate state * @param onClick called when this checkbox is clicked. If `null`, then this checkbox will not be * interactable, unless something else handles its input events and updates its [state]. * @param checkmarkStroke stroke for the checkmark. * @param outlineStroke stroke for the checkmark's box outline. Note that this stroke is applied * when drawing the outline's rounded rectangle, so attributions such as * [androidx.compose.ui.graphics.StrokeJoin] will be ignored. * @param modifier the [Modifier] to be applied to this checkbox * @param enabled controls the enabled state of this checkbox. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [CheckboxColors] that will be used to resolve the colors used for this checkbox in * different states. See [CheckboxDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this checkbox. You can use this to change the checkbox's appearance * or preview the checkbox in different states. Note that if `null` is provided, interactions will * still happen internally. * @see [Checkbox] if you want a simple component that represents Boolean state */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun TriStateCheckbox( state: ToggleableState, onClick: (() -> Unit)?, checkmarkStroke: Stroke, outlineStroke: Stroke, modifier: Modifier = Modifier, enabled: Boolean = true, colors: CheckboxColors = CheckboxDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { val isCheckboxStylingFixEnabled = ComposeMaterial3Flags.isCheckboxStylingFixEnabled val indication = if (isCheckboxStylingFixEnabled) ripple( bounded = false, radius = CheckboxTokens.StateLayerSize / 2, color = colors.indicatorColor(state), ) else { ripple(bounded = false, radius = CheckboxTokens.StateLayerSize / 2) } val toggleableModifier = if (onClick != null) { Modifier.triStateToggleable( state = state, onClick = onClick, enabled = enabled, role = Role.Checkbox, interactionSource = interactionSource, indication = indication, ) } else { Modifier } CheckboxImpl( enabled = enabled, value = state, modifier = modifier .then( if (onClick != null) { Modifier.minimumInteractiveComponentSize() } else { Modifier } ) .then(toggleableModifier) .then( if (isCheckboxStylingFixEnabled) { Modifier } else { Modifier.padding(CheckboxDefaultPadding) } ), colors = colors, checkmarkStroke = checkmarkStroke, outlineStroke = outlineStroke, ) } /** Defaults used in [Checkbox] and [TriStateCheckbox]. */ object CheckboxDefaults { /** * Creates a [CheckboxColors] that will animate between the provided colors according to the * Material specification. */ @Composable fun colors() = MaterialTheme.colorScheme.defaultCheckboxColors /** * Creates a [CheckboxColors] that will animate between the provided colors according to the * Material specification. * * @param checkedColor the color that will be used for the border and box when checked * @param uncheckedColor color that will be used for the border when unchecked. By default, the * inner box is transparent when unchecked. * @param checkmarkColor color that will be used for the checkmark when checked * @param disabledCheckedColor color that will be used for the box and border when disabled and * checked * @param disabledUncheckedColor color that will be used for the border when disabled and * unchecked. By default, the inner box is transparent when unchecked. * @param disabledIndeterminateColor color that will be used for the box and border in a * [TriStateCheckbox] when disabled AND in an [ToggleableState.Indeterminate] state */ @Composable fun colors( checkedColor: Color = Color.Unspecified, uncheckedColor: Color = Color.Unspecified, checkmarkColor: Color = Color.Unspecified, disabledCheckedColor: Color = Color.Unspecified, disabledUncheckedColor: Color = Color.Unspecified, disabledIndeterminateColor: Color = Color.Unspecified, ): CheckboxColors = MaterialTheme.colorScheme.defaultCheckboxColors.copy( checkedCheckmarkColor = checkmarkColor, uncheckedCheckmarkColor = Color.Transparent, disabledCheckmarkColor = checkmarkColor, checkedBoxColor = checkedColor, uncheckedBoxColor = Color.Transparent, disabledCheckedBoxColor = disabledCheckedColor, disabledUncheckedBoxColor = Color.Transparent, disabledIndeterminateBoxColor = disabledIndeterminateColor, checkedBorderColor = checkedColor, uncheckedBorderColor = uncheckedColor, disabledBorderColor = disabledCheckedColor, disabledUncheckedBorderColor = disabledUncheckedColor, disabledIndeterminateBorderColor = disabledIndeterminateColor, ) /** * Creates a [CheckboxColors] that will animate between the provided colors according to the * Material specification. * * @param checkedCheckmarkColor color that will be used for the checkmark when checked * @param uncheckedCheckmarkColor color that will be used for the checkmark when unchecked * @param disabledCheckmarkColor color that will be used for the checkmark when disabled * @param checkedBoxColor the color that will be used for the box when checked * @param uncheckedBoxColor color that will be used for the box when unchecked * @param disabledCheckedBoxColor color that will be used for the box when disabled and checked * @param disabledUncheckedBoxColor color that will be used for the box when disabled and * unchecked * @param disabledIndeterminateBoxColor color that will be used for the box and border in a * [TriStateCheckbox] when disabled AND in an [ToggleableState.Indeterminate] state. * @param checkedBorderColor color that will be used for the border when checked * @param uncheckedBorderColor color that will be used for the border when unchecked * @param disabledBorderColor color that will be used for the border when disabled and checked * @param disabledUncheckedBorderColor color that will be used for the border when disabled and * unchecked * @param disabledIndeterminateBorderColor color that will be used for the border when disabled * and in an [ToggleableState.Indeterminate] state. */ @Composable fun colors( checkedCheckmarkColor: Color = Color.Unspecified, uncheckedCheckmarkColor: Color = Color.Unspecified, disabledCheckmarkColor: Color = Color.Unspecified, checkedBoxColor: Color = Color.Unspecified, uncheckedBoxColor: Color = Color.Unspecified, disabledCheckedBoxColor: Color = Color.Unspecified, disabledUncheckedBoxColor: Color = Color.Unspecified, disabledIndeterminateBoxColor: Color = Color.Unspecified, checkedBorderColor: Color = Color.Unspecified, uncheckedBorderColor: Color = Color.Unspecified, disabledBorderColor: Color = Color.Unspecified, disabledUncheckedBorderColor: Color = Color.Unspecified, disabledIndeterminateBorderColor: Color = Color.Unspecified, ): CheckboxColors = MaterialTheme.colorScheme.defaultCheckboxColors.copy( checkedCheckmarkColor = checkedCheckmarkColor, uncheckedCheckmarkColor = uncheckedCheckmarkColor, disabledCheckmarkColor = disabledCheckmarkColor, checkedBoxColor = checkedBoxColor, uncheckedBoxColor = uncheckedBoxColor, disabledCheckedBoxColor = disabledCheckedBoxColor, disabledUncheckedBoxColor = disabledUncheckedBoxColor, disabledIndeterminateBoxColor = disabledIndeterminateBoxColor, checkedBorderColor = checkedBorderColor, uncheckedBorderColor = uncheckedBorderColor, disabledBorderColor = disabledBorderColor, disabledUncheckedBorderColor = disabledUncheckedBorderColor, disabledIndeterminateBorderColor = disabledIndeterminateBorderColor, ) internal val ColorScheme.defaultCheckboxColors: CheckboxColors get() { return defaultCheckboxColorsCached ?: CheckboxColors( checkedCheckmarkColor = fromToken(CheckboxTokens.SelectedIconColor), uncheckedCheckmarkColor = Color.Transparent, disabledCheckmarkColor = fromToken(CheckboxTokens.SelectedDisabledIconColor), checkedBoxColor = fromToken(CheckboxTokens.SelectedContainerColor), uncheckedBoxColor = Color.Transparent, disabledCheckedBoxColor = fromToken(CheckboxTokens.SelectedDisabledContainerColor) .copy(alpha = CheckboxTokens.SelectedDisabledContainerOpacity), disabledUncheckedBoxColor = Color.Transparent, disabledIndeterminateBoxColor = fromToken(CheckboxTokens.SelectedDisabledContainerColor) .copy(alpha = CheckboxTokens.SelectedDisabledContainerOpacity), checkedBorderColor = fromToken(CheckboxTokens.SelectedContainerColor), uncheckedBorderColor = fromToken(CheckboxTokens.UnselectedOutlineColor), disabledBorderColor = fromToken(CheckboxTokens.SelectedDisabledContainerColor) .copy(alpha = CheckboxTokens.SelectedDisabledContainerOpacity), disabledUncheckedBorderColor = fromToken(CheckboxTokens.UnselectedDisabledOutlineColor) .copy(alpha = CheckboxTokens.UnselectedDisabledContainerOpacity), disabledIndeterminateBorderColor = fromToken(CheckboxTokens.SelectedDisabledContainerColor) .copy(alpha = CheckboxTokens.SelectedDisabledContainerOpacity), ) .also { defaultCheckboxColorsCached = it } } /** * The default stroke width for a [Checkbox]. This width will be used for the checkmark when the * `Checkbox` is in a checked or indeterminate states, or for the outline when it's unchecked. */ val StrokeWidth = 2.dp } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun CheckboxImpl( enabled: Boolean, value: ToggleableState, modifier: Modifier, colors: CheckboxColors, checkmarkStroke: Stroke, outlineStroke: Stroke, ) { val isCheckboxStylingFixEnabled = ComposeMaterial3Flags.isCheckboxStylingFixEnabled val transition = updateTransition(value) val defaultAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val checkDrawFraction = transition.animateFloat( transitionSpec = { when { // TODO Load the motionScheme tokens from the component tokens file initialState == ToggleableState.Off -> defaultAnimationSpec targetState == ToggleableState.Off -> snap(delayMillis = SnapAnimationDelay) else -> defaultAnimationSpec } } ) { when (it) { ToggleableState.On -> 1f ToggleableState.Off -> 0f ToggleableState.Indeterminate -> 1f } } val checkCenterGravitationShiftFraction = transition.animateFloat( transitionSpec = { when { // TODO Load the motionScheme tokens from the component tokens file initialState == ToggleableState.Off -> snap() targetState == ToggleableState.Off -> snap(delayMillis = SnapAnimationDelay) else -> defaultAnimationSpec } } ) { when (it) { ToggleableState.On -> 0f ToggleableState.Off -> 0f ToggleableState.Indeterminate -> 1f } } val checkCache = remember { CheckDrawingCache() } val checkColor = if (isCheckboxStylingFixEnabled) { colors.checkmarkColor(enabled, value) } else { colors.checkmarkColor(value) } val boxColor = colors.boxColor(enabled, value) val borderColor = colors.borderColor(enabled, value) val containerSize = if (isCheckboxStylingFixEnabled) { CheckboxTokens.ContainerSize } else { CheckboxSize } Canvas(modifier.wrapContentSize(Alignment.Center).requiredSize(containerSize)) { drawBox( boxColor = boxColor.value, borderColor = borderColor.value, radius = RadiusSize.toPx(), stroke = outlineStroke, ) drawCheck( checkColor = checkColor.value, checkFraction = checkDrawFraction.value, crossCenterGravitation = checkCenterGravitationShiftFraction.value, stroke = checkmarkStroke, drawingCache = checkCache, ) } } private fun DrawScope.drawBox(boxColor: Color, borderColor: Color, radius: Float, stroke: Stroke) { val halfStrokeWidth = stroke.width / 2.0f val checkboxSize = size.width if (boxColor == borderColor) { drawRoundRect( boxColor, size = Size(checkboxSize, checkboxSize), cornerRadius = CornerRadius(radius), style = Fill, ) } else { drawRoundRect( boxColor, topLeft = Offset(stroke.width, stroke.width), size = Size(checkboxSize - stroke.width * 2, checkboxSize - stroke.width * 2), cornerRadius = CornerRadius(max(0f, radius - stroke.width)), style = Fill, ) drawRoundRect( borderColor, topLeft = Offset(halfStrokeWidth, halfStrokeWidth), size = Size(checkboxSize - stroke.width, checkboxSize - stroke.width), cornerRadius = CornerRadius(radius - halfStrokeWidth), style = stroke, ) } } @OptIn(ExperimentalMaterial3Api::class) private fun DrawScope.drawCheck( checkColor: Color, checkFraction: Float, crossCenterGravitation: Float, stroke: Stroke, drawingCache: CheckDrawingCache, ) { val isCheckboxStylingFixEnabled = ComposeMaterial3Flags.isCheckboxStylingFixEnabled val width = size.width val checkCrossX = 0.4f val checkCrossY = if (isCheckboxStylingFixEnabled) 0.65f else 0.7f val leftX = if (isCheckboxStylingFixEnabled) 0.25f else 0.2f val leftY = 0.5f val rightX = if (isCheckboxStylingFixEnabled) 0.75f else 0.8f val rightY = 0.3f val gravitatedCrossX = lerp(checkCrossX, 0.5f, crossCenterGravitation) val gravitatedCrossY = lerp(checkCrossY, 0.5f, crossCenterGravitation) // gravitate only Y for end to achieve center line val gravitatedLeftY = lerp(leftY, 0.5f, crossCenterGravitation) val gravitatedRightY = lerp(rightY, 0.5f, crossCenterGravitation) with(drawingCache) { checkPath.rewind() checkPath.moveTo(width * leftX, width * gravitatedLeftY) checkPath.lineTo(width * gravitatedCrossX, width * gravitatedCrossY) checkPath.lineTo(width * rightX, width * gravitatedRightY) // TODO: replace with proper declarative non-android alternative when ready (b/158188351) pathMeasure.setPath(checkPath, false) pathToDraw.rewind() pathMeasure.getSegment(0f, pathMeasure.length * checkFraction, pathToDraw, true) } drawPath(drawingCache.pathToDraw, checkColor, style = stroke) } @Immutable private class CheckDrawingCache( val checkPath: Path = Path(), val pathMeasure: PathMeasure = PathMeasure(), val pathToDraw: Path = Path(), ) /** * Represents the colors used by the three different sections (checkmark, box, and border) of a * [Checkbox] or [TriStateCheckbox] in different states. * * @param checkedCheckmarkColor color that will be used for the checkmark when checked * @param uncheckedCheckmarkColor color that will be used for the checkmark when unchecked * @param checkedBoxColor the color that will be used for the box when checked * @param uncheckedBoxColor color that will be used for the box when unchecked * @param disabledCheckedBoxColor color that will be used for the box when disabled and checked * @param disabledUncheckedBoxColor color that will be used for the box when disabled and unchecked * @param disabledIndeterminateBoxColor color that will be used for the box and border in a * [TriStateCheckbox] when disabled AND in an [ToggleableState.Indeterminate] state. * @param checkedBorderColor color that will be used for the border when checked * @param uncheckedBorderColor color that will be used for the border when unchecked * @param disabledBorderColor color that will be used for the border when disabled and checked * @param disabledUncheckedBorderColor color that will be used for the border when disabled and * unchecked * @param disabledIndeterminateBorderColor color that will be used for the border when disabled and * in an [ToggleableState.Indeterminate] state. * @param disabledCheckmarkColor color that will be used for the checkmark when disabled * @constructor create an instance with arbitrary colors, see [CheckboxDefaults.colors] for the * default implementation that follows Material specifications. */ @Immutable class CheckboxColors constructor( val checkedCheckmarkColor: Color, val uncheckedCheckmarkColor: Color, val checkedBoxColor: Color, val uncheckedBoxColor: Color, val disabledCheckedBoxColor: Color, val disabledUncheckedBoxColor: Color, val disabledIndeterminateBoxColor: Color, val checkedBorderColor: Color, val uncheckedBorderColor: Color, val disabledBorderColor: Color, val disabledUncheckedBorderColor: Color, val disabledIndeterminateBorderColor: Color, val disabledCheckmarkColor: Color, ) { @Deprecated( message = "This constructor is deprecated. Use the primary constructor that includes 'disabledCheckmarkColor'", level = DeprecationLevel.WARNING, ) constructor( checkedCheckmarkColor: Color, uncheckedCheckmarkColor: Color, checkedBoxColor: Color, uncheckedBoxColor: Color, disabledCheckedBoxColor: Color, disabledUncheckedBoxColor: Color, disabledIndeterminateBoxColor: Color, checkedBorderColor: Color, uncheckedBorderColor: Color, disabledBorderColor: Color, disabledUncheckedBorderColor: Color, disabledIndeterminateBorderColor: Color, ) : this( checkedCheckmarkColor = checkedCheckmarkColor, uncheckedCheckmarkColor = uncheckedCheckmarkColor, checkedBoxColor = checkedBoxColor, uncheckedBoxColor = uncheckedBoxColor, disabledCheckedBoxColor = disabledCheckedBoxColor, disabledUncheckedBoxColor = disabledUncheckedBoxColor, disabledIndeterminateBoxColor = disabledIndeterminateBoxColor, checkedBorderColor = checkedBorderColor, uncheckedBorderColor = uncheckedBorderColor, disabledBorderColor = disabledBorderColor, disabledUncheckedBorderColor = disabledUncheckedBorderColor, disabledIndeterminateBorderColor = disabledIndeterminateBorderColor, disabledCheckmarkColor = checkedCheckmarkColor, ) /** * Returns a copy of this CheckboxColors, optionally overriding some of the values. This uses * the Color.Unspecified to mean “use the value from the source” */ @Deprecated( message = "This function is deprecated. Use 'copy' that includes 'disabledCheckmarkColor' instead", level = DeprecationLevel.HIDDEN, ) fun copy( checkedCheckmarkColor: Color = this.checkedCheckmarkColor, uncheckedCheckmarkColor: Color = this.uncheckedCheckmarkColor, checkedBoxColor: Color = this.checkedBoxColor, uncheckedBoxColor: Color = this.uncheckedBoxColor, disabledCheckedBoxColor: Color = this.disabledCheckedBoxColor, disabledUncheckedBoxColor: Color = this.disabledUncheckedBoxColor, disabledIndeterminateBoxColor: Color = this.disabledIndeterminateBoxColor, checkedBorderColor: Color = this.checkedBorderColor, uncheckedBorderColor: Color = this.uncheckedBorderColor, disabledBorderColor: Color = this.disabledBorderColor, disabledUncheckedBorderColor: Color = this.disabledUncheckedBorderColor, disabledIndeterminateBorderColor: Color = this.disabledIndeterminateBorderColor, ) = CheckboxColors( checkedCheckmarkColor = checkedCheckmarkColor.takeOrElse { this.checkedCheckmarkColor }, uncheckedCheckmarkColor = uncheckedCheckmarkColor.takeOrElse { this.uncheckedCheckmarkColor }, checkedBoxColor = checkedBoxColor.takeOrElse { this.checkedBoxColor }, uncheckedBoxColor = uncheckedBoxColor.takeOrElse { this.uncheckedBoxColor }, disabledCheckedBoxColor = disabledCheckedBoxColor.takeOrElse { this.disabledCheckedBoxColor }, disabledUncheckedBoxColor = disabledUncheckedBoxColor.takeOrElse { this.disabledUncheckedBoxColor }, disabledIndeterminateBoxColor = disabledIndeterminateBoxColor.takeOrElse { this.disabledIndeterminateBoxColor }, checkedBorderColor = checkedBorderColor.takeOrElse { this.checkedBorderColor }, uncheckedBorderColor = uncheckedBorderColor.takeOrElse { this.uncheckedBorderColor }, disabledBorderColor = disabledBorderColor.takeOrElse { this.disabledBorderColor }, disabledUncheckedBorderColor = disabledUncheckedBorderColor.takeOrElse { this.disabledUncheckedBorderColor }, disabledIndeterminateBorderColor = disabledIndeterminateBorderColor.takeOrElse { this.disabledIndeterminateBorderColor }, disabledCheckmarkColor = checkedCheckmarkColor.takeOrElse { this.checkedCheckmarkColor }, ) /** * Returns a copy of this CheckboxColors, optionally overriding some of the values. This uses * the Color.Unspecified to mean “use the value from the source” */ fun copy( checkedCheckmarkColor: Color = this.checkedCheckmarkColor, uncheckedCheckmarkColor: Color = this.uncheckedCheckmarkColor, checkedBoxColor: Color = this.checkedBoxColor, uncheckedBoxColor: Color = this.uncheckedBoxColor, disabledCheckedBoxColor: Color = this.disabledCheckedBoxColor, disabledUncheckedBoxColor: Color = this.disabledUncheckedBoxColor, disabledIndeterminateBoxColor: Color = this.disabledIndeterminateBoxColor, checkedBorderColor: Color = this.checkedBorderColor, uncheckedBorderColor: Color = this.uncheckedBorderColor, disabledBorderColor: Color = this.disabledBorderColor, disabledUncheckedBorderColor: Color = this.disabledUncheckedBorderColor, disabledIndeterminateBorderColor: Color = this.disabledIndeterminateBorderColor, disabledCheckmarkColor: Color = this.disabledCheckmarkColor, ) = CheckboxColors( checkedCheckmarkColor = checkedCheckmarkColor.takeOrElse { this.checkedCheckmarkColor }, uncheckedCheckmarkColor = uncheckedCheckmarkColor.takeOrElse { this.uncheckedCheckmarkColor }, checkedBoxColor = checkedBoxColor.takeOrElse { this.checkedBoxColor }, uncheckedBoxColor = uncheckedBoxColor.takeOrElse { this.uncheckedBoxColor }, disabledCheckedBoxColor = disabledCheckedBoxColor.takeOrElse { this.disabledCheckedBoxColor }, disabledUncheckedBoxColor = disabledUncheckedBoxColor.takeOrElse { this.disabledUncheckedBoxColor }, disabledIndeterminateBoxColor = disabledIndeterminateBoxColor.takeOrElse { this.disabledIndeterminateBoxColor }, checkedBorderColor = checkedBorderColor.takeOrElse { this.checkedBorderColor }, uncheckedBorderColor = uncheckedBorderColor.takeOrElse { this.uncheckedBorderColor }, disabledBorderColor = disabledBorderColor.takeOrElse { this.disabledBorderColor }, disabledUncheckedBorderColor = disabledUncheckedBorderColor.takeOrElse { this.disabledUncheckedBorderColor }, disabledIndeterminateBorderColor = disabledIndeterminateBorderColor.takeOrElse { this.disabledIndeterminateBorderColor }, disabledCheckmarkColor = disabledCheckmarkColor.takeOrElse { this.disabledCheckmarkColor }, ) /** * Represents the color used for the checkbox container's background indication, depending on * [state]. * * @param state the [ToggleableState] of the checkbox */ internal fun indicatorColor(state: ToggleableState): Color { return if (state == ToggleableState.Off) { uncheckedBoxColor } else { checkedBoxColor } } /** * Represents the color used for the checkmark inside the checkbox, depending on [enabled] and * [state]. * * @param enabled whether the checkbox is enabled or not * @param state the [ToggleableState] of the checkbox */ @Composable internal fun checkmarkColor(enabled: Boolean, state: ToggleableState): State { val target = if (enabled) { if (state == ToggleableState.Off) { uncheckedCheckmarkColor } else { checkedCheckmarkColor } } else { disabledCheckmarkColor } return animateColorAsState(target, colorAnimationSpecForState(state)) } /** * Represents the color used for the checkmark inside the checkbox, depending on [state]. * * @param state the [ToggleableState] of the checkbox */ @Composable internal fun checkmarkColor(state: ToggleableState): State { val target = if (state == ToggleableState.Off) { uncheckedCheckmarkColor } else { checkedCheckmarkColor } return animateColorAsState(target, colorAnimationSpecForState(state)) } /** * Represents the color used for the box (background) of the checkbox, depending on [enabled] * and [state]. * * @param enabled whether the checkbox is enabled or not * @param state the [ToggleableState] of the checkbox */ @Composable internal fun boxColor(enabled: Boolean, state: ToggleableState): State { val target = if (enabled) { when (state) { ToggleableState.On, ToggleableState.Indeterminate -> checkedBoxColor ToggleableState.Off -> uncheckedBoxColor } } else { when (state) { ToggleableState.On -> disabledCheckedBoxColor ToggleableState.Indeterminate -> disabledIndeterminateBoxColor ToggleableState.Off -> disabledUncheckedBoxColor } } // If not enabled 'snap' to the disabled state, as there should be no animations between // enabled / disabled. return if (enabled) { animateColorAsState(target, colorAnimationSpecForState(state)) } else { rememberUpdatedState(target) } } /** * Represents the color used for the border of the checkbox, depending on [enabled] and [state]. * * @param enabled whether the checkbox is enabled or not * @param state the [ToggleableState] of the checkbox */ @Composable internal fun borderColor(enabled: Boolean, state: ToggleableState): State { val target = if (enabled) { when (state) { ToggleableState.On, ToggleableState.Indeterminate -> checkedBorderColor ToggleableState.Off -> uncheckedBorderColor } } else { when (state) { ToggleableState.Indeterminate -> disabledIndeterminateBorderColor ToggleableState.On -> disabledBorderColor ToggleableState.Off -> disabledUncheckedBorderColor } } // If not enabled 'snap' to the disabled state, as there should be no animations between // enabled / disabled. return if (enabled) { animateColorAsState(target, colorAnimationSpecForState(state)) } else { rememberUpdatedState(target) } } /** Returns the color [AnimationSpec] for the given state. */ @Composable private fun colorAnimationSpecForState(state: ToggleableState): AnimationSpec { // TODO Load the motionScheme tokens from the component tokens file return if (state == ToggleableState.Off) { // Box out MotionSchemeKeyTokens.FastEffects.value() } else { // Box in MotionSchemeKeyTokens.DefaultEffects.value() } } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is CheckboxColors) return false if (checkedCheckmarkColor != other.checkedCheckmarkColor) return false if (uncheckedCheckmarkColor != other.uncheckedCheckmarkColor) return false if (disabledCheckmarkColor != other.disabledCheckmarkColor) return false if (checkedBoxColor != other.checkedBoxColor) return false if (uncheckedBoxColor != other.uncheckedBoxColor) return false if (disabledCheckedBoxColor != other.disabledCheckedBoxColor) return false if (disabledUncheckedBoxColor != other.disabledUncheckedBoxColor) return false if (disabledIndeterminateBoxColor != other.disabledIndeterminateBoxColor) return false if (checkedBorderColor != other.checkedBorderColor) return false if (uncheckedBorderColor != other.uncheckedBorderColor) return false if (disabledBorderColor != other.disabledBorderColor) return false if (disabledUncheckedBorderColor != other.disabledUncheckedBorderColor) return false if (disabledIndeterminateBorderColor != other.disabledIndeterminateBorderColor) return false return true } override fun hashCode(): Int { var result = checkedCheckmarkColor.hashCode() result = 31 * result + uncheckedCheckmarkColor.hashCode() result = 31 * result + disabledCheckmarkColor.hashCode() result = 31 * result + checkedBoxColor.hashCode() result = 31 * result + uncheckedBoxColor.hashCode() result = 31 * result + disabledCheckedBoxColor.hashCode() result = 31 * result + disabledUncheckedBoxColor.hashCode() result = 31 * result + disabledIndeterminateBoxColor.hashCode() result = 31 * result + checkedBorderColor.hashCode() result = 31 * result + uncheckedBorderColor.hashCode() result = 31 * result + disabledBorderColor.hashCode() result = 31 * result + disabledUncheckedBorderColor.hashCode() result = 31 * result + disabledIndeterminateBorderColor.hashCode() return result } } private const val SnapAnimationDelay = 100 // TODO(b/188529841): Update the padding and size when the Checkbox spec is finalized. private val CheckboxDefaultPadding = 2.dp private val CheckboxSize = 20.dp private val RadiusSize = 2.dp ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Switch.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.SnapSpec import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.indication import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.InteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.requiredSize import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.SwitchTokens import androidx.compose.material3.tokens.SwitchTokens.TrackOutlineWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.compositeOver import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.invalidateMeasurement import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch /** * [Material Design switch](https://m3.material.io/components/switch) * * Switches toggle the state of a single item on or off. * * ![Switch * image](https://developer.android.com/images/reference/androidx/compose/material3/switch.png) * * @sample androidx.compose.material3.samples.SwitchSample * * Switch can be used with a custom icon via [thumbContent] parameter * * @sample androidx.compose.material3.samples.SwitchWithThumbIconSample * @param checked whether or not this switch is checked * @param onCheckedChange called when this switch is clicked. If `null`, then this switch will not * be interactable, unless something else handles its input events and updates its state. * @param modifier the [Modifier] to be applied to this switch * @param thumbContent content that will be drawn inside the thumb, expected to measure * [SwitchDefaults.IconSize] * @param enabled controls the enabled state of this switch. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param colors [SwitchColors] that will be used to resolve the colors used for this switch in * different states. See [SwitchDefaults.colors]. * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this switch. You can use this to change the switch's appearance or * preview the switch in different states. Note that if `null` is provided, interactions will * still happen internally. */ @Composable @Suppress("ComposableLambdaParameterNaming", "ComposableLambdaParameterPosition") fun Switch( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, thumbContent: (@Composable () -> Unit)? = null, enabled: Boolean = true, colors: SwitchColors = SwitchDefaults.colors(), interactionSource: MutableInteractionSource? = null, ) { @Suppress("NAME_SHADOWING") val interactionSource = interactionSource ?: remember { MutableInteractionSource() } // TODO: Add Swipeable modifier b/223797571 val toggleableModifier = if (onCheckedChange != null) { Modifier.minimumInteractiveComponentSize() .toggleable( value = checked, onValueChange = onCheckedChange, enabled = enabled, role = Role.Switch, interactionSource = interactionSource, indication = null, ) } else { Modifier } SwitchImpl( modifier = modifier .then(toggleableModifier) .wrapContentSize(Alignment.Center) .requiredSize(SwitchWidth, SwitchHeight), checked = checked, enabled = enabled, colors = colors, interactionSource = interactionSource, thumbShape = SwitchTokens.HandleShape.value, thumbContent = thumbContent, ) } @Composable @Suppress("ComposableLambdaParameterNaming", "ComposableLambdaParameterPosition") private fun SwitchImpl( modifier: Modifier, checked: Boolean, enabled: Boolean, colors: SwitchColors, thumbContent: (@Composable () -> Unit)?, interactionSource: InteractionSource, thumbShape: Shape, ) { val trackColor = colors.trackColor(enabled, checked) val resolvedThumbColor = colors.thumbColor(enabled, checked) val trackShape = SwitchTokens.TrackShape.value Box( modifier .border(TrackOutlineWidth, colors.borderColor(enabled, checked), trackShape) .background(trackColor, trackShape) ) { Box( modifier = Modifier.align(Alignment.CenterStart) .then( ThumbElement( interactionSource = interactionSource, checked = checked, // TODO Load the motionScheme tokens from the component tokens file animationSpec = MotionSchemeKeyTokens.FastSpatial.value(), ) ) .indication( interactionSource = interactionSource, indication = ripple(bounded = false, radius = SwitchTokens.StateLayerSize / 2), ) .background(resolvedThumbColor, thumbShape), contentAlignment = Alignment.Center, ) { if (thumbContent != null) { val iconColor = colors.iconColor(enabled, checked) CompositionLocalProvider( LocalContentColor provides iconColor, content = thumbContent, ) } } } } private data class ThumbElement( val interactionSource: InteractionSource, val checked: Boolean, val animationSpec: FiniteAnimationSpec, ) : ModifierNodeElement() { override fun create() = ThumbNode(interactionSource, checked, animationSpec) override fun update(node: ThumbNode) { node.interactionSource = interactionSource if (node.checked != checked) { node.invalidateMeasurement() } node.checked = checked node.animationSpec = animationSpec node.update() } override fun InspectorInfo.inspectableProperties() { name = "switchThumb" properties["interactionSource"] = interactionSource properties["checked"] = checked properties["animationSpec"] = animationSpec } } private class ThumbNode( var interactionSource: InteractionSource, var checked: Boolean, var animationSpec: FiniteAnimationSpec, ) : Modifier.Node(), LayoutModifierNode { override val shouldAutoInvalidate: Boolean get() = false private var isPressed = false private var offsetAnim: Animatable? = null private var sizeAnim: Animatable? = null private var initialOffset: Float = Float.NaN private var initialSize: Float = Float.NaN override fun onAttach() { coroutineScope.launch { var pressCount = 0 interactionSource.interactions.collect { interaction -> when (interaction) { is PressInteraction.Press -> pressCount++ is PressInteraction.Release -> pressCount-- is PressInteraction.Cancel -> pressCount-- } val pressed = pressCount > 0 if (isPressed != pressed) { isPressed = pressed invalidateMeasurement() } } } } override fun onReset() { super.onReset() offsetAnim = null sizeAnim = null initialSize = Float.NaN initialOffset = Float.NaN } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { val hasContent = measurable.maxIntrinsicHeight(constraints.maxWidth) != 0 && measurable.maxIntrinsicWidth(constraints.maxHeight) != 0 val size = when { isPressed -> SwitchTokens.PressedHandleWidth hasContent || checked -> ThumbDiameter else -> UncheckedThumbDiameter }.toPx() val actualSize = (sizeAnim?.value ?: size).toInt() val placeable = measurable.measure(Constraints.fixed(actualSize, actualSize)) val thumbPaddingStart = (SwitchHeight - size.toDp()) / 2f val minBound = thumbPaddingStart.toPx() val thumbPathLength = (SwitchWidth - ThumbDiameter) - ThumbPadding val maxBound = thumbPathLength.toPx() val offset = when { isPressed && checked -> maxBound - TrackOutlineWidth.toPx() isPressed && !checked -> TrackOutlineWidth.toPx() checked -> maxBound else -> minBound } if (sizeAnim?.targetValue != size) { coroutineScope.launch { sizeAnim?.animateTo(size, if (isPressed) SnapSpec else animationSpec) } } if (offsetAnim?.targetValue != offset) { coroutineScope.launch { offsetAnim?.animateTo(offset, if (isPressed) SnapSpec else animationSpec) } } if (initialSize.isNaN() && initialOffset.isNaN()) { initialSize = size initialOffset = offset } return layout(actualSize, actualSize) { placeable.placeRelative(offsetAnim?.value?.toInt() ?: offset.toInt(), 0) } } fun update() { if (sizeAnim == null && !initialSize.isNaN()) { sizeAnim = Animatable(initialSize) } if (offsetAnim == null && !initialOffset.isNaN()) offsetAnim = Animatable(initialOffset) } } /** Contains the default values used by [Switch] */ object SwitchDefaults { /** * Creates a [SwitchColors] that represents the different colors used in a [Switch] in different * states. */ @Composable fun colors() = MaterialTheme.colorScheme.defaultSwitchColors /** * Creates a [SwitchColors] that represents the different colors used in a [Switch] in different * states. * * @param checkedThumbColor the color used for the thumb when enabled and checked * @param checkedTrackColor the color used for the track when enabled and checked * @param checkedBorderColor the color used for the border when enabled and checked * @param checkedIconColor the color used for the icon when enabled and checked * @param uncheckedThumbColor the color used for the thumb when enabled and unchecked * @param uncheckedTrackColor the color used for the track when enabled and unchecked * @param uncheckedBorderColor the color used for the border when enabled and unchecked * @param uncheckedIconColor the color used for the icon when enabled and unchecked * @param disabledCheckedThumbColor the color used for the thumb when disabled and checked * @param disabledCheckedTrackColor the color used for the track when disabled and checked * @param disabledCheckedBorderColor the color used for the border when disabled and checked * @param disabledCheckedIconColor the color used for the icon when disabled and checked * @param disabledUncheckedThumbColor the color used for the thumb when disabled and unchecked * @param disabledUncheckedTrackColor the color used for the track when disabled and unchecked * @param disabledUncheckedBorderColor the color used for the border when disabled and unchecked * @param disabledUncheckedIconColor the color used for the icon when disabled and unchecked */ @Composable fun colors( checkedThumbColor: Color = SwitchTokens.SelectedHandleColor.value, checkedTrackColor: Color = SwitchTokens.SelectedTrackColor.value, checkedBorderColor: Color = Color.Transparent, checkedIconColor: Color = SwitchTokens.SelectedIconColor.value, uncheckedThumbColor: Color = SwitchTokens.UnselectedHandleColor.value, uncheckedTrackColor: Color = SwitchTokens.UnselectedTrackColor.value, uncheckedBorderColor: Color = SwitchTokens.UnselectedFocusTrackOutlineColor.value, uncheckedIconColor: Color = SwitchTokens.UnselectedIconColor.value, disabledCheckedThumbColor: Color = SwitchTokens.DisabledSelectedHandleColor.value .copy(alpha = SwitchTokens.DisabledSelectedHandleOpacity) .compositeOver(MaterialTheme.colorScheme.surface), disabledCheckedTrackColor: Color = SwitchTokens.DisabledSelectedTrackColor.value .copy(alpha = SwitchTokens.DisabledTrackOpacity) .compositeOver(MaterialTheme.colorScheme.surface), disabledCheckedBorderColor: Color = Color.Transparent, disabledCheckedIconColor: Color = SwitchTokens.DisabledSelectedIconColor.value .copy(alpha = SwitchTokens.DisabledSelectedIconOpacity) .compositeOver(MaterialTheme.colorScheme.surface), disabledUncheckedThumbColor: Color = SwitchTokens.DisabledUnselectedHandleColor.value .copy(alpha = SwitchTokens.DisabledUnselectedHandleOpacity) .compositeOver(MaterialTheme.colorScheme.surface), disabledUncheckedTrackColor: Color = SwitchTokens.DisabledUnselectedTrackColor.value .copy(alpha = SwitchTokens.DisabledTrackOpacity) .compositeOver(MaterialTheme.colorScheme.surface), disabledUncheckedBorderColor: Color = SwitchTokens.DisabledUnselectedTrackOutlineColor.value .copy(alpha = SwitchTokens.DisabledTrackOpacity) .compositeOver(MaterialTheme.colorScheme.surface), disabledUncheckedIconColor: Color = SwitchTokens.DisabledUnselectedIconColor.value .copy(alpha = SwitchTokens.DisabledUnselectedIconOpacity) .compositeOver(MaterialTheme.colorScheme.surface), ): SwitchColors = SwitchColors( checkedThumbColor = checkedThumbColor, checkedTrackColor = checkedTrackColor, checkedBorderColor = checkedBorderColor, checkedIconColor = checkedIconColor, uncheckedThumbColor = uncheckedThumbColor, uncheckedTrackColor = uncheckedTrackColor, uncheckedBorderColor = uncheckedBorderColor, uncheckedIconColor = uncheckedIconColor, disabledCheckedThumbColor = disabledCheckedThumbColor, disabledCheckedTrackColor = disabledCheckedTrackColor, disabledCheckedBorderColor = disabledCheckedBorderColor, disabledCheckedIconColor = disabledCheckedIconColor, disabledUncheckedThumbColor = disabledUncheckedThumbColor, disabledUncheckedTrackColor = disabledUncheckedTrackColor, disabledUncheckedBorderColor = disabledUncheckedBorderColor, disabledUncheckedIconColor = disabledUncheckedIconColor, ) internal val ColorScheme.defaultSwitchColors: SwitchColors get() { return defaultSwitchColorsCached ?: SwitchColors( checkedThumbColor = fromToken(SwitchTokens.SelectedHandleColor), checkedTrackColor = fromToken(SwitchTokens.SelectedTrackColor), checkedBorderColor = Color.Transparent, checkedIconColor = fromToken(SwitchTokens.SelectedIconColor), uncheckedThumbColor = fromToken(SwitchTokens.UnselectedHandleColor), uncheckedTrackColor = fromToken(SwitchTokens.UnselectedTrackColor), uncheckedBorderColor = fromToken(SwitchTokens.UnselectedFocusTrackOutlineColor), uncheckedIconColor = fromToken(SwitchTokens.UnselectedIconColor), disabledCheckedThumbColor = fromToken(SwitchTokens.DisabledSelectedHandleColor) .copy(alpha = SwitchTokens.DisabledSelectedHandleOpacity) .compositeOver(surface), disabledCheckedTrackColor = fromToken(SwitchTokens.DisabledSelectedTrackColor) .copy(alpha = SwitchTokens.DisabledTrackOpacity) .compositeOver(surface), disabledCheckedBorderColor = Color.Transparent, disabledCheckedIconColor = fromToken(SwitchTokens.DisabledSelectedIconColor) .copy(alpha = SwitchTokens.DisabledSelectedIconOpacity) .compositeOver(surface), disabledUncheckedThumbColor = fromToken(SwitchTokens.DisabledUnselectedHandleColor) .copy(alpha = SwitchTokens.DisabledUnselectedHandleOpacity) .compositeOver(surface), disabledUncheckedTrackColor = fromToken(SwitchTokens.DisabledUnselectedTrackColor) .copy(alpha = SwitchTokens.DisabledTrackOpacity) .compositeOver(surface), disabledUncheckedBorderColor = fromToken(SwitchTokens.DisabledUnselectedTrackOutlineColor) .copy(alpha = SwitchTokens.DisabledTrackOpacity) .compositeOver(surface), disabledUncheckedIconColor = fromToken(SwitchTokens.DisabledUnselectedIconColor) .copy(alpha = SwitchTokens.DisabledUnselectedIconOpacity) .compositeOver(surface), ) .also { defaultSwitchColorsCached = it } } /** Icon size to use for `thumbContent` */ val IconSize = 16.dp } /** * Represents the colors used by a [Switch] in different states * * @param checkedThumbColor the color used for the thumb when enabled and checked * @param checkedTrackColor the color used for the track when enabled and checked * @param checkedBorderColor the color used for the border when enabled and checked * @param checkedIconColor the color used for the icon when enabled and checked * @param uncheckedThumbColor the color used for the thumb when enabled and unchecked * @param uncheckedTrackColor the color used for the track when enabled and unchecked * @param uncheckedBorderColor the color used for the border when enabled and unchecked * @param uncheckedIconColor the color used for the icon when enabled and unchecked * @param disabledCheckedThumbColor the color used for the thumb when disabled and checked * @param disabledCheckedTrackColor the color used for the track when disabled and checked * @param disabledCheckedBorderColor the color used for the border when disabled and checked * @param disabledCheckedIconColor the color used for the icon when disabled and checked * @param disabledUncheckedThumbColor the color used for the thumb when disabled and unchecked * @param disabledUncheckedTrackColor the color used for the track when disabled and unchecked * @param disabledUncheckedBorderColor the color used for the border when disabled and unchecked * @param disabledUncheckedIconColor the color used for the icon when disabled and unchecked * @constructor create an instance with arbitrary colors. See [SwitchDefaults.colors] for the * default implementation that follows Material specifications. */ @Immutable class SwitchColors constructor( val checkedThumbColor: Color, val checkedTrackColor: Color, val checkedBorderColor: Color, val checkedIconColor: Color, val uncheckedThumbColor: Color, val uncheckedTrackColor: Color, val uncheckedBorderColor: Color, val uncheckedIconColor: Color, val disabledCheckedThumbColor: Color, val disabledCheckedTrackColor: Color, val disabledCheckedBorderColor: Color, val disabledCheckedIconColor: Color, val disabledUncheckedThumbColor: Color, val disabledUncheckedTrackColor: Color, val disabledUncheckedBorderColor: Color, val disabledUncheckedIconColor: Color, ) { /** * Returns a copy of this SwitchColors, optionally overriding some of the values. This uses the * Color.Unspecified to mean “use the value from the source” */ fun copy( checkedThumbColor: Color = this.checkedThumbColor, checkedTrackColor: Color = this.checkedTrackColor, checkedBorderColor: Color = this.checkedBorderColor, checkedIconColor: Color = this.checkedIconColor, uncheckedThumbColor: Color = this.uncheckedThumbColor, uncheckedTrackColor: Color = this.uncheckedTrackColor, uncheckedBorderColor: Color = this.uncheckedBorderColor, uncheckedIconColor: Color = this.uncheckedIconColor, disabledCheckedThumbColor: Color = this.disabledCheckedThumbColor, disabledCheckedTrackColor: Color = this.disabledCheckedTrackColor, disabledCheckedBorderColor: Color = this.disabledCheckedBorderColor, disabledCheckedIconColor: Color = this.disabledCheckedIconColor, disabledUncheckedThumbColor: Color = this.disabledUncheckedThumbColor, disabledUncheckedTrackColor: Color = this.disabledUncheckedTrackColor, disabledUncheckedBorderColor: Color = this.disabledUncheckedBorderColor, disabledUncheckedIconColor: Color = this.disabledUncheckedIconColor, ) = SwitchColors( checkedThumbColor.takeOrElse { this.checkedThumbColor }, checkedTrackColor.takeOrElse { this.checkedTrackColor }, checkedBorderColor.takeOrElse { this.checkedBorderColor }, checkedIconColor.takeOrElse { this.checkedIconColor }, uncheckedThumbColor.takeOrElse { this.uncheckedThumbColor }, uncheckedTrackColor.takeOrElse { this.uncheckedTrackColor }, uncheckedBorderColor.takeOrElse { this.uncheckedBorderColor }, uncheckedIconColor.takeOrElse { this.uncheckedIconColor }, disabledCheckedThumbColor.takeOrElse { this.disabledCheckedThumbColor }, disabledCheckedTrackColor.takeOrElse { this.disabledCheckedTrackColor }, disabledCheckedBorderColor.takeOrElse { this.disabledCheckedBorderColor }, disabledCheckedIconColor.takeOrElse { this.disabledCheckedIconColor }, disabledUncheckedThumbColor.takeOrElse { this.disabledUncheckedThumbColor }, disabledUncheckedTrackColor.takeOrElse { this.disabledUncheckedTrackColor }, disabledUncheckedBorderColor.takeOrElse { this.disabledUncheckedBorderColor }, disabledUncheckedIconColor.takeOrElse { this.disabledUncheckedIconColor }, ) /** * Represents the color used for the switch's thumb, depending on [enabled] and [checked]. * * @param enabled whether the [Switch] is enabled or not * @param checked whether the [Switch] is checked or not */ @Stable internal fun thumbColor(enabled: Boolean, checked: Boolean): Color = if (enabled) { if (checked) checkedThumbColor else uncheckedThumbColor } else { if (checked) disabledCheckedThumbColor else disabledUncheckedThumbColor } /** * Represents the color used for the switch's track, depending on [enabled] and [checked]. * * @param enabled whether the [Switch] is enabled or not * @param checked whether the [Switch] is checked or not */ @Stable internal fun trackColor(enabled: Boolean, checked: Boolean): Color = if (enabled) { if (checked) checkedTrackColor else uncheckedTrackColor } else { if (checked) disabledCheckedTrackColor else disabledUncheckedTrackColor } /** * Represents the color used for the switch's border, depending on [enabled] and [checked]. * * @param enabled whether the [Switch] is enabled or not * @param checked whether the [Switch] is checked or not */ @Stable internal fun borderColor(enabled: Boolean, checked: Boolean): Color = if (enabled) { if (checked) checkedBorderColor else uncheckedBorderColor } else { if (checked) disabledCheckedBorderColor else disabledUncheckedBorderColor } /** * Represents the content color passed to the icon if used * * @param enabled whether the [Switch] is enabled or not * @param checked whether the [Switch] is checked or not */ @Stable internal fun iconColor(enabled: Boolean, checked: Boolean): Color = if (enabled) { if (checked) checkedIconColor else uncheckedIconColor } else { if (checked) disabledCheckedIconColor else disabledUncheckedIconColor } override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is SwitchColors) return false if (checkedThumbColor != other.checkedThumbColor) return false if (checkedTrackColor != other.checkedTrackColor) return false if (checkedBorderColor != other.checkedBorderColor) return false if (checkedIconColor != other.checkedIconColor) return false if (uncheckedThumbColor != other.uncheckedThumbColor) return false if (uncheckedTrackColor != other.uncheckedTrackColor) return false if (uncheckedBorderColor != other.uncheckedBorderColor) return false if (uncheckedIconColor != other.uncheckedIconColor) return false if (disabledCheckedThumbColor != other.disabledCheckedThumbColor) return false if (disabledCheckedTrackColor != other.disabledCheckedTrackColor) return false if (disabledCheckedBorderColor != other.disabledCheckedBorderColor) return false if (disabledCheckedIconColor != other.disabledCheckedIconColor) return false if (disabledUncheckedThumbColor != other.disabledUncheckedThumbColor) return false if (disabledUncheckedTrackColor != other.disabledUncheckedTrackColor) return false if (disabledUncheckedBorderColor != other.disabledUncheckedBorderColor) return false if (disabledUncheckedIconColor != other.disabledUncheckedIconColor) return false return true } override fun hashCode(): Int { var result = checkedThumbColor.hashCode() result = 31 * result + checkedTrackColor.hashCode() result = 31 * result + checkedBorderColor.hashCode() result = 31 * result + checkedIconColor.hashCode() result = 31 * result + uncheckedThumbColor.hashCode() result = 31 * result + uncheckedTrackColor.hashCode() result = 31 * result + uncheckedBorderColor.hashCode() result = 31 * result + uncheckedIconColor.hashCode() result = 31 * result + disabledCheckedThumbColor.hashCode() result = 31 * result + disabledCheckedTrackColor.hashCode() result = 31 * result + disabledCheckedBorderColor.hashCode() result = 31 * result + disabledCheckedIconColor.hashCode() result = 31 * result + disabledUncheckedThumbColor.hashCode() result = 31 * result + disabledUncheckedTrackColor.hashCode() result = 31 * result + disabledUncheckedBorderColor.hashCode() result = 31 * result + disabledUncheckedIconColor.hashCode() return result } } /* @VisibleForTesting */ internal val ThumbDiameter = SwitchTokens.SelectedHandleWidth internal val UncheckedThumbDiameter = SwitchTokens.UnselectedHandleWidth private val SwitchWidth = SwitchTokens.TrackWidth private val SwitchHeight = SwitchTokens.TrackHeight private val ThumbPadding = (SwitchHeight - ThumbDiameter) / 2 private val SnapSpec = SnapSpec() ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Tab.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.compose.animation.animateColor import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.selection.selectable import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.PrimaryNavigationTabTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.FirstBaseline import androidx.compose.ui.layout.LastBaseline import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.layoutId import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastFirst import kotlin.math.max /** * [Material Design tab](https://m3.material.io/components/tabs/overview) * * A default Tab, also known as a Primary Navigation Tab. Tabs organize content across different * screens, data sets, and other interactions. * * ![Tabs * image](https://developer.android.com/images/reference/androidx/compose/material3/secondary-tabs.png) * * A Tab represents a single page of content using a text label and/or icon. It represents its * selected state by tinting the text label and/or image with [selectedContentColor]. * * This should typically be used inside of a [TabRow], see the corresponding documentation for * example usage. * * This Tab has slots for [text] and/or [icon] - see the other Tab overload for a generic Tab that * is not opinionated about its content. * * @param selected whether this tab is selected or not * @param onClick called when this tab is clicked * @param modifier the [Modifier] to be applied to this tab * @param enabled controls the enabled state of this tab. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param text the text label displayed in this tab * @param icon the icon displayed in this tab * @param selectedContentColor the color for the content of this tab when selected, and the color of * the ripple. * @param unselectedContentColor the color for the content of this tab when not selected * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this tab. You can use this to change the tab's appearance or * preview the tab in different states. Note that if `null` is provided, interactions will still * happen internally. * @see LeadingIconTab */ @Composable fun Tab( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, text: @Composable (() -> Unit)? = null, icon: @Composable (() -> Unit)? = null, selectedContentColor: Color = LocalContentColor.current, unselectedContentColor: Color = selectedContentColor, interactionSource: MutableInteractionSource? = null, ) { val styledText: @Composable (() -> Unit)? = text?.let { @Composable { val style = PrimaryNavigationTabTokens.LabelTextFont.value.copy( textAlign = TextAlign.Center ) ProvideTextStyle(style, content = text) } } Tab( modifier = modifier.badgeBounds(), selected = selected, onClick = onClick, enabled = enabled, selectedContentColor = selectedContentColor, unselectedContentColor = unselectedContentColor, interactionSource = interactionSource, ) { TabBaselineLayout(icon = icon, text = styledText) } } /** * [Material Design tab](https://m3.material.io/components/tabs/overview) * * Tabs organize content across different screens, data sets, and other interactions. * * A LeadingIconTab represents a single page of content using a text label and an icon in front of * the label. It represents its selected state by tinting the text label and icon with * [selectedContentColor]. * * This should typically be used inside of a [TabRow], see the corresponding documentation for * example usage. * * @param selected whether this tab is selected or not * @param onClick called when this tab is clicked * @param text the text label displayed in this tab * @param icon the icon displayed in this tab. Should be 24.dp. * @param modifier the [Modifier] to be applied to this tab * @param enabled controls the enabled state of this tab. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param selectedContentColor the color for the content of this tab when selected, and the color of * the ripple. * @param unselectedContentColor the color for the content of this tab when not selected * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this tab. You can use this to change the tab's appearance or * preview the tab in different states. Note that if `null` is provided, interactions will still * happen internally. * @see Tab */ @Composable fun LeadingIconTab( selected: Boolean, onClick: () -> Unit, text: @Composable () -> Unit, icon: @Composable () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, selectedContentColor: Color = LocalContentColor.current, unselectedContentColor: Color = selectedContentColor, interactionSource: MutableInteractionSource? = null, ) { // The color of the Ripple should always the be selected color, as we want to show the color // before the item is considered selected, and hence before the new contentColor is // provided by TabTransition. val ripple = ripple(bounded = true, color = selectedContentColor) TabTransition(selectedContentColor, unselectedContentColor, selected) { Row( modifier = modifier .height(SmallTabHeight) .selectable( selected = selected, onClick = onClick, enabled = enabled, role = Role.Tab, interactionSource = interactionSource, indication = ripple, ) .padding(horizontal = HorizontalTextPadding) .fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { icon() Spacer(Modifier.requiredWidth(TextDistanceFromLeadingIcon)) val style = PrimaryNavigationTabTokens.LabelTextFont.value.copy(textAlign = TextAlign.Center) ProvideTextStyle(style, content = text) } } } /** * [Material Design tab](https://m3.material.io/components/tabs/overview) * * Tabs organize content across different screens, data sets, and other interactions. * * ![Tabs * image](https://developer.android.com/images/reference/androidx/compose/material3/secondary-tabs.png) * * Generic [Tab] overload that is not opinionated about content / color. See the other overload for * a Tab that has specific slots for text and / or an icon, as well as providing the correct colors * for selected / unselected states. * * A custom tab using this API may look like: * * @sample androidx.compose.material3.samples.FancyTab * @param selected whether this tab is selected or not * @param onClick called when this tab is clicked * @param modifier the [Modifier] to be applied to this tab * @param enabled controls the enabled state of this tab. When `false`, this component will not * respond to user input, and it will appear visually disabled and disabled to accessibility * services. * @param selectedContentColor the color for the content of this tab when selected, and the color of * the ripple. * @param unselectedContentColor the color for the content of this tab when not selected * @param interactionSource an optional hoisted [MutableInteractionSource] for observing and * emitting [Interaction]s for this tab. You can use this to change the tab's appearance or * preview the tab in different states. Note that if `null` is provided, interactions will still * happen internally. * @param content the content of this tab */ @Composable fun Tab( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, selectedContentColor: Color = LocalContentColor.current, unselectedContentColor: Color = selectedContentColor, interactionSource: MutableInteractionSource? = null, content: @Composable ColumnScope.() -> Unit, ) { // The color of the Ripple should always the selected color, as we want to show the color // before the item is considered selected, and hence before the new contentColor is // provided by TabTransition. val ripple = ripple(bounded = true, color = selectedContentColor) TabTransition(selectedContentColor, unselectedContentColor, selected) { Column( modifier = modifier .selectable( selected = selected, onClick = onClick, enabled = enabled, role = Role.Tab, interactionSource = interactionSource, indication = ripple, ) .fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, content = content, ) } } /** * Transition defining how the tint color for a tab animates, when a new tab is selected. This * component uses [LocalContentColor] to provide an interpolated value between [activeColor] and * [inactiveColor] depending on the animation status. */ @Composable private fun TabTransition( activeColor: Color, inactiveColor: Color, selected: Boolean, content: @Composable () -> Unit, ) { val transition = updateTransition(selected) // TODO Load the motionScheme tokens from the component tokens file val color by transition.animateColor( transitionSpec = { if (false isTransitioningTo true) { // Fade-in MotionSchemeKeyTokens.DefaultEffects.value() } else { // Fade-out MotionSchemeKeyTokens.FastEffects.value() } } ) { if (it) activeColor else inactiveColor } CompositionLocalProvider(LocalContentColor provides color, content = content) } /** * A [Layout] that positions [text] and an optional [icon] with the correct baseline distances. This * Layout will either be [SmallTabHeight] or [LargeTabHeight] depending on its content, and then * place the text and/or icon inside with the correct baseline alignment. */ @Composable private fun TabBaselineLayout(text: @Composable (() -> Unit)?, icon: @Composable (() -> Unit)?) { Layout({ if (text != null) { Box(Modifier.layoutId("text").padding(horizontal = HorizontalTextPadding)) { text() } } if (icon != null) { Box(Modifier.layoutId("icon")) { icon() } } }) { measurables, constraints -> val textPlaceable = text?.let { measurables .fastFirst { it.layoutId == "text" } .measure( // Measure with loose constraints for height as we don't want the text to // take up more // space than it needs constraints.copy(minHeight = 0) ) } val iconPlaceable = icon?.let { measurables.fastFirst { it.layoutId == "icon" }.measure(constraints) } val tabWidth = max(textPlaceable?.width ?: 0, iconPlaceable?.width ?: 0) val specHeight = if (textPlaceable != null && iconPlaceable != null) { LargeTabHeight } else { SmallTabHeight } .roundToPx() val tabHeight = max( specHeight, (iconPlaceable?.height ?: 0) + (textPlaceable?.height ?: 0) + IconDistanceFromBaseline.roundToPx(), ) val firstBaseline = textPlaceable?.get(FirstBaseline) val lastBaseline = textPlaceable?.get(LastBaseline) layout(tabWidth, tabHeight) { when { textPlaceable != null && iconPlaceable != null -> placeTextAndIcon( density = this@Layout, textPlaceable = textPlaceable, iconPlaceable = iconPlaceable, tabWidth = tabWidth, tabHeight = tabHeight, firstBaseline = firstBaseline!!, lastBaseline = lastBaseline!!, ) textPlaceable != null -> placeTextOrIcon(textPlaceable, tabHeight) iconPlaceable != null -> placeTextOrIcon(iconPlaceable, tabHeight) else -> {} } } } } /** Places the provided [textOrIconPlaceable] in the vertical center of the provided [tabHeight]. */ private fun Placeable.PlacementScope.placeTextOrIcon( textOrIconPlaceable: Placeable, tabHeight: Int, ) { val contentY = (tabHeight - textOrIconPlaceable.height) / 2 textOrIconPlaceable.placeRelative(0, contentY) } /** * Places the provided [textPlaceable] offset from the bottom of the tab using the correct baseline * offset, with the provided [iconPlaceable] placed above the text using the correct baseline * offset. */ private fun Placeable.PlacementScope.placeTextAndIcon( density: Density, textPlaceable: Placeable, iconPlaceable: Placeable, tabWidth: Int, tabHeight: Int, firstBaseline: Int, lastBaseline: Int, ) { val baselineOffset = if (firstBaseline == lastBaseline) { SingleLineTextBaselineWithIcon } else { DoubleLineTextBaselineWithIcon } // Total offset between the last text baseline and the bottom of the Tab layout val textOffset = with(density) { baselineOffset.roundToPx() + PrimaryNavigationTabTokens.ActiveIndicatorHeight.roundToPx() } // How much space there is between the top of the icon (essentially the top of this layout) // and the top of the text layout's bounding box (not baseline) val iconOffset = with(density) { iconPlaceable.height + IconDistanceFromBaseline.roundToPx() - firstBaseline } val textPlaceableX = (tabWidth - textPlaceable.width) / 2 val textPlaceableY = tabHeight - lastBaseline - textOffset textPlaceable.placeRelative(textPlaceableX, textPlaceableY) val iconPlaceableX = (tabWidth - iconPlaceable.width) / 2 val iconPlaceableY = textPlaceableY - iconOffset iconPlaceable.placeRelative(iconPlaceableX, iconPlaceableY) } // Tab specifications private val SmallTabHeight = PrimaryNavigationTabTokens.ContainerHeight private val LargeTabHeight = 72.dp // The horizontal padding on the left and right of text internal val HorizontalTextPadding = 16.dp // Distance from the top of the indicator to the text baseline when there is one line of text and an // icon private val SingleLineTextBaselineWithIcon = 14.dp // Distance from the top of the indicator to the last text baseline when there are two lines of text // and an icon private val DoubleLineTextBaselineWithIcon = 6.dp // Distance from the first text baseline to the bottom of the icon in a combined tab private val IconDistanceFromBaseline = 20.sp // Distance from the end of the leading icon to the start of the text private val TextDistanceFromLeadingIcon = 8.dp ``` ## File: compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/TabRow.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.material3 import androidx.collection.mutableIntListOf import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.requiredHeight import androidx.compose.foundation.layout.requiredWidth import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectableGroup import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.material3.tokens.MotionSchemeKeyTokens import androidx.compose.material3.tokens.PrimaryNavigationTabTokens import androidx.compose.material3.tokens.SecondaryNavigationTabTokens import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasureResult import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.layout.layout import androidx.compose.ui.node.LayoutModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.debugInspectorInfo import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastFold import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastMap import kotlin.math.max import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch /** * [Material Design fixed primary tabs](https://m3.material.io/components/tabs/overview) * * Primary tabs are placed at the top of the content pane under a top app bar. They display the main * content destinations. Fixed tabs display all tabs in a set simultaneously. They are best for * switching between related content quickly, such as between transportation methods in a map. To * navigate between fixed tabs, tap an individual tab, or swipe left or right in the content area. * * A TabRow contains a row of [Tab]s, and displays an indicator underneath the currently selected * tab. A TabRow places its tabs evenly spaced along the entire row, with each tab taking up an * equal amount of space. See [PrimaryScrollableTabRow] for a tab row that does not enforce equal * size, and allows scrolling to tabs that do not fit on screen. * * A simple example with text tabs looks like: * * @sample androidx.compose.material3.samples.PrimaryTextTabs * * You can also provide your own custom tab, such as: * * @sample androidx.compose.material3.samples.FancyTabs * * Where the custom tab itself could look like: * * @sample androidx.compose.material3.samples.FancyTab * * As well as customizing the tab, you can also provide a custom [indicator], to customize the * indicator displayed for a tab. [indicator] will be placed to fill the entire TabRow, so it should * internally take care of sizing and positioning the indicator to match changes to * [selectedTabIndex]. * * For example, given an indicator that draws a rounded rectangle near the edges of the [Tab]: * * @sample androidx.compose.material3.samples.FancyIndicator * * We can reuse [TabRowDefaults.tabIndicatorOffset] and just provide this indicator, as we aren't * changing how the size and position of the indicator changes between tabs: * * @sample androidx.compose.material3.samples.FancyIndicatorTabs * * You may also want to use a custom transition, to allow you to dynamically change the appearance * of the indicator as it animates between tabs, such as changing its color or size. [indicator] is * stacked on top of the entire TabRow, so you just need to provide a custom transition that * animates the offset of the indicator from the start of the TabRow. For example, take the * following example that uses a transition to animate the offset, width, and color of the same * FancyIndicator from before, also adding a physics based 'spring' effect to the indicator in the * direction of motion: * * @sample androidx.compose.material3.samples.FancyAnimatedIndicatorWithModifier * * We can now just pass this indicator directly to TabRow: * * @sample androidx.compose.material3.samples.FancyIndicatorContainerTabs * @param selectedTabIndex the index of the currently selected tab * @param modifier the [Modifier] to be applied to this tab row * @param containerColor the color used for the background of this tab row. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this tab row. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param indicator the indicator that represents which tab is currently selected. By default this * will be a [TabRowDefaults.PrimaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] * modifier to animate its position. * @param divider the divider displayed at the bottom of the tab row. This provides a layer of * separation between the tab row and the content displayed underneath. * @param tabs the tabs inside this tab row. Typically this will be multiple [Tab]s. Each element * inside this lambda will be measured and placed evenly across the row, each taking up equal * space. */ @Composable fun PrimaryTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, containerColor: Color = TabRowDefaults.primaryContainerColor, contentColor: Color = TabRowDefaults.primaryContentColor, indicator: @Composable TabIndicatorScope.() -> Unit = { TabRowDefaults.PrimaryIndicator( modifier = Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = true), width = Dp.Unspecified, ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, ) { TabRowImpl(modifier, containerColor, contentColor, indicator, divider, tabs) } /** * [Material Design fixed secondary tabs](https://m3.material.io/components/tabs/overview) * * Secondary tabs are used within a content area to further separate related content and establish * hierarchy. Fixed tabs display all tabs in a set simultaneously. To navigate between fixed tabs, * tap an individual tab, or swipe left or right in the content area. * * A TabRow contains a row of [Tab]s, and displays an indicator underneath the currently selected * tab. A Fixed TabRow places its tabs evenly spaced along the entire row, with each tab taking up * an equal amount of space. See [SecondaryScrollableTabRow] for a tab row that does not enforce * equal size, and allows scrolling to tabs that do not fit on screen. * * A simple example with text tabs looks like: * * @sample androidx.compose.material3.samples.SecondaryTextTabs * @param selectedTabIndex the index of the currently selected tab * @param modifier the [Modifier] to be applied to this tab row * @param containerColor the color used for the background of this tab row. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this tab row. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param indicator the indicator that represents which tab is currently selected. By default this * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] * modifier to animate its position. Note that this indicator will be forced to fill up the entire * tab row, so you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual * drawn indicator inside this space, and provide an offset from the start. * @param divider the divider displayed at the bottom of the tab row. This provides a layer of * separation between the tab row and the content displayed underneath. * @param tabs the tabs inside this tab row. Typically this will be multiple [Tab]s. Each element * inside this lambda will be measured and placed evenly across the row, each taking up equal * space. */ @Composable fun SecondaryTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, containerColor: Color = TabRowDefaults.secondaryContainerColor, contentColor: Color = TabRowDefaults.secondaryContentColor, indicator: @Composable TabIndicatorScope.() -> Unit = @Composable { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = false) ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, ) { TabRowImpl(modifier, containerColor, contentColor, indicator, divider, tabs) } /** * [Material Design scrollable primary tabs](https://m3.material.io/components/tabs/overview) * * Primary tabs are placed at the top of the content pane under a top app bar. They display the main * content destinations. When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable * tabs can use longer text labels and a larger number of tabs. They are best used for browsing on * touch interfaces. * * A scrollable tab row contains a row of [Tab]s, and displays an indicator underneath the currently * selected tab. A scrollable tab row places its tabs offset from the starting edge, and allows * scrolling to tabs that are placed off screen. For a fixed tab row that does not allow scrolling, * and evenly places its tabs, see [PrimaryTabRow]. * * @param selectedTabIndex the index of the currently selected tab * @param modifier the [Modifier] to be applied to this tab row * @param scrollState the [ScrollState] of this tab row * @param containerColor the color used for the background of this tab row. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this tab row. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param edgePadding the padding between the starting and ending edge of the scrollable tab row, * and the tabs inside the row. This padding helps inform the user that this tab row can be * scrolled, unlike a [TabRow]. * @param indicator the indicator that represents which tab is currently selected. By default this * will be a [TabRowDefaults.PrimaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] * modifier to animate its position. * @param divider the divider displayed at the bottom of the tab row. This provides a layer of * separation between the tab row and the content displayed underneath. * @param minTabWidth the minimum width for a [Tab] in this tab row regardless of content size. * @param tabs the tabs inside this tab row. Typically this will be multiple [Tab]s. Each element * inside this lambda will be measured and placed evenly across the row, each taking up equal * space. */ @Composable fun PrimaryScrollableTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, scrollState: ScrollState = rememberScrollState(), containerColor: Color = TabRowDefaults.primaryContainerColor, contentColor: Color = TabRowDefaults.primaryContentColor, edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding, indicator: @Composable TabIndicatorScope.() -> Unit = @Composable { TabRowDefaults.PrimaryIndicator( Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = true), width = Dp.Unspecified, ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, minTabWidth: Dp = TabRowDefaults.ScrollableTabRowMinTabWidth, tabs: @Composable () -> Unit, ) { ScrollableTabRowImpl( selectedTabIndex = selectedTabIndex, indicator = indicator, modifier = modifier, containerColor = containerColor, contentColor = contentColor, edgePadding = edgePadding, minTabWidth = minTabWidth, divider = divider, tabs = tabs, scrollState = scrollState, ) } /** * [Material Design scrollable secondary tabs](https://m3.material.io/components/tabs/overview) * * Material Design scrollable tabs. * * Secondary tabs are used within a content area to further separate related content and establish * hierarchy. When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use * longer text labels and a larger number of tabs. They are best used for browsing on touch * interfaces. * * A scrollable tab row contains a row of [Tab]s, and displays an indicator underneath the currently * selected tab. A scrollable tab row places its tabs offset from the starting edge, and allows * scrolling to tabs that are placed off screen. For a fixed tab row that does not allow scrolling, * and evenly places its tabs, see [SecondaryTabRow]. * * @param selectedTabIndex the index of the currently selected tab * @param modifier the [Modifier] to be applied to this tab row * @param scrollState the [ScrollState] of this tab row * @param containerColor the color used for the background of this tab row. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this tab row. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param edgePadding the padding between the starting and ending edge of the scrollable tab row, * and the tabs inside the row. This padding helps inform the user that this tab row can be * scrolled, unlike a [TabRow]. * @param indicator the indicator that represents which tab is currently selected. By default this * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] * modifier to animate its position. Note that this indicator will be forced to fill up the entire * tab row, so you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual * drawn indicator inside this space, and provide an offset from the start. * @param divider the divider displayed at the bottom of the tab row. This provides a layer of * separation between the tab row and the content displayed underneath. * @param minTabWidth the minimum width for a [Tab] in this tab row regardless of content size. * @param tabs the tabs inside this tab row. Typically this will be multiple [Tab]s. Each element * inside this lambda will be measured and placed evenly across the row, each taking up equal * space. */ @Composable fun SecondaryScrollableTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, scrollState: ScrollState = rememberScrollState(), containerColor: Color = TabRowDefaults.secondaryContainerColor, contentColor: Color = TabRowDefaults.secondaryContentColor, edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding, indicator: @Composable TabIndicatorScope.() -> Unit = @Composable { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = false) ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, minTabWidth: Dp = TabRowDefaults.ScrollableTabRowMinTabWidth, tabs: @Composable () -> Unit, ) { ScrollableTabRowImpl( selectedTabIndex = selectedTabIndex, indicator = indicator, modifier = modifier, containerColor = containerColor, contentColor = contentColor, edgePadding = edgePadding, minTabWidth = minTabWidth, divider = divider, tabs = tabs, scrollState = scrollState, ) } /** * Scope for the composable used to render a Tab indicator, this can be used for more complex * indicators requiring layout information about the tabs like [TabRowDefaults.PrimaryIndicator] and * [TabRowDefaults.SecondaryIndicator] */ interface TabIndicatorScope { /** * A layout modifier that provides tab positions, this can be used to animate and layout a * TabIndicator depending on size, position, and content size of each Tab. * * @sample androidx.compose.material3.samples.FancyAnimatedIndicatorWithModifier */ fun Modifier.tabIndicatorLayout( measure: MeasureScope.(Measurable, Constraints, List) -> MeasureResult ): Modifier /** * A Modifier that follows the default offset and animation * * @param selectedTabIndex the index of the current selected tab * @param matchContentSize this modifier can also animate the width of the indicator to match * the content size of the tab. */ fun Modifier.tabIndicatorOffset( selectedTabIndex: Int, matchContentSize: Boolean = false, ): Modifier } internal interface TabPositionsHolder { fun setTabPositions(positions: List) } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun TabRowImpl( modifier: Modifier, containerColor: Color, contentColor: Color, indicator: @Composable TabIndicatorScope.() -> Unit, divider: @Composable () -> Unit, tabs: @Composable () -> Unit, ) { Surface( modifier = modifier.selectableGroup(), color = containerColor, contentColor = contentColor, ) { // TODO Load the motionScheme tokens from the component tokens file val tabIndicatorAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val scope = remember { object : TabIndicatorScope, TabPositionsHolder { val tabPositions = mutableStateOf<(List)>(listOf()) override fun Modifier.tabIndicatorLayout( measure: MeasureScope.(Measurable, Constraints, List) -> MeasureResult ): Modifier = this.layout { measurable: Measurable, constraints: Constraints -> measure(measurable, constraints, tabPositions.value) } override fun Modifier.tabIndicatorOffset( selectedTabIndex: Int, matchContentSize: Boolean, ): Modifier = this.then( TabIndicatorModifier( tabPositions, selectedTabIndex, matchContentSize, tabIndicatorAnimationSpec, ) ) override fun setTabPositions(positions: List) { tabPositions.value = positions } } } Layout( modifier = Modifier.fillMaxWidth(), contents = listOf(tabs, divider, { scope.indicator() }), ) { (tabMeasurables, dividerMeasurables, indicatorMeasurables), constraints -> val tabRowWidth = constraints.maxWidth val tabCount = tabMeasurables.size var tabWidth = 0 if (tabCount > 0) { tabWidth = (tabRowWidth / tabCount) } val tabRowHeight = tabMeasurables.fastFold(initial = 0) { max, curr -> maxOf(curr.maxIntrinsicHeight(tabWidth), max) } scope.setTabPositions( List(tabCount) { index -> var contentWidth = minOf(tabMeasurables[index].maxIntrinsicWidth(tabRowHeight), tabWidth) .toDp() contentWidth -= HorizontalTextPadding * 2 // Enforce minimum touch target of 24.dp val indicatorWidth = maxOf(contentWidth, 24.dp) TabPosition(tabWidth.toDp() * index, tabWidth.toDp(), indicatorWidth) } ) val tabPlaceables = tabMeasurables.fastMap { it.measure( constraints.copy( minWidth = tabWidth, maxWidth = tabWidth, minHeight = tabRowHeight, maxHeight = tabRowHeight, ) ) } val dividerPlaceables = dividerMeasurables.fastMap { it.measure(constraints.copy(minHeight = 0)) } val indicatorPlaceables = indicatorMeasurables.fastMap { it.measure( constraints.copy( minWidth = tabWidth, maxWidth = tabWidth, minHeight = 0, maxHeight = tabRowHeight, ) ) } layout(tabRowWidth, tabRowHeight) { tabPlaceables.fastForEachIndexed { index, placeable -> placeable.placeRelative(index * tabWidth, 0) } dividerPlaceables.fastForEach { placeable -> placeable.placeRelative(0, tabRowHeight - placeable.height) } indicatorPlaceables.fastForEach { it.placeRelative(0, tabRowHeight - it.height) } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ScrollableTabRowImpl( selectedTabIndex: Int, modifier: Modifier, containerColor: Color, contentColor: Color, edgePadding: Dp, minTabWidth: Dp, scrollState: ScrollState, indicator: @Composable TabIndicatorScope.() -> Unit, divider: @Composable () -> Unit, tabs: @Composable () -> Unit, ) { Surface(modifier = modifier, color = containerColor, contentColor = contentColor) { val coroutineScope = rememberCoroutineScope() // TODO Load the motionScheme tokens from the component tokens file val scrollAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val tabIndicatorAnimationSpec: FiniteAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val scrollableTabData = remember(scrollState, coroutineScope) { ScrollableTabData( scrollState = scrollState, coroutineScope = coroutineScope, animationSpec = scrollAnimationSpec, ) } val scope = remember { object : TabIndicatorScope, TabPositionsHolder { val tabPositions = mutableStateOf<(List)>(listOf()) override fun Modifier.tabIndicatorLayout( measure: MeasureScope.(Measurable, Constraints, List) -> MeasureResult ): Modifier = this.layout { measurable: Measurable, constraints: Constraints -> measure(measurable, constraints, tabPositions.value) } override fun Modifier.tabIndicatorOffset( selectedTabIndex: Int, matchContentSize: Boolean, ): Modifier = this.then( TabIndicatorModifier( tabPositions, selectedTabIndex, matchContentSize, tabIndicatorAnimationSpec, ) ) override fun setTabPositions(positions: List) { tabPositions.value = positions } } } Box(contentAlignment = Alignment.BottomStart) { divider() Layout( contents = listOf(tabs, { scope.indicator() }), modifier = Modifier.fillMaxWidth() .wrapContentSize(align = Alignment.CenterStart) .horizontalScroll(scrollState) .selectableGroup() .clipToBounds(), ) { (tabMeasurables, indicatorMeasurables), constraints -> val padding = edgePadding.roundToPx() val tabCount = tabMeasurables.size val layoutHeight = tabMeasurables.fastFold(initial = 0) { curr, measurable -> maxOf(curr, measurable.maxIntrinsicHeight(Constraints.Infinity)) } var layoutWidth = padding * 2 val tabConstraints = constraints.copy( minWidth = minTabWidth.roundToPx(), minHeight = layoutHeight, maxHeight = layoutHeight, ) var left = edgePadding val tabPlaceables = tabMeasurables.fastMap { it.measure(tabConstraints) } // Get indicator widths based on incoming content size, not based on forced minimum // width applied below. val indicatorWidth = mutableIntListOf() tabMeasurables.fastForEach { indicatorWidth.add(it.maxIntrinsicWidth(Constraints.Infinity)) } val positions = List(tabCount) { index -> val tabWidth = maxOf(minTabWidth, tabPlaceables[index].width.toDp()) layoutWidth += tabWidth.roundToPx() // Enforce minimum touch target of 24.dp val contentWidth = maxOf(indicatorWidth[index].toDp() - (HorizontalTextPadding * 2), 24.dp) val tabPosition = TabPosition(left = left, width = tabWidth, contentWidth = contentWidth) left += tabWidth tabPosition } scope.setTabPositions(positions) val indicatorPlaceables = indicatorMeasurables.fastMap { it.measure( constraints.copy( minWidth = 0, maxWidth = positions[selectedTabIndex].contentWidth.roundToPx(), minHeight = 0, maxHeight = layoutHeight, ) ) } layout(layoutWidth, layoutHeight) { left = edgePadding tabPlaceables.fastForEachIndexed { index, placeable -> placeable.placeRelative(left.roundToPx(), 0) left += positions[index].width } indicatorPlaceables.fastForEach { val relativeOffset = max(0, (positions[selectedTabIndex].width.roundToPx() - it.width) / 2) it.placeRelative(relativeOffset, layoutHeight - it.height) } scrollableTabData.onLaidOut( density = this@Layout, edgeOffset = padding, tabPositions = positions, selectedTab = selectedTabIndex, ) } } } } } internal data class TabIndicatorModifier( val tabPositionsState: State>, val selectedTabIndex: Int, val followContentSize: Boolean, val animationSpec: FiniteAnimationSpec, ) : ModifierNodeElement() { override fun create(): TabIndicatorOffsetNode { return TabIndicatorOffsetNode( tabPositionsState = tabPositionsState, selectedTabIndex = selectedTabIndex, followContentSize = followContentSize, animationSpec = animationSpec, ) } override fun update(node: TabIndicatorOffsetNode) { node.tabPositionsState = tabPositionsState node.selectedTabIndex = selectedTabIndex node.followContentSize = followContentSize node.animationSpec = animationSpec } override fun InspectorInfo.inspectableProperties() { // Show nothing in the inspector. } } internal class TabIndicatorOffsetNode( var tabPositionsState: State>, var selectedTabIndex: Int, var followContentSize: Boolean, var animationSpec: FiniteAnimationSpec, ) : Modifier.Node(), LayoutModifierNode { private var offsetAnimatable: Animatable? = null private var widthAnimatable: Animatable? = null private var initialOffset: Dp? = null private var initialWidth: Dp? = null override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { if (tabPositionsState.value.isEmpty()) { return layout(0, 0) {} } val currentTabWidth = if (followContentSize) { tabPositionsState.value[selectedTabIndex].contentWidth } else { tabPositionsState.value[selectedTabIndex].width } if (initialWidth != null) { val widthAnim = widthAnimatable ?: Animatable(initialWidth!!, Dp.VectorConverter).also { widthAnimatable = it } if (currentTabWidth != widthAnim.targetValue) { coroutineScope.launch { widthAnim.animateTo(currentTabWidth, animationSpec) } } } else { initialWidth = currentTabWidth } val indicatorOffset = tabPositionsState.value[selectedTabIndex].left if (initialOffset != null) { val offsetAnim = offsetAnimatable ?: Animatable(initialOffset!!, Dp.VectorConverter).also { offsetAnimatable = it } if (indicatorOffset != offsetAnim.targetValue) { coroutineScope.launch { offsetAnim.animateTo(indicatorOffset, animationSpec) } } } else { initialOffset = indicatorOffset } val offset = if (layoutDirection == LayoutDirection.Ltr) { offsetAnimatable?.value ?: indicatorOffset } else { -(offsetAnimatable?.value ?: indicatorOffset) } val width = widthAnimatable?.value ?: currentTabWidth val placeable = measurable.measure( constraints.copy(minWidth = width.roundToPx(), maxWidth = width.roundToPx()) ) return layout(placeable.width, placeable.height) { placeable.place(offset.roundToPx(), 0) } } } @Suppress("ComposableLambdaInMeasurePolicy") @Composable private fun TabRowWithSubcomposeImpl( modifier: Modifier, containerColor: Color, contentColor: Color, indicator: @Composable (tabPositions: List) -> Unit, divider: @Composable () -> Unit, tabs: @Composable () -> Unit, ) { Surface( modifier = modifier.selectableGroup(), color = containerColor, contentColor = contentColor, ) { SubcomposeLayout(Modifier.fillMaxWidth()) { constraints -> val tabRowWidth = constraints.maxWidth val tabMeasurables = subcompose(TabSlots.Tabs, tabs) val tabCount = tabMeasurables.size var tabWidth = 0 if (tabCount > 0) { tabWidth = (tabRowWidth / tabCount) } val tabRowHeight = tabMeasurables.fastFold(initial = 0) { max, curr -> maxOf(curr.maxIntrinsicHeight(tabWidth), max) } val tabPlaceables = tabMeasurables.fastMap { it.measure( constraints.copy( minWidth = tabWidth, maxWidth = tabWidth, minHeight = tabRowHeight, maxHeight = tabRowHeight, ) ) } val tabPositions = List(tabCount) { index -> var contentWidth = minOf(tabMeasurables[index].maxIntrinsicWidth(tabRowHeight), tabWidth) .toDp() contentWidth -= HorizontalTextPadding * 2 // Enforce minimum touch target of 24.dp val indicatorWidth = maxOf(contentWidth, 24.dp) TabPosition(tabWidth.toDp() * index, tabWidth.toDp(), indicatorWidth) } layout(tabRowWidth, tabRowHeight) { tabPlaceables.fastForEachIndexed { index, placeable -> placeable.placeRelative(index * tabWidth, 0) } subcompose(TabSlots.Divider, divider).fastForEach { val placeable = it.measure(constraints.copy(minHeight = 0)) placeable.placeRelative(0, tabRowHeight - placeable.height) } subcompose(TabSlots.Indicator) { indicator(tabPositions) } .fastForEach { it.measure(Constraints.fixed(tabRowWidth, tabRowHeight)).placeRelative(0, 0) } } } } } @Suppress("ComposableLambdaInMeasurePolicy") @Composable private fun ScrollableTabRowWithSubcomposeImpl( selectedTabIndex: Int, indicator: @Composable (tabPositions: List) -> Unit, modifier: Modifier = Modifier, containerColor: Color = TabRowDefaults.primaryContainerColor, contentColor: Color = TabRowDefaults.primaryContentColor, edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, scrollState: ScrollState, ) { Surface(modifier = modifier, color = containerColor, contentColor = contentColor) { val coroutineScope = rememberCoroutineScope() // TODO Load the motionScheme tokens from the component tokens file val scrollAnimationSpec = MotionSchemeKeyTokens.DefaultSpatial.value() val scrollableTabData = remember(scrollState, coroutineScope) { ScrollableTabData( scrollState = scrollState, coroutineScope = coroutineScope, animationSpec = scrollAnimationSpec, ) } SubcomposeLayout( Modifier.fillMaxWidth() .wrapContentSize(align = Alignment.CenterStart) .horizontalScroll(scrollState) .selectableGroup() .clipToBounds() ) { constraints -> val minTabWidth = TabRowDefaults.ScrollableTabRowMinTabWidth.roundToPx() val padding = edgePadding.roundToPx() val tabMeasurables = subcompose(TabSlots.Tabs, tabs) val layoutHeight = tabMeasurables.fastFold(initial = 0) { curr, measurable -> maxOf(curr, measurable.maxIntrinsicHeight(Constraints.Infinity)) } val tabConstraints = constraints.copy( minWidth = minTabWidth, minHeight = layoutHeight, maxHeight = layoutHeight, ) val tabPlaceables = mutableListOf() val tabContentWidths = mutableListOf() tabMeasurables.fastForEach { val placeable = it.measure(tabConstraints) var contentWidth = minOf(it.maxIntrinsicWidth(placeable.height), placeable.width).toDp() contentWidth -= HorizontalTextPadding * 2 tabPlaceables.add(placeable) tabContentWidths.add(contentWidth) } val layoutWidth = tabPlaceables.fastFold(initial = padding * 2) { curr, measurable -> curr + measurable.width } // Position the children. layout(layoutWidth, layoutHeight) { // Place the tabs val tabPositions = mutableListOf() var left = padding tabPlaceables.fastForEachIndexed { index, placeable -> placeable.placeRelative(left, 0) tabPositions.add( TabPosition( left = left.toDp(), width = placeable.width.toDp(), contentWidth = tabContentWidths[index], ) ) left += placeable.width } // The divider is measured with its own height, and width equal to the total width // of the tab row, and then placed on top of the tabs. subcompose(TabSlots.Divider, divider).fastForEach { val placeable = it.measure( constraints.copy( minHeight = 0, minWidth = layoutWidth, maxWidth = layoutWidth, ) ) placeable.placeRelative(0, layoutHeight - placeable.height) } // The indicator container is measured to fill the entire space occupied by the tab // row, and then placed on top of the divider. subcompose(TabSlots.Indicator) { indicator(tabPositions) } .fastForEach { it.measure(Constraints.fixed(layoutWidth, layoutHeight)).placeRelative(0, 0) } scrollableTabData.onLaidOut( density = this@SubcomposeLayout, edgeOffset = padding, tabPositions = tabPositions, selectedTab = selectedTabIndex, ) } } } } /** * Data class that contains information about a tab's position on screen, used for calculating where * to place the indicator that shows which tab is selected. * * @property left the left edge's x position from the start of the [TabRow] * @property right the right edge's x position from the start of the [TabRow] * @property width the width of this tab * @property contentWidth the content width of this tab. Should be a minimum of 24.dp */ @Immutable class TabPosition internal constructor(val left: Dp, val width: Dp, val contentWidth: Dp) { val right: Dp get() = left + width override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is TabPosition) return false if (left != other.left) return false if (width != other.width) return false if (contentWidth != other.contentWidth) return false return true } override fun hashCode(): Int { var result = left.hashCode() result = 31 * result + width.hashCode() result = 31 * result + contentWidth.hashCode() return result } override fun toString(): String { return "TabPosition(left=$left, right=$right, width=$width, contentWidth=$contentWidth)" } } /** Contains default implementations and values used for TabRow. */ object TabRowDefaults { /** * The default minimum width for a tab in a [PrimaryScrollableTabRow] or * [SecondaryScrollableTabRow]. */ val ScrollableTabRowMinTabWidth = 90.dp /** * The default padding from the starting edge before a tab in a [PrimaryScrollableTabRow] or * [SecondaryScrollableTabRow]. */ val ScrollableTabRowEdgeStartPadding = 52.dp /** Default container color of a tab row. */ @Deprecated( message = "Use TabRowDefaults.primaryContainerColor instead", replaceWith = ReplaceWith("primaryContainerColor"), ) val containerColor: Color @Composable get() = PrimaryNavigationTabTokens.ContainerColor.value /** Default container color of a [PrimaryTabRow]. */ val primaryContainerColor: Color @Composable get() = PrimaryNavigationTabTokens.ContainerColor.value /** Default container color of a [SecondaryTabRow]. */ val secondaryContainerColor: Color @Composable get() = SecondaryNavigationTabTokens.ContainerColor.value /** Default content color of a tab row. */ @Deprecated( message = "Use TabRowDefaults.primaryContentColor instead", replaceWith = ReplaceWith("primaryContentColor"), ) val contentColor: Color @Composable get() = PrimaryNavigationTabTokens.ActiveLabelTextColor.value /** Default content color of a [PrimaryTabRow]. */ val primaryContentColor: Color @Composable get() = PrimaryNavigationTabTokens.ActiveLabelTextColor.value /** Default content color of a [SecondaryTabRow]. */ val secondaryContentColor: Color @Composable get() = SecondaryNavigationTabTokens.ActiveLabelTextColor.value /** * Default indicator, which will be positioned at the bottom of the [TabRow], on top of the * divider. * * @param modifier modifier for the indicator's layout * @param height height of the indicator * @param color color of the indicator */ @Composable @Deprecated( message = "Use SecondaryIndicator instead.", replaceWith = ReplaceWith("SecondaryIndicator(modifier, height, color)"), ) fun Indicator( modifier: Modifier = Modifier, height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight, color: Color = MaterialTheme.colorScheme.fromToken(PrimaryNavigationTabTokens.ActiveIndicatorColor), ) { Box(modifier.fillMaxWidth().height(height).background(color = color)) } /** * Primary indicator, which will be positioned at the bottom of the [TabRow], on top of the * divider. * * @param modifier modifier for the indicator's layout * @param width width of the indicator * @param height height of the indicator * @param color color of the indicator * @param shape shape of the indicator */ @Composable fun PrimaryIndicator( modifier: Modifier = Modifier, width: Dp = 24.dp, height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight, color: Color = PrimaryNavigationTabTokens.ActiveIndicatorColor.value, shape: Shape = PrimaryNavigationTabTokens.ActiveIndicatorShape, ) { Spacer( modifier .requiredHeight(height) .requiredWidth(width) .background(color = color, shape = shape) ) } /** * Secondary indicator, which will be positioned at the bottom of the [TabRow], on top of the * divider. * * @param modifier modifier for the indicator's layout * @param height height of the indicator * @param color color of the indicator */ @Composable fun SecondaryIndicator( modifier: Modifier = Modifier, height: Dp = PrimaryNavigationTabTokens.ActiveIndicatorHeight, color: Color = PrimaryNavigationTabTokens.ActiveIndicatorColor.value, ) { Box(modifier.fillMaxWidth().height(height).background(color = color)) } /** * [Modifier] that takes up all the available width inside the [TabRow], and then animates the * offset of the indicator it is applied to, depending on the [currentTabPosition]. * * @param currentTabPosition [TabPosition] of the currently selected tab. This is used to * calculate the offset of the indicator this modifier is applied to, as well as its width. */ @Deprecated( level = DeprecationLevel.WARNING, message = "Solely for use alongside deprecated TabRowDefaults.Indicator method. For " + "recommended PrimaryIndicator and SecondaryIndicator methods, please use " + "TabIndicatorScope.tabIndicatorOffset method.", ) fun Modifier.tabIndicatorOffset(currentTabPosition: TabPosition): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "tabIndicatorOffset" value = currentTabPosition } ) { // TODO Load the motionScheme tokens from the component tokens file val currentTabWidth by animateDpAsState( targetValue = currentTabPosition.width, animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value(), ) val indicatorOffset by animateDpAsState( targetValue = currentTabPosition.left, animationSpec = MotionSchemeKeyTokens.DefaultSpatial.value(), ) fillMaxWidth() .wrapContentSize(Alignment.BottomStart) .offset { IntOffset(x = indicatorOffset.roundToPx(), y = 0) } .width(currentTabWidth) } } private enum class TabSlots { Tabs, Divider, Indicator, } /** Class holding onto state needed for [ScrollableTabRow] */ private class ScrollableTabData( private val scrollState: ScrollState, private val coroutineScope: CoroutineScope, private val animationSpec: FiniteAnimationSpec, ) { private var selectedTab: Int? = null fun onLaidOut( density: Density, edgeOffset: Int, tabPositions: List, selectedTab: Int, ) { // Animate if the new tab is different from the old tab, or this is called for the first // time (i.e selectedTab is `null`). if (this.selectedTab != selectedTab) { this.selectedTab = selectedTab tabPositions.getOrNull(selectedTab)?.let { // Scrolls to the tab with [tabPosition], trying to place it in the center of the // screen or as close to the center as possible. val calculatedOffset = it.calculateTabOffset(density, edgeOffset, tabPositions) if (scrollState.value != calculatedOffset) { coroutineScope.launch { scrollState.animateScrollTo(calculatedOffset, animationSpec = animationSpec) } } } } } /** * @return the offset required to horizontally center the tab inside this TabRow. If the tab is * at the start / end, and there is not enough space to fully centre the tab, this will just * clamp to the min / max position given the max width. */ private fun TabPosition.calculateTabOffset( density: Density, edgeOffset: Int, tabPositions: List, ): Int = with(density) { val totalTabRowWidth = tabPositions.last().right.roundToPx() + edgeOffset val visibleWidth = totalTabRowWidth - scrollState.maxValue val tabOffset = left.roundToPx() val scrollerCenter = visibleWidth / 2 val tabWidth = width.roundToPx() val centeredTabOffset = tabOffset - (scrollerCenter - tabWidth / 2) // How much space we have to scroll. If the visible width is <= to the total width, then // we have no space to scroll as everything is always visible. val availableSpace = (totalTabRowWidth - visibleWidth).coerceAtLeast(0) return centeredTabOffset.coerceIn(0, availableSpace) } } @Deprecated(level = DeprecationLevel.HIDDEN, message = "Maintained for Binary Compatibility.") @Composable fun PrimaryScrollableTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, scrollState: ScrollState = rememberScrollState(), containerColor: Color = TabRowDefaults.primaryContainerColor, contentColor: Color = TabRowDefaults.primaryContentColor, edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding, indicator: @Composable TabIndicatorScope.() -> Unit = @Composable { TabRowDefaults.PrimaryIndicator( Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = true), width = Dp.Unspecified, ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, ) = PrimaryScrollableTabRow( selectedTabIndex = selectedTabIndex, modifier = modifier, scrollState = scrollState, containerColor = containerColor, contentColor = contentColor, edgePadding = edgePadding, indicator = indicator, divider = divider, minTabWidth = TabRowDefaults.ScrollableTabRowMinTabWidth, tabs = tabs, ) @Deprecated(level = DeprecationLevel.HIDDEN, message = "Maintained for Binary Compatibility.") @Composable fun SecondaryScrollableTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, scrollState: ScrollState = rememberScrollState(), containerColor: Color = TabRowDefaults.secondaryContainerColor, contentColor: Color = TabRowDefaults.secondaryContentColor, edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding, indicator: @Composable TabIndicatorScope.() -> Unit = @Composable { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(selectedTabIndex, matchContentSize = false) ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, ) = SecondaryScrollableTabRow( selectedTabIndex = selectedTabIndex, modifier = modifier, scrollState = scrollState, containerColor = containerColor, contentColor = contentColor, edgePadding = edgePadding, indicator = indicator, divider = divider, minTabWidth = TabRowDefaults.ScrollableTabRowMinTabWidth, tabs = tabs, ) /** * [Material Design tabs](https://m3.material.io/components/tabs/overview) * * Material Design fixed tabs. * * For primary indicator tabs, use [PrimaryTabRow]. For secondary indicator tabs, use * [SecondaryTabRow]. * * Fixed tabs display all tabs in a set simultaneously. They are best for switching between related * content quickly, such as between transportation methods in a map. To navigate between fixed tabs, * tap an individual tab, or swipe left or right in the content area. * * A TabRow contains a row of [Tab]s, and displays an indicator underneath the currently selected * tab. A TabRow places its tabs evenly spaced along the entire row, with each tab taking up an * equal amount of space. See [ScrollableTabRow] for a tab row that does not enforce equal size, and * allows scrolling to tabs that do not fit on screen. * * A simple example with text tabs looks like: * * @sample androidx.compose.material3.samples.TextTabs * * You can also provide your own custom tab, such as: * * @sample androidx.compose.material3.samples.FancyTabs * * Where the custom tab itself could look like: * * @sample androidx.compose.material3.samples.FancyTab * * As well as customizing the tab, you can also provide a custom [indicator], to customize the * indicator displayed for a tab. [indicator] will be placed to fill the entire TabRow, so it should * internally take care of sizing and positioning the indicator to match changes to * [selectedTabIndex]. * * For example, given an indicator that draws a rounded rectangle near the edges of the [Tab]: * * @sample androidx.compose.material3.samples.FancyIndicator * * We can reuse [TabRowDefaults.tabIndicatorOffset] and just provide this indicator, as we aren't * changing how the size and position of the indicator changes between tabs: * * @sample androidx.compose.material3.samples.FancyIndicatorTabs * * You may also want to use a custom transition, to allow you to dynamically change the appearance * of the indicator as it animates between tabs, such as changing its color or size. [indicator] is * stacked on top of the entire TabRow, so you just need to provide a custom transition that * animates the offset of the indicator from the start of the TabRow. For example, take the * following example that uses a transition to animate the offset, width, and color of the same * FancyIndicator from before, also adding a physics based 'spring' effect to the indicator in the * direction of motion: * * @sample androidx.compose.material3.samples.FancyAnimatedIndicatorWithModifier * * We can now just pass this indicator directly to TabRow: * * @sample androidx.compose.material3.samples.FancyIndicatorContainerTabs * @param selectedTabIndex the index of the currently selected tab * @param modifier the [Modifier] to be applied to this tab row * @param containerColor the color used for the background of this tab row. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this tab row. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param indicator the indicator that represents which tab is currently selected. By default this * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] * modifier to animate its position. Note that this indicator will be forced to fill up the entire * tab row, so you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual * drawn indicator inside this space, and provide an offset from the start. * @param divider the divider displayed at the bottom of the tab row. This provides a layer of * separation between the tab row and the content displayed underneath. * @param tabs the tabs inside this tab row. Typically this will be multiple [Tab]s. Each element * inside this lambda will be measured and placed evenly across the row, each taking up equal * space. */ @Composable @Deprecated( level = DeprecationLevel.WARNING, message = "Replaced with PrimaryTabRow and SecondaryTabRow.", replaceWith = ReplaceWith( "SecondaryTabRow(selectedTabIndex, modifier, containerColor, contentColor, indicator, divider, tabs)" ), ) @Suppress("DEPRECATION") fun TabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, containerColor: Color = TabRowDefaults.primaryContainerColor, contentColor: Color = TabRowDefaults.primaryContentColor, indicator: @Composable (tabPositions: List) -> Unit = @Composable { tabPositions -> if (selectedTabIndex < tabPositions.size) { TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]) ) } }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, ) { TabRowWithSubcomposeImpl(modifier, containerColor, contentColor, indicator, divider, tabs) } /** * [Material Design tabs](https://m3.material.io/components/tabs/overview) * * Material Design scrollable tabs. * * For primary indicator tabs, use [PrimaryScrollableTabRow]. For secondary indicator tabs, use * [SecondaryScrollableTabRow]. * * When a set of tabs cannot fit on screen, use scrollable tabs. Scrollable tabs can use longer text * labels and a larger number of tabs. They are best used for browsing on touch interfaces. * * A ScrollableTabRow contains a row of [Tab]s, and displays an indicator underneath the currently * selected tab. A ScrollableTabRow places its tabs offset from the starting edge, and allows * scrolling to tabs that are placed off screen. For a fixed tab row that does not allow scrolling, * and evenly places its tabs, see [TabRow]. * * @param selectedTabIndex the index of the currently selected tab * @param modifier the [Modifier] to be applied to this tab row * @param containerColor the color used for the background of this tab row. Use [Color.Transparent] * to have no color. * @param contentColor the preferred color for content inside this tab row. Defaults to either the * matching content color for [containerColor], or to the current [LocalContentColor] if * [containerColor] is not a color from the theme. * @param edgePadding the padding between the starting and ending edge of the scrollable tab row, * and the tabs inside the row. This padding helps inform the user that this tab row can be * scrolled, unlike a [TabRow]. * @param indicator the indicator that represents which tab is currently selected. By default this * will be a [TabRowDefaults.SecondaryIndicator], using a [TabRowDefaults.tabIndicatorOffset] * modifier to animate its position. Note that this indicator will be forced to fill up the entire * tab row, so you should use [TabRowDefaults.tabIndicatorOffset] or similar to animate the actual * drawn indicator inside this space, and provide an offset from the start. * @param divider the divider displayed at the bottom of the tab row. This provides a layer of * separation between the tab row and the content displayed underneath. * @param tabs the tabs inside this tab row. Typically this will be multiple [Tab]s. Each element * inside this lambda will be measured and placed evenly across the row, each taking up equal * space. */ @Composable @Deprecated( level = DeprecationLevel.WARNING, message = "Replaced with PrimaryScrollableTabRow and SecondaryScrollableTabRow tab variants.", replaceWith = ReplaceWith( "SecondaryScrollableTabRow(selectedTabIndex, modifier, containerColor, contentColor, edgePadding, indicator, divider, tabs)" ), ) @Suppress("DEPRECATION") fun ScrollableTabRow( selectedTabIndex: Int, modifier: Modifier = Modifier, containerColor: Color = TabRowDefaults.primaryContainerColor, contentColor: Color = TabRowDefaults.primaryContentColor, edgePadding: Dp = TabRowDefaults.ScrollableTabRowEdgeStartPadding, indicator: @Composable (tabPositions: List) -> Unit = @Composable { tabPositions -> TabRowDefaults.SecondaryIndicator( Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex]) ) }, divider: @Composable () -> Unit = @Composable { HorizontalDivider() }, tabs: @Composable () -> Unit, ) { ScrollableTabRowWithSubcomposeImpl( selectedTabIndex = selectedTabIndex, indicator = indicator, modifier = modifier, containerColor = containerColor, contentColor = contentColor, edgePadding = edgePadding, divider = divider, tabs = tabs, scrollState = rememberScrollState(), ) } ``` ================================================ FILE: .claude/skills/compose-expert/references/source-code/navigation-source.md ================================================ # Navigation Compose Source Reference ## File: navigation/navigation-compose/src/androidMain/kotlin/androidx/navigation/compose/NavHostController.android.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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. */ @file:JvmName("NavHostControllerKt") @file:JvmMultifileClass package androidx.navigation.compose import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.platform.LocalContext import androidx.navigation.NavDestination import androidx.navigation.NavHostController import androidx.navigation.Navigator @Composable public actual fun rememberNavController( vararg navigators: Navigator ): NavHostController { val context = LocalContext.current return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) { createNavController(context) } .apply { for (navigator in navigators) { navigatorProvider.addNavigator(navigator) } } } private fun createNavController(context: Context) = NavHostController(context).apply { navigatorProvider.addNavigator(ComposeNavGraphNavigator(navigatorProvider)) navigatorProvider.addNavigator(ComposeNavigator()) navigatorProvider.addNavigator(DialogNavigator()) } /** Saver to save and restore the NavController across config change and process death. */ private fun NavControllerSaver(context: Context): Saver = Saver( save = { it.saveState() }, restore = { createNavController(context).apply { restoreState(it) } }, ) ``` ## File: navigation/navigation-compose/src/androidMain/kotlin/androidx/navigation/compose/internal/NavComposeUtils.android.kt ```kotlin /* * Copyright 2025 The Android Open Source Project * * 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 androidx.navigation.compose.internal import androidx.activity.compose.PredictiveBackHandler import androidx.compose.runtime.Composable import java.lang.ref.WeakReference import java.util.UUID import kotlinx.coroutines.flow.Flow internal actual typealias BackEventCompat = androidx.activity.BackEventCompat @Composable internal actual fun PredictiveBackHandler( enabled: Boolean, onBack: suspend (progress: Flow) -> Unit, ) { PredictiveBackHandler(enabled, onBack) } internal actual fun randomUUID(): String = UUID.randomUUID().toString() /** * Class WeakReference encapsulates weak reference to an object, which could be used to either * retrieve a strong reference to an object, or return null, if object was already destroyed by the * memory manager. */ internal actual class WeakReference actual constructor(reference: T) { private val weakReference = WeakReference(reference) actual fun get(): T? = weakReference.get() actual fun clear() = weakReference.clear() } internal actual typealias DefaultNavTransitions = StandardDefaultNavTransitions ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/ComposeNavGraphNavigator.kt ```kotlin /* * Copyright 2023 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.navigation.NavBackStackEntry import androidx.navigation.NavGraph import androidx.navigation.NavGraphNavigator import androidx.navigation.Navigator import androidx.navigation.NavigatorProvider import kotlin.jvm.JvmSuppressWildcards /** * Custom subclass of [NavGraphNavigator] that adds support for defining transitions at the * navigation graph level. */ @Navigator.Name("navigation") internal class ComposeNavGraphNavigator(navigatorProvider: NavigatorProvider) : NavGraphNavigator(navigatorProvider) { override fun createDestination(): NavGraph { return ComposeNavGraph(this) } internal class ComposeNavGraph(navGraphNavigator: Navigator) : NavGraph(navGraphNavigator) { internal var enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null internal var exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null internal var popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null internal var popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null internal var predictivePopEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> EnterTransition?)? = null internal var predictivePopExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> ExitTransition?)? = null internal var sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = null } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/ComposeNavigator.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination import androidx.navigation.NavOptions import androidx.navigation.Navigator import androidx.navigation.compose.ComposeNavigator.Destination import kotlin.jvm.JvmSuppressWildcards import kotlinx.coroutines.flow.StateFlow /** * Navigator that navigates through [Composable]s. Every destination using this Navigator must set a * valid [Composable] by setting it directly on an instantiated [Destination] or calling * [composable]. */ @Navigator.Name("composable") public class ComposeNavigator constructor() : Navigator(NAME) { /** Get the map of transitions currently in progress from the [state]. */ internal val transitionsInProgress get() = state.transitionsInProgress /** Get the back stack from the [state]. */ public val backStack: StateFlow> get() = state.backStack internal val isPop = mutableStateOf(false) override fun navigate( entries: List, navOptions: NavOptions?, navigatorExtras: Extras?, ) { entries.forEach { entry -> state.pushWithTransition(entry) } isPop.value = false } override fun createDestination(): Destination { return Destination(this) {} } override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { state.popWithTransition(popUpTo, savedState) isPop.value = true } /** * Function to prepare the entry for transition. * * This should be called when the entry needs to move the [androidx.lifecycle.Lifecycle.State] * in preparation for a transition such as when using predictive back. */ public fun prepareForTransition(entry: NavBackStackEntry) { state.prepareForTransition(entry) } /** * Callback to mark a navigation in transition as complete. * * This should be called in conjunction with [navigate] and [popBackStack] as those calls merely * start a transition to the target destination, and requires manually marking the transition as * complete by calling this method. * * Failing to call this method could result in entries being prevented from reaching their final * [androidx.lifecycle.Lifecycle.State]. */ public fun onTransitionComplete(entry: NavBackStackEntry) { state.markTransitionComplete(entry) } /** NavDestination specific to [ComposeNavigator] */ @NavDestination.ClassType(Composable::class) public class Destination( navigator: ComposeNavigator, internal val content: @Composable AnimatedContentScope.(@JvmSuppressWildcards NavBackStackEntry) -> Unit, ) : NavDestination(navigator) { @Deprecated( message = "Deprecated in favor of Destination that supports AnimatedContent", level = DeprecationLevel.HIDDEN, ) public constructor( navigator: ComposeNavigator, content: @Composable (NavBackStackEntry) -> @JvmSuppressWildcards Unit, ) : this(navigator, content = { entry -> content(entry) }) internal var enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null internal var exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null internal var popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null internal var popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null internal var predictivePopEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> EnterTransition?)? = null internal var predictivePopExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> ExitTransition?)? = null internal var sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = null } internal companion object { internal const val NAME = "composable" } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/ComposeNavigatorDestinationBuilder.kt ```kotlin /* * Copyright 2024 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestinationBuilder import androidx.navigation.NavDestinationDsl import androidx.navigation.NavType import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass import kotlin.reflect.KType /** DSL for constructing a new [ComposeNavigator.Destination] */ @NavDestinationDsl public class ComposeNavigatorDestinationBuilder : NavDestinationBuilder { private val composeNavigator: ComposeNavigator private val content: @Composable (AnimatedContentScope.(NavBackStackEntry) -> Unit) public var enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null public var exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null public var popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null public var popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null public var sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = null /** * DSL for constructing a new [ComposeNavigator.Destination] * * @param navigator navigator used to create the destination * @param route the destination's unique route * @param content composable for the destination */ public constructor( navigator: ComposeNavigator, route: String, content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) : super(navigator, route) { this.composeNavigator = navigator this.content = content } /** * DSL for constructing a new [ComposeNavigator.Destination] * * @param navigator navigator used to create the destination * @param route the destination's unique route from a [KClass] * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param content composable for the destination */ public constructor( navigator: ComposeNavigator, route: KClass<*>, typeMap: Map>, content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) : super(navigator, route, typeMap) { this.composeNavigator = navigator this.content = content } override fun instantiateDestination(): ComposeNavigator.Destination { return ComposeNavigator.Destination(composeNavigator, content) } override fun build(): ComposeNavigator.Destination { return super.build().also { destination -> destination.enterTransition = enterTransition destination.exitTransition = exitTransition destination.popEnterTransition = popEnterTransition destination.popExitTransition = popExitTransition destination.sizeTransform = sizeTransform } } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/DialogHost.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.window.Dialog import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.DialogNavigator.Destination /** * Show each [Destination] on the [DialogNavigator]'s back stack as a [Dialog]. * * Note that [NavHost] will call this for you; you do not need to call it manually. */ @Composable public fun DialogHost(dialogNavigator: DialogNavigator) { val saveableStateHolder = rememberSaveableStateHolder() val dialogBackStack by dialogNavigator.backStack.collectAsState() val visibleBackStack = rememberVisibleList(dialogBackStack) visibleBackStack.PopulateVisibleList(dialogBackStack) val transitionInProgress by dialogNavigator.transitionInProgress.collectAsState() val dialogsToDispose = remember { mutableStateListOf() } visibleBackStack.forEach { backStackEntry -> val destination = backStackEntry.destination as Destination Dialog( onDismissRequest = { dialogNavigator.dismiss(backStackEntry) }, properties = destination.dialogProperties, ) { DisposableEffect(backStackEntry) { dialogsToDispose.add(backStackEntry) onDispose { dialogNavigator.onTransitionComplete(backStackEntry) dialogsToDispose.remove(backStackEntry) } } // while in the scope of the composable, we provide the navBackStackEntry as the // ViewModelStoreOwner and LifecycleOwner backStackEntry.LocalOwnersProvider(saveableStateHolder) { destination.content(backStackEntry) } } } // Dialogs may have been popped before it was composed. To prevent leakage, we need to // mark popped entries as complete here. Check that we don't accidentally complete popped // entries that were composed, unless they were disposed of already. LaunchedEffect(transitionInProgress, dialogsToDispose) { transitionInProgress.forEach { entry -> if ( !dialogNavigator.backStack.value.contains(entry) && !dialogsToDispose.contains(entry) ) { dialogNavigator.onTransitionComplete(entry) } } } } @Composable internal fun MutableList.PopulateVisibleList( backStack: Collection ) { val isInspecting = LocalInspectionMode.current backStack.forEach { entry -> DisposableEffect(entry.lifecycle) { val observer = LifecycleEventObserver { _, event -> // show dialog in preview if (isInspecting && !contains(entry)) { add(entry) } // ON_START -> add to visibleBackStack, ON_STOP -> remove from visibleBackStack if (event == Lifecycle.Event.ON_START) { // We want to treat the visible lists as Sets but we want to keep // the functionality of mutableStateListOf() so that we recompose in response // to adds and removes. if (!contains(entry)) { add(entry) } } if (event == Lifecycle.Event.ON_STOP) { remove(entry) } } entry.lifecycle.addObserver(observer) onDispose { entry.lifecycle.removeObserver(observer) } } } } @Composable internal fun rememberVisibleList( backStack: Collection ): SnapshotStateList { // show dialog in preview val isInspecting = LocalInspectionMode.current return remember(backStack) { mutableStateListOf().also { it.addAll( backStack.filter { entry -> if (isInspecting) { true } else { entry.lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) } } ) } } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/DialogNavigator.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.runtime.Composable import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.navigation.FloatingWindow import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination import androidx.navigation.NavOptions import androidx.navigation.Navigator import androidx.navigation.compose.DialogNavigator.Destination /** * Navigator that navigates through [Composable]s that will be hosted within a [Dialog]. Every * destination using this Navigator must set a valid [Composable] by setting it directly on an * instantiated [Destination] or calling [dialog]. */ @Navigator.Name("dialog") public class DialogNavigator() : Navigator(NAME) { /** Get the back stack from the [state]. */ internal val backStack get() = state.backStack /** Get the transitioning dialogs from the [state]. */ internal val transitionInProgress get() = state.transitionsInProgress /** Dismiss the dialog destination associated with the given [backStackEntry]. */ internal fun dismiss(backStackEntry: NavBackStackEntry) { popBackStack(backStackEntry, false) } override fun navigate( entries: List, navOptions: NavOptions?, navigatorExtras: Extras?, ) { entries.forEach { entry -> state.push(entry) } } override fun createDestination(): Destination { return Destination(this) {} } override fun popBackStack(popUpTo: NavBackStackEntry, savedState: Boolean) { state.popWithTransition(popUpTo, savedState) // When popping, the incoming dialog is marked transitioning to hold it in // STARTED. With pop complete, we can remove it from transition so it can move to RESUMED. val popIndex = state.transitionsInProgress.value.indexOf(popUpTo) // do not mark complete for entries up to and including popUpTo state.transitionsInProgress.value.forEachIndexed { index, entry -> if (index > popIndex) onTransitionComplete(entry) } } internal fun onTransitionComplete(entry: NavBackStackEntry) { state.markTransitionComplete(entry) } /** NavDestination specific to [DialogNavigator] */ @NavDestination.ClassType(Composable::class) public class Destination( navigator: DialogNavigator, internal val dialogProperties: DialogProperties = DialogProperties(), internal val content: @Composable (NavBackStackEntry) -> Unit, ) : NavDestination(navigator), FloatingWindow internal companion object { internal const val NAME = "dialog" } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/DialogNavigatorDestinationBuilder.kt ```kotlin /* * Copyright 2024 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.runtime.Composable import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestinationBuilder import androidx.navigation.NavDestinationDsl import androidx.navigation.NavType import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass import kotlin.reflect.KType /** DSL for constructing a new [DialogNavigator.Destination] */ @NavDestinationDsl public class DialogNavigatorDestinationBuilder : NavDestinationBuilder { private val dialogNavigator: DialogNavigator private val dialogProperties: DialogProperties private val content: @Composable (NavBackStackEntry) -> Unit /** * DSL for constructing a new [DialogNavigator.Destination] * * @param navigator navigator used to create the destination * @param route the destination's unique route * @param dialogProperties properties that should be passed to * [androidx.compose.ui.window.Dialog]. * @param content composable for the destination */ public constructor( navigator: DialogNavigator, route: String, dialogProperties: DialogProperties, content: @Composable (NavBackStackEntry) -> Unit, ) : super(navigator, route) { this.dialogNavigator = navigator this.dialogProperties = dialogProperties this.content = content } /** * DSL for constructing a new [DialogNavigator.Destination] * * @param navigator navigator used to create the destination * @param route the destination's unique route from a [KClass] * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param dialogProperties properties that should be passed to * [androidx.compose.ui.window.Dialog]. * @param content composable for the destination */ public constructor( navigator: DialogNavigator, route: KClass<*>, typeMap: Map>, dialogProperties: DialogProperties, content: @Composable (NavBackStackEntry) -> Unit, ) : super(navigator, route, typeMap) { this.dialogNavigator = navigator this.dialogProperties = dialogProperties this.content = content } override fun instantiateDestination(): DialogNavigator.Destination { return DialogNavigator.Destination(dialogNavigator, dialogProperties, content) } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavBackStackEntryProvider.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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. */ @file:JvmName("NavBackStackEntryProviderKt") package androidx.navigation.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.saveable.SaveableStateHolder import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.createSavedStateHandle import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavBackStackEntry import androidx.navigation.compose.internal.WeakReference import androidx.navigation.compose.internal.randomUUID import androidx.savedstate.compose.LocalSavedStateRegistryOwner import kotlin.jvm.JvmName /** * Provides [this] [NavBackStackEntry] as [LocalViewModelStoreOwner], [LocalLifecycleOwner] and * [LocalSavedStateRegistryOwner] to the [content] and saves the [content]'s saveable states with * the given [saveableStateHolder]. * * @param saveableStateHolder The [SaveableStateHolder] that holds the saved states. The same holder * should be used for all [NavBackStackEntry]s in the encapsulating [Composable] and the holder * should be hoisted. * @param content The content [Composable] */ @Composable public fun NavBackStackEntry.LocalOwnersProvider( saveableStateHolder: SaveableStateHolder, content: @Composable () -> Unit, ) { // This outer `CompositionLocalProvider` explicitly provides the owners from this // `NavBackStackEntry` directly to the `SaveableStateProvider`. This prevents potential issues, // such as in testing scenarios, where these owners might not be set. CompositionLocalProvider( LocalViewModelStoreOwner provides this, LocalLifecycleOwner provides this, LocalSavedStateRegistryOwner provides this, ) { saveableStateHolder.SaveableStateProvider { // This inner `CompositionLocalProvider`, located inside the `SaveableStateProvider` // lambda, ensures that the `content` composable receives the correct owners // from this `NavBackStackEntry`. This layering prevents unintended owner overrides // by `SaveableStateProvider` and ensures the destination content correctly interacts // with its navigation-scoped owners. CompositionLocalProvider( LocalViewModelStoreOwner provides this, LocalLifecycleOwner provides this, LocalSavedStateRegistryOwner provides this, content = content, ) } } } @Composable private fun SaveableStateHolder.SaveableStateProvider(content: @Composable () -> Unit) { val viewModel = viewModel { BackStackEntryIdViewModel(createSavedStateHandle()) } // Stash a reference to the SaveableStateHolder in the ViewModel so that // it is available when the ViewModel is cleared, marking the permanent removal of this // NavBackStackEntry from the back stack. Which, because of animations, // only happens after this leaves composition. Which means we can't rely on // DisposableEffect to clean up this reference (as it'll be cleaned up too early) viewModel.saveableStateHolderRef = WeakReference(this) SaveableStateProvider(viewModel.id, content) } internal class BackStackEntryIdViewModel(handle: SavedStateHandle) : ViewModel() { private val IdKey = "SaveableStateHolder_BackStackEntryKey" // we create our own id for each back stack entry to support multiple entries of the same // destination. this id will be restored by SavedStateHandle val id: String = (handle.get(IdKey) ?: randomUUID().also { handle.set(IdKey, it) }) lateinit var saveableStateHolderRef: WeakReference // onCleared will be called on the entries removed from the back stack. here we notify // SaveableStateProvider that we should remove any state is had associated with this // destination as it is no longer needed. override fun onCleared() { super.onCleared() saveableStateHolderRef.get()?.removeState(id) saveableStateHolderRef.clear() } } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavGraphBuilder.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.compose.runtime.Composable import androidx.compose.ui.window.DialogProperties import androidx.navigation.NamedNavArgument import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDeepLink import androidx.navigation.NavGraph import androidx.navigation.NavGraphBuilder import androidx.navigation.NavType import androidx.navigation.get import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass import kotlin.reflect.KType /** * Add the [Composable] to the [NavGraphBuilder] * * @param route route for the destination * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param content composable for the destination */ @Deprecated( message = "Deprecated in favor of composable builder that supports AnimatedContent", level = DeprecationLevel.HIDDEN, ) public fun NavGraphBuilder.composable( route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), content: @Composable (NavBackStackEntry) -> Unit, ) { addDestination( ComposeNavigator.Destination(provider[ComposeNavigator::class]) { entry -> content(entry) } .apply { this.route = route arguments.forEach { (argumentName, argument) -> addArgument(argumentName, argument) } deepLinks.forEach { deepLink -> addDeepLink(deepLink) } } ) } /** * Add the [Composable] to the [NavGraphBuilder] * * @param route route for the destination * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to determine the destination's enter transition * @param exitTransition callback to determine the destination's exit transition * @param popEnterTransition callback to determine the destination's popEnter transition * @param popExitTransition callback to determine the destination's popExit transition * @param content composable for the destination */ @Deprecated( message = "Deprecated in favor of composable builder that supports sizeTransform", level = DeprecationLevel.HIDDEN, ) public fun NavGraphBuilder.composable( route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = exitTransition, content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { destination( ComposeNavigatorDestinationBuilder(provider[ComposeNavigator::class], route, content) .apply { arguments.forEach { (argumentName, argument) -> argument(argumentName, argument) } deepLinks.forEach { deepLink -> deepLink(deepLink) } this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition } ) } /** * Add the [Composable] to the [NavGraphBuilder] * * @param route route for the destination * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to determine the destination's enter transition * @param exitTransition callback to determine the destination's exit transition * @param popEnterTransition callback to determine the destination's popEnter transition * @param popExitTransition callback to determine the destination's popExit transition * @param sizeTransform callback to determine the destination's sizeTransform. * @param content composable for the destination * @sample androidx.navigation.compose.samples.SizeTransformComposable */ public fun NavGraphBuilder.composable( route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = exitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = null, content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { destination( ComposeNavigatorDestinationBuilder(provider[ComposeNavigator::class], route, content) .apply { arguments.forEach { (argumentName, argument) -> argument(argumentName, argument) } deepLinks.forEach { deepLink -> deepLink(deepLink) } this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition this.sizeTransform = sizeTransform } ) } /** * Add the [Composable] to the [NavGraphBuilder] * * @param T route from a [KClass] for the destination * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [T] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to determine the destination's enter transition * @param exitTransition callback to determine the destination's exit transition * @param popEnterTransition callback to determine the destination's popEnter transition * @param popExitTransition callback to determine the destination's popExit transition * @param sizeTransform callback to determine the destination's sizeTransform. * @param content composable for the destination */ public inline fun NavGraphBuilder.composable( typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), noinline enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = null, noinline exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = null, noinline popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, noinline popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, noinline sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { composable( T::class, typeMap, deepLinks, enterTransition, exitTransition, popEnterTransition, popExitTransition, sizeTransform, content, ) } /** * Add the [Composable] to the [NavGraphBuilder] * * @param route route from a [KClass] for the destination * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to determine the destination's enter transition * @param exitTransition callback to determine the destination's exit transition * @param popEnterTransition callback to determine the destination's popEnter transition * @param popExitTransition callback to determine the destination's popExit transition * @param sizeTransform callback to determine the destination's sizeTransform. * @param content composable for the destination */ public fun NavGraphBuilder.composable( route: KClass, typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = null, exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = null, popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit, ) { destination( ComposeNavigatorDestinationBuilder( provider[ComposeNavigator::class], route, typeMap, content, ) .apply { deepLinks.forEach { deepLink -> deepLink(deepLink) } this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition this.sizeTransform = sizeTransform } ) } /** * Construct a nested [NavGraph] * * @sample androidx.navigation.compose.samples.NavWithArgsInNestedGraph * @param startDestination the starting destination's route for this NavGraph * @param route the destination's unique route * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param builder the builder used to construct the graph */ @Deprecated( message = "Deprecated in favor of navigation builder that supports AnimatedContent", level = DeprecationLevel.HIDDEN, ) public fun NavGraphBuilder.navigation( startDestination: String, route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), builder: NavGraphBuilder.() -> Unit, ) { navigation(startDestination, route, arguments, deepLinks, null, null, null, null, null, builder) } /** * Construct a nested [NavGraph] * * @param startDestination the starting destination's route for this NavGraph * @param route the destination's unique route * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to define enter transitions for destination in this NavGraph * @param exitTransition callback to define exit transitions for destination in this NavGraph * @param popEnterTransition callback to define pop enter transitions for destination in this * NavGraph * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph * @param builder the builder used to construct the graph * @return the newly constructed nested NavGraph */ @Deprecated( message = "Deprecated in favor of navigation builder that supports sizeTransform", level = DeprecationLevel.HIDDEN, ) public fun NavGraphBuilder.navigation( startDestination: String, route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = null, exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = null, popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition?)? = enterTransition, popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition?)? = exitTransition, builder: NavGraphBuilder.() -> Unit, ) { navigation( startDestination, route, arguments, deepLinks, enterTransition, exitTransition, popEnterTransition, popExitTransition, null, builder, ) } /** * Construct a nested [NavGraph] * * @param startDestination the starting destination's route for this NavGraph * @param route the destination's unique route * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to define enter transitions for destination in this NavGraph * @param exitTransition callback to define exit transitions for destination in this NavGraph * @param popEnterTransition callback to define pop enter transitions for destination in this * NavGraph * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph * @param sizeTransform callback to define the size transform for destinations in this NavGraph * @param builder the builder used to construct the graph * @return the newly constructed nested NavGraph */ public fun NavGraphBuilder.navigation( startDestination: String, route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = null, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = null, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition?)? = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition?)? = exitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = null, builder: NavGraphBuilder.() -> Unit, ) { addDestination( NavGraphBuilder(provider, startDestination, route).apply(builder).build().apply { arguments.forEach { (argumentName, argument) -> addArgument(argumentName, argument) } deepLinks.forEach { deepLink -> addDeepLink(deepLink) } if (this is ComposeNavGraphNavigator.ComposeNavGraph) { this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition this.sizeTransform = sizeTransform } } ) } /** * Construct a nested [NavGraph] * * @param T the destination's unique route from a KClass * @param startDestination the starting destination's route from [KClass] for this NavGraph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [T] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to define enter transitions for destination in this NavGraph * @param exitTransition callback to define exit transitions for destination in this NavGraph * @param popEnterTransition callback to define pop enter transitions for destination in this * NavGraph * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph * @param sizeTransform callback to define the size transform for destinations in this NavGraph * @param builder the builder used to construct the graph * @return the newly constructed nested NavGraph * @sample androidx.navigation.compose.samples.SizeTransformNav */ public inline fun NavGraphBuilder.navigation( startDestination: KClass<*>, typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), noinline enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = null, noinline exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = null, noinline popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, noinline popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, noinline sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, noinline builder: NavGraphBuilder.() -> Unit, ) { navigation( startDestination, T::class, typeMap, deepLinks, enterTransition, exitTransition, popEnterTransition, popExitTransition, sizeTransform, builder, ) } /** * Construct a nested [NavGraph] * * @param route the destination's unique route from a KClass * @param startDestination the starting destination's route from [KClass] for this NavGraph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to define enter transitions for destination in this NavGraph * @param exitTransition callback to define exit transitions for destination in this NavGraph * @param popEnterTransition callback to define pop enter transitions for destination in this * NavGraph * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph * @param sizeTransform callback to define the size transform for destinations in this NavGraph * @param builder the builder used to construct the graph * @return the newly constructed nested NavGraph */ public fun NavGraphBuilder.navigation( startDestination: KClass<*>, route: KClass, typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = null, exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = null, popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, builder: NavGraphBuilder.() -> Unit, ) { addDestination( NavGraphBuilder(provider, startDestination, route, typeMap).apply(builder).build().apply { deepLinks.forEach { deepLink -> addDeepLink(deepLink) } if (this is ComposeNavGraphNavigator.ComposeNavGraph) { this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition this.sizeTransform = sizeTransform } } ) } /** * Construct a nested [NavGraph] * * @param T the destination's unique route from a KClass * @param startDestination the starting destination's route from an Object for this NavGraph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [T] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to define enter transitions for destination in this NavGraph * @param exitTransition callback to define exit transitions for destination in this NavGraph * @param popEnterTransition callback to define pop enter transitions for destination in this * NavGraph * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph * @param sizeTransform callback to define the size transform for destinations in this NavGraph * @param builder the builder used to construct the graph * @return the newly constructed nested NavGraph */ public inline fun NavGraphBuilder.navigation( startDestination: Any, typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), noinline enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = null, noinline exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = null, noinline popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, noinline popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, noinline sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, noinline builder: NavGraphBuilder.() -> Unit, ) { navigation( startDestination, T::class, typeMap, deepLinks, enterTransition, exitTransition, popEnterTransition, popExitTransition, sizeTransform, builder, ) } /** * Construct a nested [NavGraph] * * @param route the destination's unique route from a KClass * @param startDestination the starting destination's route from an Object for this NavGraph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param enterTransition callback to define enter transitions for destination in this NavGraph * @param exitTransition callback to define exit transitions for destination in this NavGraph * @param popEnterTransition callback to define pop enter transitions for destination in this * NavGraph * @param popExitTransition callback to define pop exit transitions for destination in this NavGraph * @param sizeTransform callback to define the size transform for destinations in this NavGraph * @param builder the builder used to construct the graph * @return the newly constructed nested NavGraph */ public fun NavGraphBuilder.navigation( startDestination: Any, route: KClass, typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), enterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = null, exitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = null, popEnterTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards EnterTransition?)? = enterTransition, popExitTransition: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards ExitTransition?)? = exitTransition, sizeTransform: (AnimatedContentTransitionScope.() -> @JvmSuppressWildcards SizeTransform?)? = null, builder: NavGraphBuilder.() -> Unit, ) { addDestination( NavGraphBuilder(provider, startDestination, route, typeMap).apply(builder).build().apply { deepLinks.forEach { deepLink -> addDeepLink(deepLink) } if (this is ComposeNavGraphNavigator.ComposeNavGraph) { this.enterTransition = enterTransition this.exitTransition = exitTransition this.popEnterTransition = popEnterTransition this.popExitTransition = popExitTransition this.sizeTransform = sizeTransform } } ) } /** * Add the [Composable] to the [NavGraphBuilder] that will be hosted within a * [androidx.compose.ui.window.Dialog]. This is suitable only when this dialog represents a separate * screen in your app that needs its own lifecycle and saved state, independent of any other * destination in your navigation graph. For use cases such as `AlertDialog`, you should use those * APIs directly in the [composable] destination that wants to show that dialog. * * @param route route for the destination * @param arguments list of arguments to associate with destination * @param deepLinks list of deep links to associate with the destinations * @param dialogProperties properties that should be passed to [androidx.compose.ui.window.Dialog]. * @param content composable content for the destination that will be hosted within the Dialog */ public fun NavGraphBuilder.dialog( route: String, arguments: List = emptyList(), deepLinks: List = emptyList(), dialogProperties: DialogProperties = DialogProperties(), content: @Composable (NavBackStackEntry) -> Unit, ) { destination( DialogNavigatorDestinationBuilder( provider[DialogNavigator::class], route, dialogProperties, content, ) .apply { arguments.forEach { (argumentName, argument) -> argument(argumentName, argument) } deepLinks.forEach { deepLink -> deepLink(deepLink) } } ) } /** * Add the [Composable] to the [NavGraphBuilder] that will be hosted within a * [androidx.compose.ui.window.Dialog]. This is suitable only when this dialog represents a separate * screen in your app that needs its own lifecycle and saved state, independent of any other * destination in your navigation graph. For use cases such as `AlertDialog`, you should use those * APIs directly in the [composable] destination that wants to show that dialog. * * @param T route from a KClass for the destination * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [T] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param dialogProperties properties that should be passed to [androidx.compose.ui.window.Dialog]. * @param content composable content for the destination that will be hosted within the Dialog */ public inline fun NavGraphBuilder.dialog( typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), dialogProperties: DialogProperties = DialogProperties(), noinline content: @Composable (NavBackStackEntry) -> Unit, ) { dialog(T::class, typeMap, deepLinks, dialogProperties, content) } /** * Add the [Composable] to the [NavGraphBuilder] that will be hosted within a * [androidx.compose.ui.window.Dialog]. This is suitable only when this dialog represents a separate * screen in your app that needs its own lifecycle and saved state, independent of any other * destination in your navigation graph. For use cases such as `AlertDialog`, you should use those * APIs directly in the [composable] destination that wants to show that dialog. * * @param route route from [KClass] of [T] for the destination * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param deepLinks list of deep links to associate with the destinations * @param dialogProperties properties that should be passed to [androidx.compose.ui.window.Dialog]. * @param content composable content for the destination that will be hosted within the Dialog */ public fun NavGraphBuilder.dialog( route: KClass, typeMap: Map> = emptyMap(), deepLinks: List = emptyList(), dialogProperties: DialogProperties = DialogProperties(), content: @Composable (NavBackStackEntry) -> Unit, ) { destination( DialogNavigatorDestinationBuilder( provider[DialogNavigator::class], route, typeMap, dialogProperties, content, ) .apply { deepLinks.forEach { deepLink -> deepLink(deepLink) } } ) } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavHost.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.navigation.compose import androidx.collection.mutableObjectFloatMapOf import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.SeekableTransitionState import androidx.compose.animation.core.animate import androidx.compose.animation.core.rememberTransition import androidx.compose.animation.core.tween import androidx.compose.animation.togetherWith import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.navigation.NavBackStackEntry import androidx.navigation.NavDestination import androidx.navigation.NavDestination.Companion.hierarchy import androidx.navigation.NavGraph import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.Navigator import androidx.navigation.compose.internal.DefaultNavTransitions import androidx.navigation.compose.internal.PredictiveBackHandler import androidx.navigation.createGraph import androidx.navigation.get import kotlin.coroutines.cancellation.CancellationException import kotlin.jvm.JvmSuppressWildcards import kotlin.reflect.KClass import kotlin.reflect.KType import kotlinx.coroutines.launch /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @sample androidx.navigation.compose.samples.NavScaffold * @param navController the navController for this host * @param startDestination the route for the start destination * @param modifier The modifier to be applied to the layout. * @param route the route for the graph * @param builder the builder used to construct the graph */ @Deprecated( message = "Deprecated in favor of NavHost that supports AnimatedContent", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, route: String? = null, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, remember(route, startDestination, builder) { navController.createGraph(startDestination, route, builder) }, modifier, ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route for the graph * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param builder the builder used to construct the graph */ @Deprecated( message = "Deprecated in favor of NavHost that supports sizeTransform", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: String? = null, enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, remember(route, startDestination, builder) { navController.createGraph(startDestination, route, builder) }, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition, ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route for the graph * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param sizeTransform callback to define the size transform for destinations in this host * @param builder the builder used to construct the graph */ @Deprecated( message = "Deprecated in favor of NavHost that supports predictivePopEnterTransition and predictivePopExitTransition", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: String? = null, enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, startDestination, modifier, contentAlignment, route, enterTransition, exitTransition, popEnterTransition, popExitTransition, sizeTransform = sizeTransform, builder = builder, ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route for the graph * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param predictivePopEnterTransition callback to define predictivePopEnter transitions for * destination in this host * @param predictivePopExitTransition callback to define predictivePopExit transitions for * destination in this host * @param sizeTransform callback to define the size transform for destinations in this host * @param builder the builder used to construct the graph */ @Composable public fun NavHost( navController: NavHostController, startDestination: String, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: String? = null, enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, predictivePopEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> EnterTransition) = DefaultNavTransitions.predictivePopEnterTransition, predictivePopExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> ExitTransition) = DefaultNavTransitions.predictivePopExitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, remember(route, startDestination, builder) { navController.createGraph(startDestination, route, builder) }, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition, predictivePopEnterTransition, predictivePopExitTransition, sizeTransform, ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route from a [KClass] for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route from a [KClass] for the graph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param sizeTransform callback to define the size transform for destinations in this host * @param builder the builder used to construct the graph */ @Deprecated( message = "Deprecated in favor of NavHost that supports predictivePopEnterTransition and predictivePopExitTransition", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, startDestination: KClass<*>, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: KClass<*>? = null, typeMap: Map> = emptyMap(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, startDestination, modifier, contentAlignment, route, typeMap, enterTransition, exitTransition, popEnterTransition, popExitTransition, DefaultNavTransitions.predictivePopEnterTransition, DefaultNavTransitions.predictivePopExitTransition, sizeTransform, builder, ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route from a [KClass] for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route from a [KClass] for the graph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param predictivePopEnterTransition callback to define predictivePopEnter transitions for * destination in this host * @param predictivePopExitTransition callback to define predictivePopExit transitions for * destination in this host * @param sizeTransform callback to define the size transform for destinations in this host * @param builder the builder used to construct the graph */ @Composable public fun NavHost( navController: NavHostController, startDestination: KClass<*>, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: KClass<*>? = null, typeMap: Map> = emptyMap(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, predictivePopEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> EnterTransition) = DefaultNavTransitions.predictivePopEnterTransition, predictivePopExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> ExitTransition) = DefaultNavTransitions.predictivePopExitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, remember(route, startDestination, builder) { navController.createGraph(startDestination, route, typeMap, builder) }, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition, predictivePopEnterTransition, predictivePopExitTransition, sizeTransform, ) } /** * Provides in place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route from a an Object for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route from a [KClass] for the graph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param sizeTransform callback to define the size transform for destinations in this host * @param builder the builder used to construct the graph */ @Deprecated( message = "Deprecated in favor of NavHost that supports predictivePopEnterTransition and predictivePopExitTransition", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, startDestination: Any, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: KClass<*>? = null, typeMap: Map> = emptyMap(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, startDestination, modifier, contentAlignment, route, typeMap, enterTransition, exitTransition, popEnterTransition, popExitTransition, DefaultNavTransitions.predictivePopEnterTransition, DefaultNavTransitions.predictivePopExitTransition, sizeTransform, builder, ) } /** * Provides in place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The builder passed into this method is [remember]ed. This means that for this NavHost, the * contents of the builder cannot be changed. * * @param navController the navController for this host * @param startDestination the route from a an Object for the start destination * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param route the route from a [KClass] for the graph * @param typeMap map of destination arguments' kotlin type [KType] to its respective custom * [NavType]. May be empty if [route] does not use custom NavTypes. * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param predictivePopEnterTransition callback to define predictivePopEnter transitions for * destination in this host * @param predictivePopExitTransition callback to define predictivePopExit transitions for * destination in this host * @param sizeTransform callback to define the size transform for destinations in this host * @param builder the builder used to construct the graph */ @Composable public fun NavHost( navController: NavHostController, startDestination: Any, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, route: KClass<*>? = null, typeMap: Map> = emptyMap(), enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, predictivePopEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> EnterTransition) = DefaultNavTransitions.predictivePopEnterTransition, predictivePopExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> ExitTransition) = DefaultNavTransitions.predictivePopExitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, builder: NavGraphBuilder.() -> Unit, ) { NavHost( navController, remember(route, startDestination, builder) { navController.createGraph(startDestination, route, typeMap, builder) }, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition, predictivePopEnterTransition, predictivePopExitTransition, sizeTransform, ) } /** * Provides in place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * The graph passed into this method is [remember]ed. This means that for this NavHost, the graph * cannot be changed. * * @param navController the navController for this host * @param graph the graph for this host * @param modifier The modifier to be applied to the layout. */ @Deprecated( message = "Deprecated in favor of NavHost that supports AnimatedContent", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, graph: NavGraph, modifier: Modifier = Modifier, ): Unit = NavHost(navController, graph, modifier) /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * @param navController the navController for this host * @param graph the graph for this host * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host */ @Deprecated( message = "Deprecated in favor of NavHost that supports sizeTransform", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, graph: NavGraph, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, enterTransition: (AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, ) { NavHost( navController, graph, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition, sizeTransform = null, // sizeTransform ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * @param navController the navController for this host * @param graph the graph for this host * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param sizeTransform callback to define the size transform for destinations in this host */ @Deprecated( message = "Deprecated in favor of NavHost that supports predictivePopEnterTransition and predictivePopExitTransition", level = DeprecationLevel.HIDDEN, ) @Composable public fun NavHost( navController: NavHostController, graph: NavGraph, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, ) { NavHost( navController, graph, modifier, contentAlignment, enterTransition, exitTransition, popEnterTransition, popExitTransition, DefaultNavTransitions.predictivePopEnterTransition, DefaultNavTransitions.predictivePopExitTransition, sizeTransform, ) } /** * Provides a place in the Compose hierarchy for self contained navigation to occur. * * Once this is called, any Composable within the given [NavGraphBuilder] can be navigated to from * the provided [navController]. * * @param navController the navController for this host * @param graph the graph for this host * @param modifier The modifier to be applied to the layout. * @param contentAlignment The [Alignment] of the [AnimatedContent] * @param enterTransition callback to define enter transitions for destination in this host * @param exitTransition callback to define exit transitions for destination in this host * @param popEnterTransition callback to define popEnter transitions for destination in this host * @param popExitTransition callback to define popExit transitions for destination in this host * @param predictivePopEnterTransition callback to define predictivePopEnter transitions for * destination in this host * @param predictivePopExitTransition callback to define predictivePopExit transitions for * destination in this host * @param sizeTransform callback to define the size transform for destinations in this host */ @Composable public fun NavHost( navController: NavHostController, graph: NavGraph, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, enterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = DefaultNavTransitions.enterTransition, exitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = DefaultNavTransitions.exitTransition, popEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> EnterTransition) = enterTransition, popExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> ExitTransition) = exitTransition, predictivePopEnterTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> EnterTransition) = DefaultNavTransitions.predictivePopEnterTransition, predictivePopExitTransition: (@JvmSuppressWildcards AnimatedContentTransitionScope.(Int) -> ExitTransition) = DefaultNavTransitions.predictivePopExitTransition, sizeTransform: (@JvmSuppressWildcards AnimatedContentTransitionScope.() -> SizeTransform?)? = DefaultNavTransitions.sizeTransform, ) { val lifecycleOwner = LocalLifecycleOwner.current val viewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) { "NavHost requires a ViewModelStoreOwner to be provided via LocalViewModelStoreOwner" } navController.setViewModelStore(viewModelStoreOwner.viewModelStore) // Then set the graph navController.graph = graph // Find the ComposeNavigator, returning early if it isn't found // (such as is the case when using TestNavHostController) val composeNavigator = navController.navigatorProvider.get>(ComposeNavigator.NAME) as? ComposeNavigator ?: return val currentBackStack by composeNavigator.backStack.collectAsState() var progress by remember { mutableFloatStateOf(0f) } var inPredictiveBack by remember { mutableStateOf(false) } var swipeEdge by remember { mutableIntStateOf(0) } PredictiveBackHandler(currentBackStack.size > 1) { backEvent -> // This block handles the three phases of a predictive back gesture: // 1. OnStarted: When the gesture begins. // 2. OnProgressed: As the user drags their finger. // 3. OnCompleted or OnCancelled: When the gesture finishes or is cancelled. // // Always guard with `currentBackStack.size > 1`: // If `enabled` becomes stale (set false mid-frame while a gesture is in-flight), // these checks prevent IndexOutOfBounds when accessing the stack. var currentBackStackEntry: NavBackStackEntry? = null // --- OnStarted --- if (currentBackStack.size > 1) { progress = 0f currentBackStackEntry = currentBackStack.lastOrNull() composeNavigator.prepareForTransition(currentBackStackEntry!!) val previousEntry = currentBackStack[currentBackStack.size - 2] composeNavigator.prepareForTransition(previousEntry) } try { backEvent.collect { // --- OnProgressed --- if (currentBackStack.size > 1) { inPredictiveBack = true progress = it.progress swipeEdge = it.swipeEdge } } // --- OnCompleted --- if (currentBackStack.size > 1) { inPredictiveBack = false composeNavigator.popBackStack(currentBackStackEntry!!, false) } } catch (_: CancellationException) { // --- OnCancelled --- if (currentBackStack.size > 1) { inPredictiveBack = false } } } DisposableEffect(lifecycleOwner) { // Setup the navController with proper owners navController.setLifecycleOwner(lifecycleOwner) onDispose {} } val saveableStateHolder = rememberSaveableStateHolder() val allVisibleEntries by navController.visibleEntries.collectAsState() // Intercept back only when there's a destination to pop val visibleEntries by remember { derivedStateOf { allVisibleEntries.filter { entry -> entry.destination.navigatorName == ComposeNavigator.NAME } } } val backStackEntry: NavBackStackEntry? = visibleEntries.lastOrNull() val zIndices = remember { mutableObjectFloatMapOf() } if (backStackEntry != null) { val finalEnter: AnimatedContentTransitionScope.() -> EnterTransition = { val targetDestination = targetState.destination as ComposeNavigator.Destination if (inPredictiveBack) { targetDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createPredictivePopEnterTransition(this, swipeEdge) } ?: predictivePopEnterTransition.invoke(this, swipeEdge) } else if (composeNavigator.isPop.value) { targetDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createPopEnterTransition(this) } ?: popEnterTransition.invoke(this) } else { targetDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createEnterTransition(this) } ?: enterTransition.invoke(this) } } val finalExit: AnimatedContentTransitionScope.() -> ExitTransition = { val initialDestination = initialState.destination as ComposeNavigator.Destination if (inPredictiveBack) { initialDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createPredictivePopExitTransition(this, swipeEdge) } ?: predictivePopExitTransition.invoke(this, swipeEdge) } else if (composeNavigator.isPop.value) { initialDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createPopExitTransition(this) } ?: popExitTransition.invoke(this) } else { initialDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createExitTransition(this) } ?: exitTransition.invoke(this) } } val finalSizeTransform: AnimatedContentTransitionScope.() -> SizeTransform? = { val targetDestination = targetState.destination as ComposeNavigator.Destination targetDestination.hierarchy.firstNotNullOfOrNull { destination -> destination.createSizeTransform(this) } ?: sizeTransform?.invoke(this) } DisposableEffect(true) { onDispose { visibleEntries.forEach { entry -> composeNavigator.onTransitionComplete(entry) } } } val transitionState = remember { // The state returned here cannot be nullable cause it produces the input of the // transitionSpec passed into the AnimatedContent and that must match the non-nullable // scope exposed by the transitions on the NavHost and composable APIs. SeekableTransitionState(backStackEntry) } val transition = rememberTransition(transitionState, label = "entry") if (inPredictiveBack) { LaunchedEffect(progress) { // Update transition progress safely (same guard against stale enabled state). if (currentBackStack.size > 1) { val previousEntry = currentBackStack[currentBackStack.size - 2] transitionState.seekTo(progress, previousEntry) } } } else { LaunchedEffect(backStackEntry) { // This ensures we don't animate after the back gesture is cancelled and we // are already on the current state if (transitionState.currentState != backStackEntry) { transitionState.animateTo(backStackEntry) } else { // convert from nanoseconds to milliseconds val totalDuration = transition.totalDurationNanos / 1000000 // When the predictive back gesture is cancel, we need to manually animate // the SeekableTransitionState from where it left off, to zero and then // snapTo the final position. animate( transitionState.fraction, 0f, animationSpec = tween((transitionState.fraction * totalDuration).toInt()), ) { value, _ -> this@LaunchedEffect.launch { if (value > 0) { // Seek the original transition back to the currentState transitionState.seekTo(value) } if (value == 0f) { // Once we animate to the start, we need to snap to the right state. transitionState.snapTo(backStackEntry) } } } } } } transition.AnimatedContent( modifier, transitionSpec = { // If the initialState of the AnimatedContent is not in visibleEntries, we are in // a case where visible has cleared the old state for some reason, so instead of // attempting to animate away from the initialState, we skip the animation. if (initialState in visibleEntries) { val initialZIndex = zIndices.getOrPut(initialState.id) { 0f } val targetZIndex = when { targetState.id == initialState.id -> initialZIndex composeNavigator.isPop.value || inPredictiveBack -> initialZIndex - 1f else -> initialZIndex + 1f } zIndices[targetState.id] = targetZIndex ContentTransform( finalEnter(this), finalExit(this), targetZIndex, finalSizeTransform(this), ) } else { EnterTransition.None togetherWith ExitTransition.None } }, contentAlignment, contentKey = { it.id }, ) { // In some specific cases, such as clearing your back stack by changing your // start destination, AnimatedContent can contain an entry that is no longer // part of visible entries since it was cleared from the back stack and is not // animating. In these cases the currentEntry will be null, and in those cases, // AnimatedContent will just skip attempting to transition the old entry. // See https://issuetracker.google.com/238686802 val isPredictiveBackCancelAnimation = transitionState.currentState == backStackEntry val currentEntry = if (inPredictiveBack || isPredictiveBackCancelAnimation) { // We have to do this because the previous entry does not show up in // visibleEntries // even if we prepare it above as part of onBackStackChangeStarted it } else { visibleEntries.lastOrNull { entry -> it == entry } } // while in the scope of the composable, we provide the navBackStackEntry as the // ViewModelStoreOwner and LifecycleOwner currentEntry?.LocalOwnersProvider(saveableStateHolder) { (currentEntry.destination as ComposeNavigator.Destination).content( this, currentEntry, ) } } LaunchedEffect(transition.currentState, transition.targetState) { if ( transition.currentState == transition.targetState && // There is a race condition where previous animation has completed the new // animation has yet to start and there is a navigate call before this effect. // We need to make sure we are completing only when the start is settled on the // actual entry. (navController.currentBackStackEntry == null || transition.targetState == backStackEntry) ) { visibleEntries.forEach { entry -> composeNavigator.onTransitionComplete(entry) } zIndices.removeIf { key, _ -> key != transition.targetState.id } } } } val dialogNavigator = navController.navigatorProvider.get>(DialogNavigator.NAME) as? DialogNavigator ?: return // Show any dialog destinations DialogHost(dialogNavigator) } private fun NavDestination.createEnterTransition( scope: AnimatedContentTransitionScope ): EnterTransition? = when (this) { is ComposeNavigator.Destination -> this.enterTransition?.invoke(scope) is ComposeNavGraphNavigator.ComposeNavGraph -> this.enterTransition?.invoke(scope) else -> null } private fun NavDestination.createExitTransition( scope: AnimatedContentTransitionScope ): ExitTransition? = when (this) { is ComposeNavigator.Destination -> this.exitTransition?.invoke(scope) is ComposeNavGraphNavigator.ComposeNavGraph -> this.exitTransition?.invoke(scope) else -> null } private fun NavDestination.createPopEnterTransition( scope: AnimatedContentTransitionScope ): EnterTransition? = when (this) { is ComposeNavigator.Destination -> this.popEnterTransition?.invoke(scope) is ComposeNavGraphNavigator.ComposeNavGraph -> this.popEnterTransition?.invoke(scope) else -> null } private fun NavDestination.createPopExitTransition( scope: AnimatedContentTransitionScope ): ExitTransition? = when (this) { is ComposeNavigator.Destination -> this.popExitTransition?.invoke(scope) is ComposeNavGraphNavigator.ComposeNavGraph -> this.popExitTransition?.invoke(scope) else -> null } private fun NavDestination.createPredictivePopEnterTransition( scope: AnimatedContentTransitionScope, swipeEdge: Int, ): EnterTransition? = when (this) { is ComposeNavigator.Destination -> this.predictivePopEnterTransition?.invoke(scope, swipeEdge) is ComposeNavGraphNavigator.ComposeNavGraph -> this.predictivePopEnterTransition?.invoke(scope, swipeEdge) else -> null } private fun NavDestination.createPredictivePopExitTransition( scope: AnimatedContentTransitionScope, swipeEdge: Int, ): ExitTransition? = when (this) { is ComposeNavigator.Destination -> this.predictivePopExitTransition?.invoke(scope, swipeEdge) is ComposeNavGraphNavigator.ComposeNavGraph -> this.predictivePopExitTransition?.invoke(scope, swipeEdge) else -> null } private fun NavDestination.createSizeTransform( scope: AnimatedContentTransitionScope ): SizeTransform? = when (this) { is ComposeNavigator.Destination -> this.sizeTransform?.invoke(scope) is ComposeNavGraphNavigator.ComposeNavGraph -> this.sizeTransform?.invoke(scope) else -> null } ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/NavHostController.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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. */ @file:JvmName("NavHostControllerKt") @file:JvmMultifileClass package androidx.navigation.compose import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.navigation.NavBackStackEntry import androidx.navigation.NavController import androidx.navigation.NavDestination import androidx.navigation.NavHostController import androidx.navigation.Navigator import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName /** * Gets the current navigation back stack entry as a [MutableState]. When the given navController * changes the back stack due to a [NavController.navigate] or [NavController.popBackStack] this * will trigger a recompose and return the top entry on the back stack. * * @return a mutable state of the current back stack entry */ @Composable public fun NavController.currentBackStackEntryAsState(): State { return currentBackStackEntryFlow.collectAsState(null) } /** * Creates a NavHostController that handles the adding of the [ComposeNavigator] and * [DialogNavigator]. Additional [Navigator] instances can be passed through [navigators] to be * applied to the returned NavController. Note that each [Navigator] must be separately remembered * before being passed in here: any changes to those inputs will cause the NavController to be * recreated. * * @see NavHost */ @Composable public expect fun rememberNavController( vararg navigators: Navigator ): NavHostController ``` ## File: navigation/navigation-compose/src/commonMain/kotlin/androidx/navigation/compose/internal/NavComposeUtils.kt ```kotlin /* * Copyright 2025 The Android Open Source Project * * 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 androidx.navigation.compose.internal import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleOut import androidx.compose.runtime.Composable import androidx.navigation.NavBackStackEntry import kotlinx.coroutines.flow.Flow internal expect class BackEventCompat { val touchX: Float val touchY: Float val progress: Float val swipeEdge: Int } @Composable internal expect fun PredictiveBackHandler( enabled: Boolean = true, onBack: suspend (progress: Flow) -> Unit, ) internal expect fun randomUUID(): String /** * Class WeakReference encapsulates weak reference to an object, which could be used to either * retrieve a strong reference to an object, or return null, if object was already destroyed by the * memory manager. */ internal expect class WeakReference(reference: T) { fun get(): T? fun clear() } internal expect object DefaultNavTransitions { val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition val predictivePopEnterTransition: AnimatedContentTransitionScope.(Int) -> EnterTransition val predictivePopExitTransition: AnimatedContentTransitionScope.(Int) -> ExitTransition val sizeTransform: (AnimatedContentTransitionScope.() -> SizeTransform?)? } internal object StandardDefaultNavTransitions { val enterTransition: AnimatedContentTransitionScope.() -> EnterTransition = { fadeIn(animationSpec = tween(700)) } val exitTransition: AnimatedContentTransitionScope.() -> ExitTransition = { fadeOut(animationSpec = tween(700)) } val predictivePopEnterTransition: AnimatedContentTransitionScope.(Int) -> EnterTransition = { fadeIn( spring( dampingRatio = 1.0f, // reflects material3 motionScheme.defaultEffectsSpec() stiffness = 1600.0f, // reflects material3 motionScheme.defaultEffectsSpec() ) ) } val predictivePopExitTransition: AnimatedContentTransitionScope.(Int) -> ExitTransition = { scaleOut(targetScale = 0.7f) // reflects material3 motionScheme.defaultEffectsSpec() } val sizeTransform: (AnimatedContentTransitionScope.() -> SizeTransform?)? = null } ``` ================================================ FILE: .claude/skills/compose-expert/references/source-code/runtime-source.md ================================================ # Compose Runtime Source Reference ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composable.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.runtime /** * [Composable] functions are the fundamental building blocks of an application built with Compose. * * [Composable] can be applied to a function or lambda to indicate that the function/lambda can be * used as part of a composition to describe a transformation from application data into a tree or * hierarchy. * * Annotating a function or expression with [Composable] changes the type of that function or * expression. For example, [Composable] functions can only ever be called from within another * [Composable] function. A useful mental model for [Composable] functions is that an implicit * "composable context" is passed into a [Composable] function, and is done so implicitly when it is * called from within another [Composable] function. This "context" can be used to store information * from previous executions of the function that happened at the same logical point of the tree. */ @MustBeDocumented @Retention(AnnotationRetention.BINARY) @Target( // function declarations // @Composable fun Foo() { ... } // lambda expressions // val foo = @Composable { ... } AnnotationTarget.FUNCTION, // type declarations // var foo: @Composable () -> Unit = { ... } // parameter types // foo: @Composable () -> Unit AnnotationTarget.TYPE, // composable types inside of type signatures // foo: (@Composable () -> Unit) -> Unit AnnotationTarget.TYPE_PARAMETER, // composable property getters and setters // val foo: Int @Composable get() { ... } // var bar: Int // @Composable get() { ... } AnnotationTarget.PROPERTY_GETTER, ) public annotation class Composable ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composition.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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. */ @file:OptIn(InternalComposeApi::class) package androidx.compose.runtime import androidx.collection.MutableScatterSet import androidx.collection.ObjectList import androidx.collection.ScatterMap import androidx.collection.ScatterSet import androidx.compose.runtime.collection.ScopeMap import androidx.compose.runtime.collection.fastForEach import androidx.compose.runtime.composer.DebugStringFormattable import androidx.compose.runtime.composer.RememberManager import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.asGapBufferSlotTable import androidx.compose.runtime.composer.gapbuffer.changelist.ChangeList import androidx.compose.runtime.composer.linkbuffer.asLinkBufferSlotTable import androidx.compose.runtime.internal.AtomicReference import androidx.compose.runtime.internal.RememberEventDispatcher import androidx.compose.runtime.internal.trace import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import androidx.compose.runtime.snapshots.ReaderKind import androidx.compose.runtime.snapshots.StateObjectImpl import androidx.compose.runtime.snapshots.fastAll import androidx.compose.runtime.snapshots.fastAny import androidx.compose.runtime.tooling.CompositionErrorContextImpl import androidx.compose.runtime.tooling.CompositionObserver import androidx.compose.runtime.tooling.CompositionObserverHandle import androidx.compose.runtime.tooling.ObservableComposition /** * A composition object is usually constructed for you, and returned from an API that is used to * initially compose a UI. For instance, [setContent] returns a Composition. * * The [dispose] method should be used when you would like to dispose of the UI and the Composition. */ public interface Composition { /** * Returns true if any pending invalidations have been scheduled. An invalidation is schedule if * [RecomposeScope.invalidate] has been called on any composition scopes create for the * composition. * * Modifying [MutableState.value] of a value produced by [mutableStateOf] will automatically * call [RecomposeScope.invalidate] for any scope that read [State.value] of the mutable state * instance during composition. * * @see RecomposeScope * @see mutableStateOf */ public val hasInvalidations: Boolean /** True if [dispose] has been called. */ public val isDisposed: Boolean /** * Clear the hierarchy that was created from the composition and release resources allocated for * composition. After calling [dispose] the composition will no longer be recomposed and calling * [setContent] will throw an [IllegalStateException]. Calling [dispose] is idempotent, all * calls after the first are a no-op. */ public fun dispose() /** * Update the composition with the content described by the [content] composable. After this has * been called the changes to produce the initial composition has been calculated and applied to * the composition. * * Will throw an [IllegalStateException] if the composition has been disposed. * * @param content A composable function that describes the content of the composition. * @exception IllegalStateException thrown in the composition has been [dispose]d. */ public fun setContent(content: @Composable () -> Unit) } /** * A [ReusableComposition] is a [Composition] that can be reused for different composable content. * * This interface is used by components that have to synchronize lifecycle of parent and child * compositions and efficiently reuse the nodes emitted by [ReusableComposeNode]. */ public sealed interface ReusableComposition : Composition { /** * Update the composition with the content described by the [content] composable. After this has * been called the changes to produce the initial composition has been calculated and applied to * the composition. * * This method forces this composition into "reusing" state before setting content. In reusing * state, all remembered content is discarded, and nodes emitted by [ReusableComposeNode] are * re-used for the new content. The nodes are only reused if the group structure containing the * node matches new content. * * Will throw an [IllegalStateException] if the composition has been disposed. * * @param content A composable function that describes the content of the composition. * @exception IllegalStateException thrown in the composition has been [dispose]d. */ public fun setContentWithReuse(content: @Composable () -> Unit) /** * Deactivate all observation scopes in composition and remove all remembered slots while * preserving nodes in place. The composition can be re-activated by calling [setContent] with a * new content. */ public fun deactivate() } /** * A key to locate a service using the [CompositionServices] interface optionally implemented by * implementations of [Composition]. */ public interface CompositionServiceKey /** * Allows finding composition services from the runtime. The services requested through this * interface are internal to the runtime and cannot be provided directly. * * The [CompositionServices] interface is used by the runtime to provide optional and/or * experimental services through public extension functions. * * Implementation of [Composition] that delegate to another [Composition] instance should implement * this interface and delegate calls to [getCompositionService] to the original [Composition]. */ public interface CompositionServices { /** Find a service of class [T]. */ public fun getCompositionService(key: CompositionServiceKey): T? } /** * Find a Composition service. * * Find services that implement optional and/or experimental services provided through public or * experimental extension functions. */ internal fun Composition.getCompositionService(key: CompositionServiceKey) = (this as? CompositionServices)?.getCompositionService(key) /** * A controlled composition is a [Composition] that can be directly controlled by the caller. * * This is the interface used by the [Recomposer] to control how and when a composition is * invalidated and subsequently recomposed. * * Normally a composition is controlled by the [Recomposer] but it is often more efficient for tests * to take direct control over a composition by calling [ControlledComposition] instead of * [Composition]. * * @see ControlledComposition */ public sealed interface ControlledComposition : Composition { /** * True if the composition is actively compositing such as when actively in a call to * [composeContent] or [recompose]. */ public val isComposing: Boolean /** * True after [composeContent] or [recompose] has been called and [applyChanges] is expected as * the next call. An exception will be throw in [composeContent] or [recompose] is called while * there are pending from the previous composition pending to be applied. */ public val hasPendingChanges: Boolean /** * Called by the parent composition in response to calling [setContent]. After this method the * changes should be calculated but not yet applied. DO NOT call this method directly if this is * interface is controlled by a [Recomposer], either use [setContent] or * [Recomposer.composeInitial] instead. * * @param content A composable function that describes the tree. */ public fun composeContent(content: @Composable () -> Unit) /** * Record the values that were modified after the last call to [recompose] or from the initial * call to [composeContent]. This should be called before [recompose] is called to record which * parts of the composition need to be recomposed. * * @param values the set of values that have changed since the last composition. */ public fun recordModificationsOf(values: Set) /** * Returns true if any of the object instances in [values] is observed by this composition. This * allows detecting if values changed by a previous composition will potentially affect this * composition. */ public fun observesAnyOf(values: Set): Boolean /** * Execute [block] with [isComposing] set temporarily to `true`. This allows treating * invalidations reported during [prepareCompose] as if they happened while composing to avoid * double invalidations when propagating changes from a parent composition while before * composing the child composition. */ public fun prepareCompose(block: () -> Unit) /** * Record that [value] has been read. This is used primarily by the [Recomposer] to inform the * composer when the a [MutableState] instance has been read implying it should be observed for * changes. * * @param value the instance from which a property was read */ public fun recordReadOf(value: Any) /** * Record that [value] has been modified. This is used primarily by the [Recomposer] to inform * the composer when the a [MutableState] instance been change by a composable function. */ public fun recordWriteOf(value: Any) /** * Recompose the composition to calculate any changes necessary to the composition state and the * tree maintained by the applier. No changes have been made yet. Changes calculated will be * applied when [applyChanges] is called. * * @return returns `true` if any changes are pending and [applyChanges] should be called. */ public fun recompose(): Boolean /** * Insert the given list of movable content with their paired state in potentially a different * composition. If the second part of the pair is null then the movable content should be * inserted as new. If second part of the pair has a value then the state should be moved into * the referenced location and then recomposed there. */ @InternalComposeApi public fun insertMovableContent( references: List> ) /** Dispose the value state that is no longer needed. */ @InternalComposeApi public fun disposeUnusedMovableContent(state: MovableContentState) /** * Apply the changes calculated during [setContent] or [recompose]. If an exception is thrown by * [applyChanges] the composition is irreparably damaged and should be [dispose]d. */ public fun applyChanges() /** * Apply change that must occur after the main bulk of changes have been applied. Late changes * are the result of inserting movable content and it must be performed after [applyChanges] * because, for content that have moved must be inserted only after it has been removed from the * previous location. All deletes must be executed before inserts. To ensure this, all deletes * are performed in [applyChanges] and all inserts are performed in [applyLateChanges]. */ public fun applyLateChanges() /** * Call when all changes, including late changes, have been applied. This signals to the * composition that any transitory composition state can now be discarded. This is advisory only * and a controlled composition will execute correctly when this is not called. */ public fun changesApplied() /** * Abandon current changes and reset composition state. Called when recomposer cannot proceed * with current recomposition loop and needs to reset composition. */ public fun abandonChanges() /** * Invalidate all invalidation scopes. This is called, for example, by [Recomposer] when the * Recomposer becomes active after a previous period of inactivity, potentially missing more * granular invalidations. */ public fun invalidateAll() /** * Throws an exception if the internal state of the composer has been corrupted and is no longer * consistent. Used in testing the composer itself. */ @InternalComposeApi public fun verifyConsistent() /** * Temporarily delegate all invalidations sent to this composition to the [to] composition. This * is used when movable content moves between compositions. The recompose scopes are not * redirected until after the move occurs during [applyChanges] and [applyLateChanges]. This is * used to compose as if the scopes have already been changed. */ public fun delegateInvalidations( to: ControlledComposition?, groupIndex: Int, block: () -> R, ): R /** * Sets the [shouldPause] callback allowing a composition to be pausable if it is not `null`. * Setting the callback to `null` disables pausing. * * @return the previous value of the callback which will be restored once the callback is no * longer needed. * @see PausableComposition */ @Suppress("ExecutorRegistration") public fun getAndSetShouldPauseCallback(shouldPause: ShouldPauseCallback?): ShouldPauseCallback? } /** Utility function to set and restore a should pause callback. */ internal inline fun ControlledComposition.pausable( shouldPause: ShouldPauseCallback, block: () -> R, ): R { val previous = getAndSetShouldPauseCallback(shouldPause) return try { block() } finally { getAndSetShouldPauseCallback(previous) } } /** * This method is the way to initiate a composition. [parent] [CompositionContext] can be * * provided to make the composition behave as a sub-composition of the parent. If composition does * * not have a parent, [Recomposer] instance should be provided. * * It is important to call [Composition.dispose] when composition is no longer needed in order to * release resources. * * @sample androidx.compose.runtime.samples.CustomTreeComposition * @param applier The [Applier] instance to be used in the composition. * @param parent The parent [CompositionContext]. * @see Applier * @see Composition * @see Recomposer */ public fun Composition(applier: Applier<*>, parent: CompositionContext): Composition = CompositionImpl(parent, applier) /** * This method is the way to initiate a reusable composition. [parent] [CompositionContext] can be * provided to make the composition behave as a sub-composition of the parent. If composition does * not have a parent, [Recomposer] instance should be provided. * * It is important to call [Composition.dispose] when composition is no longer needed in order to * release resources. * * @param applier The [Applier] instance to be used in the composition. * @param parent The parent [CompositionContext]. * @see Applier * @see ReusableComposition * @see rememberCompositionContext */ public fun ReusableComposition( applier: Applier<*>, parent: CompositionContext, ): ReusableComposition = CompositionImpl(parent, applier) /** * This method is a way to initiate a composition. Optionally, a [parent] [CompositionContext] can * be provided to make the composition behave as a sub-composition of the parent or a [Recomposer] * can be provided. * * A controlled composition allows direct control of the composition instead of it being controlled * by the [Recomposer] passed ot the root composition. * * It is important to call [Composition.dispose] this composer is no longer needed in order to * release resources. * * @sample androidx.compose.runtime.samples.CustomTreeComposition * @param applier The [Applier] instance to be used in the composition. * @param parent The parent [CompositionContext]. * @see Applier * @see Composition * @see Recomposer */ @TestOnly public fun ControlledComposition( applier: Applier<*>, parent: CompositionContext, ): ControlledComposition = CompositionImpl(parent, applier) private val PendingApplyNoModifications = Any() @OptIn(ExperimentalComposeRuntimeApi::class) internal val ObservableCompositionServiceKey = object : CompositionServiceKey {} private const val RUNNING = 0 private const val DEACTIVATED = 1 private const val INCONSISTENT = 2 private const val DISPOSED = 3 internal abstract class SlotStorage { abstract val isEmpty: Boolean /** Clear the content of the slot table. Report removes to the remember manager */ abstract fun clear(rememberManager: RememberManager) /** Tell the slot storage to collect call-by information (used by live-edit) */ abstract fun collectCalledByInformation() /** Tell the slot storage to collect source information (used by tooling) */ abstract fun collectSourceInformation() /** Deactivate all nodes in the storage (used by lazy) */ abstract fun deactivateAll(rememberManager: RememberManager) abstract fun dispose() /** Extract one or more states of movable content that is nested in the slot storage */ abstract fun extractNestedStates( applier: Applier<*>, references: ObjectList, ): ScatterMap abstract fun disposeUnusedMovableContent( rememberManager: RememberManager, state: MovableContentState, ) /** Invalidate all scopes in the storage (used by live-edit) */ abstract fun invalidateAll() /** Invalidates all groups with the [target] group key (used by live-edit) */ abstract fun invalidateGroupsWithKey(target: Int): List? /** Returns true if the recompose scope is in the slot storage */ abstract fun ownsRecomposeScope(scope: RecomposeScopeImpl): Boolean /** Returns true if the group indicated by group owns the recompose scope */ abstract fun groupContainsAnchor(group: Int, anchor: Anchor): Boolean /** Returns true if the [parent] group contains the [child] group */ abstract fun inGroup(parent: Anchor, child: Anchor): Boolean /** Debugging */ abstract fun toDebugString(): String /** * Testing. Throws an exception if the slot table is not well-formed. A well-formed slot storage * is a slot storage where all the internal invariants hold. */ @TestOnly abstract fun verifyWellFormed() @TestOnly abstract fun getSlots(): Iterable } internal abstract class Changes : DebugStringFormattable() { abstract fun clear() abstract fun execute( slotStorage: SlotStorage, applier: Applier<*>, rememberManager: RememberManager, errorContext: CompositionErrorContextImpl?, ) abstract fun isEmpty(): Boolean fun isNotEmpty() = !isEmpty() } /** * The implementation of the [Composition] interface. * * @param parent An optional reference to the parent composition. * @param applier The applier to use to manage the tree built by the composer. */ @OptIn(ExperimentalComposeRuntimeApi::class) internal class CompositionImpl( /** * The parent composition from [rememberCompositionContext], for sub-compositions, or the an * instance of [Recomposer] for root compositions. */ @get:TestOnly val parent: CompositionContext, /** The applier to use to update the tree managed by the composition. */ private val applier: Applier<*>, ) : ControlledComposition, ReusableComposition, RecomposeScopeOwner, CompositionServices, PausableComposition, ObservableComposition { /** * `null` if a composition isn't pending to apply. `Set` or `Array>` if there are * modifications to record [PendingApplyNoModifications] if a composition is pending to apply, * no modifications. any set contents will be sent to [recordModificationsOf] after applying * changes before releasing [lock] */ private val pendingModifications = AtomicReference(null) // Held when making changes to self or composer private val lock = makeSynchronizedObject() /** * A set of remember observers that were potentially abandoned between [composeContent] or * [recompose] and [applyChanges]. When inserting new content any newly remembered * [RememberObserver]s are added to this set and then removed as [RememberObserver.onRemembered] * is dispatched. If any are left in this when exiting [applyChanges] they have been abandoned * and are sent an [RememberObserver.onAbandoned] notification. */ @Suppress("AsCollectionCall") // Requires iterator API when dispatching abandons private val abandonSet = MutableScatterSet().asMutableSet() /** The slot table is used to store the composition information required for recomposition. */ @Suppress("MemberVisibilityCanBePrivate") // published as internal internal val slotStorage: SlotStorage = createSlotStorage().also { if (parent.collectingCallByInformation) it.collectCalledByInformation() if (parent.collectingSourceInformation) it.collectSourceInformation() } @OptIn(ExperimentalComposeApi::class) private fun createSlotStorage(): SlotStorage = if (ComposeRuntimeFlags.isLinkBufferComposerEnabled) { androidx.compose.runtime.composer.linkbuffer.SlotTable() } else { androidx.compose.runtime.composer.gapbuffer.SlotTable() } /** * A map of observable objects to the [RecomposeScope]s that observe the object. If the key * object is modified the associated scopes should be invalidated. */ private val observations = ScopeMap() /** Used for testing. Returns the objects that are observed */ internal val observedObjects @TestOnly @Suppress("AsCollectionCall") get() = observations.map.asMap().keys /** * A set of scopes that were invalidated by a call from [recordModificationsOf]. This set is * only used in [addPendingInvalidationsLocked], and is reused between invocations. */ private val invalidatedScopes = MutableScatterSet() /** * A set of scopes that were invalidated conditionally (that is they were invalidated by a * [derivedStateOf] object) by a call from [recordModificationsOf]. They need to be held in the * [observations] map until invalidations are drained for composition as a later call to * [recordModificationsOf] might later cause them to be unconditionally invalidated. */ private val conditionallyInvalidatedScopes = MutableScatterSet() /** A map of object read during derived states to the corresponding derived state. */ private val derivedStates = ScopeMap>() /** Used for testing. Returns dependencies of derived states that are currently observed. */ internal val derivedStateDependencies @TestOnly @Suppress("AsCollectionCall") get() = derivedStates.map.asMap().keys /** Used for testing. Returns the conditional scopes being tracked by the composer */ internal val conditionalScopes: List @TestOnly @Suppress("AsCollectionCall") get() = conditionallyInvalidatedScopes.asSet().toList() /** * A list of changes calculated by [Composer] to be applied to the [Applier] and the [SlotTable] * to reflect the result of composition. This is a list of lambdas that need to be invoked in * order to produce the desired effects. */ private val changes = createChangeList() /** * A list of changes calculated by [Composer] to be applied after all other compositions have * had [applyChanges] called. These changes move [MovableContent] state from one composition to * another and must be applied after [applyChanges] because [applyChanges] copies and removes * the state out of the previous composition so it can be inserted into the new location. As * inserts might be earlier in the composition than the position it is deleted, this move must * be done in two phases. */ private val lateChanges = createChangeList() /** * When an observable object is modified during composition any recompose scopes that are * observing that object are invalidated immediately. Since they have already been processed * there is no need to process them again, so this set maintains a set of the recompose scopes * that were already dismissed by composition and should be ignored in the next call to * [recordModificationsOf]. */ private val observationsProcessed = ScopeMap() /** * A map of the invalid [RecomposeScope]s. If this map is non-empty the current state of the * composition does not reflect the current state of the objects it observes and should be * recomposed by calling [recompose]. Tbe value is a map of values that invalidated the scope. * The scope is checked with these instances to ensure the value has changed. This is used to * only invalidate the scope if a [derivedStateOf] object changes. */ private var invalidations = ScopeMap() /** * As [RecomposeScope]s are removed the corresponding entries in the observations set must be * removed as well. This process is expensive so should only be done if it is certain the * [observations] set contains [RecomposeScope] that is no longer needed. [pendingInvalidScopes] * is set to true whenever a [RecomposeScope] is removed from the [slotStorage]. */ @Suppress("MemberVisibilityCanBePrivate") // published as internal internal var pendingInvalidScopes = false /** * If the [shouldPause] callback is set the composition is pausable and should pause whenever * the [shouldPause] callback returns `true`. */ private var shouldPause: ShouldPauseCallback? = null private var pendingPausedComposition: PausedCompositionImpl? = null private var invalidationDelegate: CompositionImpl? = null private var invalidationDelegateGroup: Int = 0 internal val observerHolder = CompositionObserverHolder(parent = parent) private val rememberManager = RememberEventDispatcher() /** The [Composer] to use to create and update the tree managed by this composition. */ internal val composer: InternalComposer = createComposer().also { parent.registerComposer(it) } @OptIn(ExperimentalComposeApi::class) private fun createComposer(): InternalComposer = if (ComposeRuntimeFlags.isLinkBufferComposerEnabled) { LinkComposer( applier = applier, parentContext = parent, slotTable = slotStorage.asLinkBufferSlotTable(), abandonSet = abandonSet, changes = changes, lateChanges = lateChanges, composition = this, observerHolder = observerHolder, ) } else { GapComposer( applier = applier, parentContext = parent, slotTable = slotStorage.asGapBufferSlotTable(), abandonSet = abandonSet, changes = changes, lateChanges = lateChanges, composition = this, observerHolder = observerHolder, ) } @OptIn(ExperimentalComposeApi::class) private fun createChangeList(): Changes = if (ComposeRuntimeFlags.isLinkBufferComposerEnabled) { androidx.compose.runtime.composer.linkbuffer.changelist.ChangeList() } else { androidx.compose.runtime.composer.gapbuffer.changelist.ChangeList() } /** Return true if this is a root (non-sub-) composition. */ val isRoot: Boolean = parent is Recomposer /** True if [dispose] has been called. */ private var state = RUNNING /** True if a sub-composition of this composition is current composing. */ private val areChildrenComposing get() = composer.areChildrenComposing /** * The [Composable] function used to define the tree managed by this composition. This is set by * [setContent]. */ var composable: @Composable () -> Unit = {} override val isComposing: Boolean get() = composer.isComposing override val isDisposed: Boolean get() = state == DISPOSED override val hasPendingChanges: Boolean get() = synchronized(lock) { composer.hasPendingChanges } override fun setContent(content: @Composable () -> Unit) { val wasDeactivated = clearDeactivated() ensureRunning() if (wasDeactivated) { composeInitialWithReuse(content) } else { composeInitial(content) } } override fun setContentWithReuse(content: @Composable () -> Unit) { clearDeactivated() ensureRunning() composeInitialWithReuse(content) } override fun setPausableContent(content: @Composable () -> Unit): PausedComposition { val wasDeactivated = clearDeactivated() return composeInitialPaused(reusable = wasDeactivated, content) } override fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition { clearDeactivated() ensureRunning() return composeInitialPaused(reusable = true, content) } internal fun pausedCompositionFinished(ignoreSet: ScatterSet?) { pendingPausedComposition = null if (ignoreSet != null) { rememberManager.ignoreForgotten(ignoreSet) state = INCONSISTENT } } private fun composeInitial(content: @Composable () -> Unit) { this.composable = content parent.composeInitial(this, composable) } private fun composeInitialPaused( reusable: Boolean, content: @Composable () -> Unit, ): PausedComposition { checkPrecondition(pendingPausedComposition == null) { "A pausable composition is in progress" } val pausedComposition = PausedCompositionImpl( composition = this, context = parent, composer = composer, content = content, reusable = reusable, abandonSet = abandonSet, applier = applier, lock = lock, ) pendingPausedComposition = pausedComposition return pausedComposition } private fun composeInitialWithReuse(content: @Composable () -> Unit) { composer.startReuseFromRoot() composeInitial(content) composer.endReuseFromRoot() } private fun ensureRunning() { checkPrecondition(state == RUNNING) { when (state) { INCONSISTENT -> "A previous pausable composition for this composition was cancelled. This " + "composition must be disposed." DISPOSED -> "The composition is disposed" DEACTIVATED -> "The composition should be activated before setting content." else -> "" // Excluded by the precondition check } } checkPrecondition(pendingPausedComposition == null) { "A pausable composition is in progress" } } private fun clearDeactivated(): Boolean = synchronized(lock) { val isDeactivated = state == DEACTIVATED if (isDeactivated) { state = RUNNING } isDeactivated } @OptIn(ExperimentalComposeRuntimeApi::class) override fun setObserver(observer: CompositionObserver): CompositionObserverHandle { synchronized(lock) { observerHolder.observer = observer observerHolder.root = true } return object : CompositionObserverHandle { override fun dispose() { synchronized(lock) { if (observerHolder.observer == observer) { observerHolder.observer = null observerHolder.root = false } } } } } fun invalidateGroupsWithKey(key: Int) { val scopesToInvalidate = synchronized(lock) { slotStorage.invalidateGroupsWithKey(key) } // Calls to invalidate must be performed without the lock as the they may cause the // recomposer to take its lock to respond to the invalidation and that takes the locks // in the opposite order of composition so if composition begins in another thread taking // the recomposer lock with the composer lock held will deadlock. val forceComposition = scopesToInvalidate == null || scopesToInvalidate.fastAny { it.invalidateForResult(null) == InvalidationResult.IGNORED } if (forceComposition && composer.forceRecomposeScopes()) { parent.invalidate(this) } } @Suppress("UNCHECKED_CAST") private fun drainPendingModificationsForCompositionLocked() { // Recording modifications may race for lock. If there are pending modifications // and we won the lock race, drain them before composing. when (val toRecord = pendingModifications.getAndSet(PendingApplyNoModifications)) { null -> { // Do nothing, just start composing. } PendingApplyNoModifications -> { composeRuntimeError("pending composition has not been applied") } is Set<*> -> { addPendingInvalidationsLocked(toRecord as Set, forgetConditionalScopes = true) } is Array<*> -> for (changed in toRecord as Array>) { addPendingInvalidationsLocked(changed, forgetConditionalScopes = true) } else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications") } } @Suppress("UNCHECKED_CAST") private fun drainPendingModificationsLocked() { when (val toRecord = pendingModifications.getAndSet(null)) { PendingApplyNoModifications -> { // No work to do } is Set<*> -> { addPendingInvalidationsLocked(toRecord as Set, forgetConditionalScopes = false) } is Array<*> -> for (changed in toRecord as Array>) { addPendingInvalidationsLocked(changed, forgetConditionalScopes = false) } null -> { if (pendingPausedComposition == null) composeImmediateRuntimeError( "calling recordModificationsOf and applyChanges concurrently is not supported" ) // otherwise, the paused composition may be being resumed concurrently. } else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications") } } // Drain the modification out of the normal recordModificationsOf(), composition() cycle. // This avoids the checks to make sure the two calls are called in order. @Suppress("UNCHECKED_CAST") private fun drainPendingModificationsOutOfBandLocked() { when (val toRecord = pendingModifications.getAndSet(emptySet())) { PendingApplyNoModifications, null -> { // No work to do } is Set<*> -> { addPendingInvalidationsLocked(toRecord as Set, forgetConditionalScopes = false) } is Array<*> -> for (changed in toRecord as Array>) { addPendingInvalidationsLocked(changed, forgetConditionalScopes = false) } else -> composeRuntimeError("corrupt pendingModifications drain: $pendingModifications") } } override fun composeContent(content: @Composable () -> Unit) { // TODO: This should raise a signal to any currently running recompose calls // to halt and return guardChanges { synchronized(lock) { drainPendingModificationsForCompositionLocked() guardInvalidationsLocked { invalidations -> composer.composeContent(invalidations, content, shouldPause) } } } } internal fun updateMovingInvalidations() { synchronized(lock) { drainPendingModificationsOutOfBandLocked() guardInvalidationsLocked { invalidations -> composer.updateComposerInvalidations(invalidations) } } } override fun dispose() { synchronized(lock) { checkPrecondition(!composer.isComposing) { "Composition is disposed while composing. If dispose is triggered by a call in " + "@Composable function, consider wrapping it with SideEffect block." } if (state != DISPOSED) { state = DISPOSED composable = {} // Changes are deferred if the composition contains movable content that needs // to be released. NOTE: Applying these changes leaves the slot table in // potentially invalid state. The routine use to produce this change list reuses // code that extracts movable content from groups that are being deleted. This code // does not bother to correctly maintain the node counts of a group nested groups // that are going to be removed anyway so the node counts of the groups affected // are might be incorrect after the changes have been applied. val deferredChanges = composer.deferredChanges if (deferredChanges != null) { applyChangesInLocked(deferredChanges) } // Dispatch all the `onForgotten` events for object that are no longer part of a // composition because this composition is being discarded. It is important that // this is done after applying deferred changes above to avoid sending ` // onForgotten` notification to objects that are still part of movable content that // will be moved to a new location. val nonEmptySlotTable = !slotStorage.isEmpty if (nonEmptySlotTable || abandonSet.isNotEmpty()) { rememberManager.use(abandonSet, composer.errorContext) { if (nonEmptySlotTable) { applier.onBeginChanges() slotStorage.clear(rememberManager) applier.clear() applier.onEndChanges() dispatchRememberObservers() } dispatchAbandons() } } composer.dispose() } } parent.unregisterComposition(this) } override val hasInvalidations get() = synchronized(lock) { invalidations.size > 0 } /** * To bootstrap multithreading handling, recording modifications is now deferred between * recomposition with changes to apply and the application of those changes. * [pendingModifications] will contain a queue of changes to apply once all current changes have * been successfully processed. Draining this queue is the responsibility of [recompose] if it * would return `false` (changes do not need to be applied) or [applyChanges]. */ @Suppress("UNCHECKED_CAST") override fun recordModificationsOf(values: Set) { while (true) { val old = pendingModifications.get() val new: Any = when (old) { null, PendingApplyNoModifications -> values is Set<*> -> arrayOf(old, values) is Array<*> -> (old as Array>) + values else -> error("corrupt pendingModifications: $pendingModifications") } if (pendingModifications.compareAndSet(old, new)) { if (old == null) { synchronized(lock) { drainPendingModificationsLocked() } } break } } } override fun observesAnyOf(values: Set): Boolean { values.fastForEach { value -> if (value in observations || value in derivedStates) return true } return false } override fun prepareCompose(block: () -> Unit) = composer.prepareCompose(block) /** * Extract the invalidations that are in the group with the given marker. This is used when * movable content is moved between tables and the content was invalidated. This is used to move * the invalidations with the content. */ internal fun extractInvalidationsOf(anchor: Anchor): List> { return if (invalidations.size > 0) { val result = mutableListOf>() val slotStorage = slotStorage invalidations.removeIf { scope, value -> val scopeAnchor = scope.anchor if (scopeAnchor != null && slotStorage.inGroup(anchor, scopeAnchor)) { result.add(scope to value) // Remove the invalidation true } else { // Keep the invalidation false } } result } else emptyList() } /** * Extract the invalidations that are in the group with the given marker. This is used when * movable content is moved between tables and the content was invalidated. This is used to move * the invalidations with the content. */ internal inline fun extractInvalidationsOfGroup( inGroup: (Anchor) -> Boolean ): List> { return if (invalidations.size > 0) { val result = mutableListOf>() invalidations.removeIf { scope, value -> val scopeAnchor = scope.anchor if (scopeAnchor != null && inGroup(scopeAnchor)) { result.add(scope to value) // Remove the invalidation true } else { // Keep the invalidation false } } result } else emptyList() } private fun addPendingInvalidationsLocked(value: Any, forgetConditionalScopes: Boolean) { observations.forEachScopeOf(value) { scope -> if ( !observationsProcessed.remove(value, scope) && scope.invalidateForResult(value) != InvalidationResult.IGNORED ) { if (scope.isConditional && !forgetConditionalScopes) { conditionallyInvalidatedScopes.add(scope) } else { invalidatedScopes.add(scope) } } } } private fun addPendingInvalidationsLocked(values: Set, forgetConditionalScopes: Boolean) { values.fastForEach { value -> if (value is RecomposeScopeImpl) { value.invalidateForResult(null) } else { addPendingInvalidationsLocked(value, forgetConditionalScopes) derivedStates.forEachScopeOf(value) { addPendingInvalidationsLocked(it, forgetConditionalScopes) } } } val conditionallyInvalidatedScopes = conditionallyInvalidatedScopes val invalidatedScopes = invalidatedScopes if (forgetConditionalScopes && conditionallyInvalidatedScopes.isNotEmpty()) { observations.removeScopeIf { scope -> scope in conditionallyInvalidatedScopes || scope in invalidatedScopes } conditionallyInvalidatedScopes.clear() cleanUpDerivedStateObservations() } else if (invalidatedScopes.isNotEmpty()) { observations.removeScopeIf { scope -> scope in invalidatedScopes } cleanUpDerivedStateObservations() invalidatedScopes.clear() } } private fun cleanUpDerivedStateObservations() { derivedStates.removeScopeIf { derivedState -> derivedState !in observations } if (conditionallyInvalidatedScopes.isNotEmpty()) { conditionallyInvalidatedScopes.removeIf { scope -> !scope.isConditional } } } override fun recordReadOf(value: Any) { // Not acquiring lock since this happens during composition with it already held if (!areChildrenComposing) { composer.currentRecomposeScope?.let { scope -> scope.used = true val alreadyRead = scope.recordRead(value) observer()?.onReadInScope(scope, value) if (!alreadyRead) { if (value is StateObjectImpl) { value.recordReadIn(ReaderKind.Composition) } observations.add(value, scope) // Record derived state dependency mapping if (value is DerivedState<*>) { val record = value.currentRecord derivedStates.removeScope(value) record.dependencies.forEachKey { dependency -> if (dependency is StateObjectImpl) { dependency.recordReadIn(ReaderKind.Composition) } derivedStates.add(dependency, value) } scope.recordDerivedStateValue(value, record.currentValue) } } } } } private fun invalidateScopeOfLocked(value: Any) { // Invalidate any recompose scopes that read this value. observations.forEachScopeOf(value) { scope -> if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) { // If we process this during recordWriteOf, ignore it when recording modifications observationsProcessed.add(value, scope) } } } override fun recordWriteOf(value: Any) = synchronized(lock) { invalidateScopeOfLocked(value) // If writing to dependency of a derived value and the value is changed, invalidate the // scopes that read the derived value. derivedStates.forEachScopeOf(value) { invalidateScopeOfLocked(it) } } override fun recompose(): Boolean = synchronized(lock) { val pendingPausedComposition = pendingPausedComposition if (pendingPausedComposition != null && !pendingPausedComposition.isRecomposing) { // If the composition is pending do not recompose it now as the recomposition // is in the control of the pausable composition and is supposed to happen when // the resume is called. However, this may cause the pausable composition to go // revert to an incomplete state. If isRecomposing is true then this is being // called in resume() pendingPausedComposition.markIncomplete() pendingPausedComposition.pausableApplier.markRecomposePending() return false } drainPendingModificationsForCompositionLocked() guardChanges { guardInvalidationsLocked { invalidations -> composer.recompose(invalidations, shouldPause).also { shouldDrain -> // Apply would normally do this for us; do it now if apply shouldn't happen. if (!shouldDrain) drainPendingModificationsLocked() } } } } override fun insertMovableContent( references: List> ) { runtimeCheck(references.fastAll { it.first.composition == this }) guardChanges { composer.insertMovableContentReferences(references) } } override fun disposeUnusedMovableContent(state: MovableContentState) { rememberManager.use(abandonSet, composer.errorContext) { state.slotStorage.disposeUnusedMovableContent(rememberManager, state) dispatchRememberObservers() } } private fun applyChangesInLocked(changes: Changes) { rememberManager.prepare(abandonSet, composer.errorContext) try { if (changes.isEmpty()) return val applier = pendingPausedComposition?.pausableApplier ?: applier val traceName = if (applier == pendingPausedComposition?.pausableApplier) { "Compose:recordChanges" } else { "Compose:applyChanges" } trace(traceName) { val rememberManager = pendingPausedComposition?.rememberManager ?: rememberManager applier.onBeginChanges() changes.execute(slotStorage, applier, rememberManager, composer.errorContext) applier.onEndChanges() } // Side effects run after lifecycle observers so that any remembered objects // that implement RememberObserver receive onRemembered before a side effect // that captured it and operates on it can run. rememberManager.dispatchRememberObservers() rememberManager.dispatchSideEffects() if (pendingInvalidScopes) { trace("Compose:unobserve") { pendingInvalidScopes = false observations.removeScopeIf { scope -> !scope.valid } cleanUpDerivedStateObservations() } } } finally { // Only dispatch abandons if we do not have any late changes or pending paused // compositions. The instances in the abandon set can be remembered in the late changes // or when the paused composition is applied. try { if (this.lateChanges.isEmpty() && pendingPausedComposition == null) { rememberManager.dispatchAbandons() } } finally { rememberManager.clear() } } } override fun applyChanges() { synchronized(lock) { guardChanges { applyChangesInLocked(changes) drainPendingModificationsLocked() } } } override fun applyLateChanges() { synchronized(lock) { guardChanges { if (lateChanges.isNotEmpty()) { applyChangesInLocked(lateChanges) } } } } override fun changesApplied() { synchronized(lock) { guardChanges { composer.changesApplied() // By this time all abandon objects should be notified that they have been // abandoned. if (this.abandonSet.isNotEmpty()) { rememberManager.use(abandonSet, traceContext = composer.errorContext) { dispatchAbandons() } } } } } private inline fun guardInvalidationsLocked( block: (changes: ScopeMap) -> T ): T { val invalidations = takeInvalidations() return try { block(invalidations) } catch (e: Throwable) { this.invalidations = invalidations throw e } } private inline fun guardChanges(block: () -> T): T = try { trackAbandonedValues(block) } catch (e: Throwable) { abandonChanges() throw e } override fun abandonChanges() { pendingModifications.set(null) changes.clear() lateChanges.clear() if (abandonSet.isNotEmpty()) { rememberManager.use(abandonSet, composer.errorContext) { dispatchAbandons() } } } override fun invalidateAll() { slotStorage.invalidateAll() } override fun verifyConsistent() { synchronized(lock) { if (!isComposing) { composer.verifyConsistent() slotStorage.verifyWellFormed() } } } override fun delegateInvalidations( to: ControlledComposition?, groupIndex: Int, block: () -> R, ): R { return if (to != null && to != this && groupIndex >= 0) { invalidationDelegate = to as CompositionImpl invalidationDelegateGroup = groupIndex try { block() } finally { invalidationDelegate = null invalidationDelegateGroup = 0 } } else block() } override fun getAndSetShouldPauseCallback( shouldPause: ShouldPauseCallback? ): ShouldPauseCallback? { val previous = this.shouldPause this.shouldPause = shouldPause return previous } override fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult { if (scope.defaultsInScope) { scope.defaultsInvalid = true } val anchor = scope.anchor if (anchor == null || !anchor.valid) return InvalidationResult.IGNORED // The scope was removed from the composition if (!slotStorage.ownsRecomposeScope(scope)) { // The scope might be owned by the delegate val delegate = synchronized(lock) { invalidationDelegate } if (delegate?.tryImminentInvalidation(scope, instance) == true) return InvalidationResult.IMMINENT // The scope was owned by the delegate return InvalidationResult.IGNORED // The scope has not yet entered the composition } if (!scope.canRecompose) return InvalidationResult.IGNORED // The scope isn't able to be recomposed/invalidated return invalidateChecked(scope, anchor, instance).also { if (it != InvalidationResult.IGNORED) { observer()?.onScopeInvalidated(scope, instance) } } } override fun recomposeScopeReleased(scope: RecomposeScopeImpl) { pendingInvalidScopes = true observer()?.onScopeDisposed(scope) } @Suppress("UNCHECKED_CAST") override fun getCompositionService(key: CompositionServiceKey): T? = if (key == ObservableCompositionServiceKey) this as T else null private fun tryImminentInvalidation(scope: RecomposeScopeImpl, instance: Any?): Boolean = isComposing && composer.tryImminentInvalidation(scope, instance) private fun invalidateChecked( scope: RecomposeScopeImpl, anchor: Anchor, instance: Any?, ): InvalidationResult { val delegate = synchronized(lock) { val delegate = invalidationDelegate?.let { changeDelegate -> // Invalidations are delegated when recomposing changes to movable content // that is destined to be moved. The movable content is composed in the // destination composer but all the recompose scopes point the current // composer and will arrive here. this redirects the invalidations that // will be moved to the destination composer instead of recording an // invalid invalidation in the from composer. if (slotStorage.groupContainsAnchor(invalidationDelegateGroup, anchor)) { changeDelegate } else null } if (delegate == null) { if (tryImminentInvalidation(scope, instance)) { // The invalidation was redirected to the composer. return InvalidationResult.IMMINENT } // Observer requires a map of scope -> states, so we have to fill it if observer // is set. if (instance == null) { // invalidations[scope] containing ScopeInvalidated means it was invalidated // unconditionally. invalidations.set(scope, ScopeInvalidated) } else if (instance !is DerivedState<*>) { // If observer is not set, we only need to add derived states to // invalidation, as regular states are always going to invalidate. invalidations.set(scope, ScopeInvalidated) } else { if (!invalidations.anyScopeOf(scope) { it === ScopeInvalidated }) { invalidations.add(scope, instance) } } } delegate } // We call through the delegate here to ensure we don't nest synchronization scopes. if (delegate != null) { return delegate.invalidateChecked(scope, anchor, instance) } parent.invalidate(this) return if (isComposing) InvalidationResult.DEFERRED else InvalidationResult.SCHEDULED } internal fun removeObservation(instance: Any, scope: RecomposeScopeImpl) { observations.remove(instance, scope) } internal fun removeDerivedStateObservation(state: DerivedState<*>) { // remove derived state if it is not observed in other scopes if (state !in observations) { derivedStates.removeScope(state) } } /** * This takes ownership of the current invalidations and sets up a new array map to hold the new * invalidations. */ private fun takeInvalidations(): ScopeMap { val invalidations = invalidations this.invalidations = ScopeMap() return invalidations } private inline fun trackAbandonedValues(block: () -> T): T { var success = false return try { block().also { success = true } } finally { if (!success && abandonSet.isNotEmpty()) { rememberManager.use(abandonSet, composer.errorContext) { dispatchAbandons() } } } } private fun observer(): CompositionObserver? = observerHolder.current() override fun deactivate() { synchronized(lock) { checkPrecondition(pendingPausedComposition == null) { "Deactivate is not supported while pausable composition is in progress" } val nonEmptySlotTable = !slotStorage.isEmpty if (nonEmptySlotTable || abandonSet.isNotEmpty()) { trace("Compose:deactivate") { rememberManager.use(abandonSet, composer.errorContext) { if (nonEmptySlotTable) { applier.onBeginChanges() slotStorage.deactivateAll(rememberManager) applier.onEndChanges() dispatchRememberObservers() } dispatchAbandons() } } } observations.clear() derivedStates.clear() invalidations.clear() changes.clear() lateChanges.clear() composer.deactivate() state = DEACTIVATED } } // This is only used in tests to ensure the stacks do not silently leak. internal fun composerStacksSizes(): Int = composer.stacksSize() } internal object ScopeInvalidated @OptIn(ExperimentalComposeRuntimeApi::class) internal class CompositionObserverHolder( var observer: CompositionObserver? = null, var root: Boolean = false, private val parent: CompositionContext, ) { fun current(): CompositionObserver? { return if (root) { observer } else { val parentHolder = parent.observerHolder val parentObserver = parentHolder?.observer if (parentObserver != observer) { observer = parentObserver } parentObserver } } } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionLocal.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.runtime /** * Compose passes data through the composition tree explicitly through means of parameters to * composable functions. This is often times the simplest and best way to have data flow through the * tree. * * Sometimes this model can be cumbersome or break down for data that is needed by lots of * components, or when components need to pass data between one another but keep that implementation * detail private. For these cases, [CompositionLocal]s can be used as an implicit way to have data * flow through a composition. * * [CompositionLocal]s by their nature are hierarchical. They make sense when the value of the * [CompositionLocal] needs to be scoped to a particular sub-hierarchy of the composition. * * One must create a [CompositionLocal] instance, which can be referenced by the consumers * statically. [CompositionLocal] instances themselves hold no data, and can be thought of as a * type-safe identifier for the data being passed down a tree. [CompositionLocal] factory functions * take a single parameter: a factory to create a default value in cases where a [CompositionLocal] * is used without a Provider. If this is a situation you would rather not handle, you can throw an * error in this factory. * * @sample androidx.compose.runtime.samples.createCompositionLocal * * Somewhere up the tree, a [CompositionLocalProvider] component can be used, which provides a value * for the [CompositionLocal]. This would often be at the "root" of a tree, but could be anywhere, * and can also be used in multiple places to override the provided value for a sub-tree. * * @sample androidx.compose.runtime.samples.compositionLocalProvider * * Intermediate components do not need to know about the [CompositionLocal] value, and can have zero * dependencies on it. For example, `SomeScreen` might look like this: * * @sample androidx.compose.runtime.samples.someScreenSample * * Finally, a component that wishes to consume the [CompositionLocal] value can use the [current] * property of the [CompositionLocal] key which returns the current value of the [CompositionLocal], * and subscribes the component to changes of it. * * @sample androidx.compose.runtime.samples.consumeCompositionLocal */ @Stable public sealed class CompositionLocal(defaultFactory: () -> T) { internal open val defaultValueHolder: ValueHolder = LazyValueHolder(defaultFactory) internal abstract fun updatedStateOf( value: ProvidedValue, previous: ValueHolder?, ): ValueHolder /** * Return the value provided by the nearest [CompositionLocalProvider] component that invokes, * directly or indirectly, the composable function that uses this property. * * @sample androidx.compose.runtime.samples.consumeCompositionLocal */ @OptIn(InternalComposeApi::class) public inline val current: T @ReadOnlyComposable @Composable get() = currentComposer.consume(this) } /** * A [ProvidableCompositionLocal] can be used in [CompositionLocalProvider] to provide values. * * @see compositionLocalOf * @see staticCompositionLocalOf * @see CompositionLocal * @see CompositionLocalProvider */ @Stable public abstract class ProvidableCompositionLocal internal constructor(defaultFactory: () -> T) : CompositionLocal(defaultFactory) { internal abstract fun defaultProvidedValue(value: T): ProvidedValue /** * Associates a [CompositionLocal] key to a value in a call to [CompositionLocalProvider]. * * @see CompositionLocal * @see ProvidableCompositionLocal */ public infix fun provides(value: T): ProvidedValue = defaultProvidedValue(value) /** * Associates a [CompositionLocal] key to a value in a call to [CompositionLocalProvider] if the * key does not already have an associated value. * * @see CompositionLocal * @see ProvidableCompositionLocal */ public infix fun providesDefault(value: T): ProvidedValue = defaultProvidedValue(value).ifNotAlreadyProvided() /** * Associates a [CompositionLocal] key to a lambda, [compute], in a call to [CompositionLocal]. * The [compute] lambda is invoked whenever the key is retrieved. The lambda is executed in the * context of a [CompositionLocalContext] which allow retrieving the current values of other * composition locals by calling [CompositionLocalAccessorScope.currentValue], which is an * extension function provided by the context for a [CompositionLocal] key. * * The lambda passed to [providesComputed] will be invoked every time the * [CompositionLocal.current] is evaluated for the composition local and computes its value * based on the current value of the locals referenced in the lambda at the time * [CompositionLocal.current] is evaluated. This allows providing values that can be derived * from other locals. For example, if accent colors can be calculated from a single base color, * the accent colors can be provided as computed composition locals. Providing a new base color * would automatically update all the accent colors. * * @sample androidx.compose.runtime.samples.compositionLocalProvidedComputed * @sample androidx.compose.runtime.samples.compositionLocalComputedAfterProvidingLocal * @see CompositionLocal * @see CompositionLocalContext * @see ProvidableCompositionLocal */ public infix fun providesComputed( compute: CompositionLocalAccessorScope.() -> T ): ProvidedValue = ProvidedValue( compositionLocal = this, value = null, explicitNull = false, mutationPolicy = null, state = null, compute = compute, isDynamic = false, ) override fun updatedStateOf( value: ProvidedValue, previous: ValueHolder?, ): ValueHolder { return when (previous) { is DynamicValueHolder -> if (value.isDynamic) { previous.state.value = value.effectiveValue previous } else null is StaticValueHolder -> if (value.isStatic && value.effectiveValue == previous.value) previous else null is ComputedValueHolder -> if (value.compute === previous.compute) previous else null else -> null } ?: valueHolderOf(value) } private fun valueHolderOf(value: ProvidedValue): ValueHolder = when { value.isDynamic -> DynamicValueHolder( value.state ?: mutableStateOf( value.value, value.mutationPolicy ?: structuralEqualityPolicy(), ) ) value.compute != null -> ComputedValueHolder(value.compute) value.state != null -> DynamicValueHolder(value.state) else -> StaticValueHolder(value.effectiveValue) } } /** * A [DynamicProvidableCompositionLocal] is a [CompositionLocal] backed by [mutableStateOf]. * Providing new values using a [DynamicProvidableCompositionLocal] will provide the same [State] * with a different value. Reading the [CompositionLocal] value of a * [DynamicProvidableCompositionLocal] will record a read in the [RecomposeScope] of the * composition. Changing the provided value will invalidate the [RecomposeScope]s. * * @see compositionLocalOf */ internal class DynamicProvidableCompositionLocal( private val policy: SnapshotMutationPolicy, defaultFactory: () -> T, ) : ProvidableCompositionLocal(defaultFactory) { override fun defaultProvidedValue(value: T) = ProvidedValue( compositionLocal = this, value = value, explicitNull = value === null, mutationPolicy = policy, state = null, compute = null, isDynamic = true, ) } /** * A [StaticProvidableCompositionLocal] is a value that is expected to rarely change. * * @see staticCompositionLocalOf */ internal class StaticProvidableCompositionLocal(defaultFactory: () -> T) : ProvidableCompositionLocal(defaultFactory) { override fun defaultProvidedValue(value: T) = ProvidedValue( compositionLocal = this, value = value, explicitNull = value === null, mutationPolicy = null, state = null, compute = null, isDynamic = false, ) } /** * Create a [CompositionLocal] key that can be provided using [CompositionLocalProvider]. Changing * the value provided during recomposition will invalidate the content of [CompositionLocalProvider] * that read the value using [CompositionLocal.current]. * * [compositionLocalOf] creates a [ProvidableCompositionLocal] which can be used in a a call to * [CompositionLocalProvider]. Similar to [MutableList] vs. [List], if the key is made public as * [CompositionLocal] instead of [ProvidableCompositionLocal], it can be read using * [CompositionLocal.current] but not re-provided. * * @param policy a policy to determine when a [CompositionLocal] is considered changed. See * [SnapshotMutationPolicy] for details. * @param defaultFactory a value factory to supply a value when a value is not provided. This * factory is called when no value is provided through a [CompositionLocalProvider] of the caller * of the component using [CompositionLocal.current]. If no reasonable default can be provided * then consider throwing an exception. * @see CompositionLocal * @see staticCompositionLocalOf * @see mutableStateOf */ public fun compositionLocalOf( policy: SnapshotMutationPolicy = structuralEqualityPolicy(), defaultFactory: () -> T, ): ProvidableCompositionLocal = DynamicProvidableCompositionLocal(policy, defaultFactory) /** * Create a [CompositionLocal] key that can be provided using [CompositionLocalProvider]. * * Unlike [compositionLocalOf], reads of a [staticCompositionLocalOf] are not tracked by the * composer and changing the value provided in the [CompositionLocalProvider] call will cause the * entirety of the content to be recomposed instead of just the places where in the composition the * local value is used. This lack of tracking, however, makes a [staticCompositionLocalOf] more * efficient when the value provided is highly unlikely to or will never change. For example, the * android context, font loaders, or similar shared values, are unlikely to change for the * components in the content of a the [CompositionLocalProvider] and should consider using a * [staticCompositionLocalOf]. A color, or other theme like value, might change or even be animated * therefore a [compositionLocalOf] should be used. * * [staticCompositionLocalOf] creates a [ProvidableCompositionLocal] which can be used in a a call * to [CompositionLocalProvider]. Similar to [MutableList] vs. [List], if the key is made public as * [CompositionLocal] instead of [ProvidableCompositionLocal], it can be read using * [CompositionLocal.current] but not re-provided. * * @param defaultFactory a value factory to supply a value when a value is not provided. This * factory is called when no value is provided through a [CompositionLocalProvider] of the caller * of the component using [CompositionLocal.current]. If no reasonable default can be provided * then consider throwing an exception. * @see CompositionLocal * @see compositionLocalOf */ public fun staticCompositionLocalOf(defaultFactory: () -> T): ProvidableCompositionLocal = StaticProvidableCompositionLocal(defaultFactory) /** * Create a [CompositionLocal] that behaves like it was provided using * [ProvidableCompositionLocal.providesComputed] by default. If a value is provided using * [ProvidableCompositionLocal.provides] it behaves as if the [CompositionLocal] was produced by * calling [compositionLocalOf]. * * In other words, a [CompositionLocal] produced by can be provided identically to * [CompositionLocal] created with [compositionLocalOf] with the only difference is how it behaves * when the value is not provided. For a [compositionLocalOf] the default value is returned. If no * default value has be computed for [CompositionLocal] the default computation is called. * * The lambda passed to [compositionLocalWithComputedDefaultOf] will be invoked every time the * [CompositionLocal.current] is evaluated for the composition local and computes its value based on * the current value of the locals referenced in the lambda at the time [CompositionLocal.current] * is evaluated. This allows providing values that can be derived from other locals. For example, if * accent colors can be calculated from a single base color, the accent colors can be provided as * computed composition locals. Providing a new base color would automatically update all the accent * colors. * * @sample androidx.compose.runtime.samples.compositionLocalComputedByDefault * @sample androidx.compose.runtime.samples.compositionLocalComputedAfterProvidingLocal * @param defaultComputation the default computation to use when this [CompositionLocal] is not * provided. * @see CompositionLocal * @see ProvidableCompositionLocal */ public fun compositionLocalWithComputedDefaultOf( defaultComputation: CompositionLocalAccessorScope.() -> T ): ProvidableCompositionLocal = ComputedProvidableCompositionLocal(defaultComputation) internal class ComputedProvidableCompositionLocal( defaultComputation: CompositionLocalAccessorScope.() -> T ) : ProvidableCompositionLocal({ composeRuntimeError("Unexpected call to default provider") }) { override val defaultValueHolder = ComputedValueHolder(defaultComputation) override fun defaultProvidedValue(value: T): ProvidedValue = ProvidedValue( compositionLocal = this, value = value, explicitNull = value === null, mutationPolicy = null, state = null, compute = null, isDynamic = true, ) } /** * Creates a [ProvidableCompositionLocal] where the default value is resolved by querying the * [LocalHostDefaultProvider] with the given [key]. * * If a value is provided using [ProvidableCompositionLocal.provides], this behaves identically to a * [CompositionLocal] created with [compositionLocalOf]. * * When no value is provided, the default value is resolved by querying the * [LocalHostDefaultProvider] currently present in the composition. This mechanism allows the * default value to be determined dynamically by the hosting environment (such as an Android View) * rather than being hardcoded or requiring an explicit provider at the root of the composition. * * This effectively acts as a bridge, decoupling the definition of the [CompositionLocal] from the * platform-specific logic required to resolve its default. For example, a * `LocalViewModelStoreOwner` can use this to ask the host for the owner without having a direct * dependency on the Android View system. * * @param key An opaque key used to identify the requested value within the host's context. The type * and meaning of the key are defined by the [HostDefaultProvider] implementation (e.g., on * Android, this is typically a Resource ID). * @throws NullPointerException If the host cannot find a value for [key], it returns `null`. If [T] * is a non-nullable type (e.g., `compositionLocalWithHostDefaultOf`), a missing key will * result in a [NullPointerException] when the value is accessed. */ public fun compositionLocalWithHostDefaultOf( key: HostDefaultKey ): ProvidableCompositionLocal = compositionLocalWithComputedDefaultOf { LocalHostDefaultProvider.currentValue.getHostDefault(key) } public interface CompositionLocalAccessorScope { /** * An extension property that allows accessing the current value of a composition local in the * context of this scope. This scope is the type of the `this` parameter when in a computed * composition. Computed composition locals can be provided by either using * [compositionLocalWithComputedDefaultOf] or by using the * [ProvidableCompositionLocal.providesComputed] infix operator. * * @sample androidx.compose.runtime.samples.compositionLocalProvidedComputed * @see ProvidableCompositionLocal * @see ProvidableCompositionLocal.providesComputed * @see ProvidableCompositionLocal.provides * @see CompositionLocalProvider */ public val CompositionLocal.currentValue: T } /** * Stores [CompositionLocal]'s and their values. * * Can be obtained via [currentCompositionLocalContext] and passed to another composition via * [CompositionLocalProvider]. * * [CompositionLocalContext] is immutable and won't be changed after its obtaining. */ @Stable public class CompositionLocalContext internal constructor(internal val compositionLocals: PersistentCompositionLocalMap) { override fun equals(other: Any?): Boolean { return other is CompositionLocalContext && other.compositionLocals == this.compositionLocals } override fun hashCode(): Int { return compositionLocals.hashCode() } } /** * [CompositionLocalProvider] binds values to [ProvidableCompositionLocal] keys. Reading the * [CompositionLocal] using [CompositionLocal.current] will return the value provided in * [CompositionLocalProvider]'s [values] parameter for all composable functions called directly or * indirectly in the [content] lambda. * * @sample androidx.compose.runtime.samples.compositionLocalProvider * @see CompositionLocal * @see compositionLocalOf * @see staticCompositionLocalOf */ @Composable @OptIn(InternalComposeApi::class) @NonSkippableComposable public fun CompositionLocalProvider( vararg values: ProvidedValue<*>, content: @Composable () -> Unit, ) { currentComposer.startProviders(values) content() currentComposer.endProviders() } /** * [CompositionLocalProvider] binds value to [ProvidableCompositionLocal] key. Reading the * [CompositionLocal] using [CompositionLocal.current] will return the value provided in * [CompositionLocalProvider]'s [value] parameter for all composable functions called directly or * indirectly in the [content] lambda. * * @sample androidx.compose.runtime.samples.compositionLocalProvider * @see CompositionLocal * @see compositionLocalOf * @see staticCompositionLocalOf */ @Composable @OptIn(InternalComposeApi::class) @NonSkippableComposable public fun CompositionLocalProvider(value: ProvidedValue<*>, content: @Composable () -> Unit) { currentComposer.startProvider(value) content() currentComposer.endProvider() } /** * [CompositionLocalProvider] binds values to [CompositionLocal]'s, provided by [context]. Reading * the [CompositionLocal] using [CompositionLocal.current] will return the value provided in values * stored inside [context] for all composable functions called directly or indirectly in the * [content] lambda. * * @sample androidx.compose.runtime.samples.compositionLocalProvider * @see CompositionLocal * @see compositionLocalOf * @see staticCompositionLocalOf */ @Composable public fun CompositionLocalProvider( context: CompositionLocalContext, content: @Composable () -> Unit, ) { CompositionLocalProvider( *context.compositionLocals.map { it.value.toProvided(it.key) }.toTypedArray(), content = content, ) } /** * [withCompositionLocal] binds value to [ProvidableCompositionLocal] key and returns the result * produced by the [content] lambda. Use with non-unit returning [content] lambdas or else use * [CompositionLocalProvider]. Reading the [CompositionLocal] using [CompositionLocal.current] will * return the value provided in [CompositionLocalProvider]'s [value] parameter for all composable * functions called directly or indirectly in the [content] lambda. * * @see CompositionLocalProvider * @see CompositionLocal * @see compositionLocalOf * @see staticCompositionLocalOf */ @Suppress("BanInlineOptIn") // b/430604046 - These APIs are stable so are ok to inline @OptIn(InternalComposeApi::class) @Composable public inline fun withCompositionLocal( value: ProvidedValue<*>, content: @Composable () -> T, ): T { currentComposer.startProvider(value) return content().also { currentComposer.endProvider() } } /** * [withCompositionLocals] binds values to [ProvidableCompositionLocal] key and returns the result * produced by the [content] lambda. Use with non-unit returning [content] lambdas or else use * [CompositionLocalProvider]. Reading the [CompositionLocal] using [CompositionLocal.current] will * return the values provided in [CompositionLocalProvider]'s [values] parameter for all composable * functions called directly or indirectly in the [content] lambda. * * @see CompositionLocalProvider * @see CompositionLocal * @see compositionLocalOf * @see staticCompositionLocalOf */ @Suppress("BanInlineOptIn") // b/430604046 - These APIs are stable so are ok to inline @OptIn(InternalComposeApi::class) @Composable public inline fun withCompositionLocals( vararg values: ProvidedValue<*>, content: @Composable () -> T, ): T { currentComposer.startProviders(values) return content().also { currentComposer.endProvider() } } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/CompositionContext.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.runtime import androidx.collection.ScatterSet import androidx.compose.runtime.internal.persistentCompositionLocalHashMapOf import androidx.compose.runtime.tooling.CompositionData import kotlin.coroutines.CoroutineContext private val EmptyPersistentCompositionLocalMap: PersistentCompositionLocalMap = persistentCompositionLocalHashMapOf() /** * A [CompositionContext] is an opaque type that is used to logically "link" two compositions * together. The [CompositionContext] instance represents a reference to the "parent" composition in * a specific position of that composition's tree, and the instance can then be given to a new * "child" composition. This reference ensures that invalidations and [CompositionLocal]s flow * logically through the two compositions as if they were not separate. * * The "parent" of a root composition is a [Recomposer]. * * @see rememberCompositionContext */ @OptIn(InternalComposeApi::class, ExperimentalComposeRuntimeApi::class) public abstract class CompositionContext internal constructor() { internal abstract val compositeKeyHashCode: CompositeKeyHashCode internal abstract val collectingParameterInformation: Boolean internal abstract val collectingSourceInformation: Boolean internal abstract val collectingCallByInformation: Boolean internal abstract val stackTraceEnabled: Boolean internal open val observerHolder: CompositionObserverHolder? get() = null /** The [CoroutineContext] with which effects for the composition will be executed in. */ public abstract val effectCoroutineContext: CoroutineContext /** Associated composition if one exists. */ internal abstract val composition: Composition? internal abstract fun composeInitial( composition: ControlledComposition, content: @Composable () -> Unit, ) internal abstract fun composeInitialPaused( composition: ControlledComposition, shouldPause: ShouldPauseCallback, content: @Composable () -> Unit, ): ScatterSet internal abstract fun recomposePaused( composition: ControlledComposition, shouldPause: ShouldPauseCallback, invalidScopes: ScatterSet, ): ScatterSet internal abstract fun reportPausedScope(scope: RecomposeScopeImpl) internal abstract fun invalidate(composition: ControlledComposition) internal abstract fun invalidateScope(scope: RecomposeScopeImpl) internal open fun recordInspectionTable(table: MutableSet) {} internal open fun registerComposer(composer: Composer) {} internal open fun unregisterComposer(composer: Composer) {} internal abstract fun registerComposition(composition: ControlledComposition) internal abstract fun unregisterComposition(composition: ControlledComposition) internal open fun getCompositionLocalScope(): PersistentCompositionLocalMap = EmptyPersistentCompositionLocalMap internal open fun startComposing() {} internal open fun doneComposing() {} internal abstract fun insertMovableContent(reference: MovableContentStateReference) internal abstract fun deletedMovableContent(reference: MovableContentStateReference) internal abstract fun movableContentStateReleased( reference: MovableContentStateReference, data: MovableContentState, applier: Applier<*>, ) internal open fun movableContentStateResolve( reference: MovableContentStateReference ): MovableContentState? = null internal abstract fun reportRemovedComposition(composition: ControlledComposition) public abstract fun scheduleFrameEndCallback(action: () -> Unit): CancellationHandle } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Composer.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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. */ @file:OptIn(InternalComposeApi::class) @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress") package androidx.compose.runtime import androidx.compose.runtime.Composer.Companion.Empty import androidx.compose.runtime.collection.ScopeMap import androidx.compose.runtime.composer.RememberManager import androidx.compose.runtime.composer.gapbuffer.SlotReader import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter import androidx.compose.runtime.composer.gapbuffer.asGapAnchor import androidx.compose.runtime.tooling.ComposeStackTrace import androidx.compose.runtime.tooling.ComposeStackTraceFrame import androidx.compose.runtime.tooling.ComposeStackTraceMode import androidx.compose.runtime.tooling.CompositionData import androidx.compose.runtime.tooling.CompositionErrorContextImpl import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract import kotlin.coroutines.CoroutineContext import kotlin.jvm.JvmInline import kotlin.jvm.JvmName /** * Internal compose compiler plugin API that is used to update the function the composer will call * to recompose a recomposition scope. This should not be used or called directly. */ @ComposeCompilerApi public interface ScopeUpdateScope { /** * Called by generated code to update the recomposition scope with the function to call * recompose the scope. This is called by code generated by the compose compiler plugin and * should not be called directly. */ public fun updateScope(block: (Composer, Int) -> Unit) } internal enum class InvalidationResult { /** * The invalidation was ignored because the associated recompose scope is no longer part of the * composition or has yet to be entered in the composition. This could occur for invalidations * called on scopes that are no longer part of composition or if the scope was invalidated * before [ControlledComposition.applyChanges] was called that will enter the scope into the * composition. */ IGNORED, /** * The composition is not currently composing and the invalidation was recorded for a future * composition. A recomposition requested to be scheduled. */ SCHEDULED, /** * The composition that owns the recompose scope is actively composing but the scope has already * been composed or is in the process of composing. The invalidation is treated as SCHEDULED * above. */ DEFERRED, /** * The composition that owns the recompose scope is actively composing and the invalidated scope * has not been composed yet but will be recomposed before the composition completes. A new * recomposition was not scheduled for this invalidation. */ IMMINENT, } /** * An instance to hold a value provided by [CompositionLocalProvider] and is created by the * [ProvidableCompositionLocal.provides] infix operator. If [canOverride] is `false`, the provided * value will not overwrite a potentially already existing value in the scope. * * This value cannot be created directly. It can only be created by using one of the `provides` * operators of [ProvidableCompositionLocal]. * * @see ProvidableCompositionLocal.provides * @see ProvidableCompositionLocal.providesDefault * @see ProvidableCompositionLocal.providesComputed */ public class ProvidedValue internal constructor( /** * The composition local that is provided by this value. This is the left-hand side of the * [ProvidableCompositionLocal.provides] infix operator. */ public val compositionLocal: CompositionLocal, value: T?, private val explicitNull: Boolean, internal val mutationPolicy: SnapshotMutationPolicy?, internal val state: MutableState?, internal val compute: (CompositionLocalAccessorScope.() -> T)?, internal val isDynamic: Boolean, ) { private val providedValue: T? = value /** * The value provided by the [ProvidableCompositionLocal.provides] infix operator. This is the * right-hand side of the operator. */ @Suppress("UNCHECKED_CAST") public val value: T get() = providedValue as T /** * This value is `true` if the provided value will override any value provided above it. This * value is `true` when using [ProvidableCompositionLocal.provides] but `false` when using * [ProvidableCompositionLocal.providesDefault]. * * @see ProvidableCompositionLocal.provides * @see ProvidableCompositionLocal.providesDefault */ @get:JvmName("getCanOverride") public var canOverride: Boolean = true private set @Suppress("UNCHECKED_CAST") internal val effectiveValue: T get() = when { explicitNull -> null as T state != null -> state.value providedValue != null -> providedValue else -> composeRuntimeError("Unexpected form of a provided value") } internal val isStatic get() = (explicitNull || value != null) && !isDynamic internal fun ifNotAlreadyProvided() = this.also { canOverride = false } } private val SlotWriter.nextGroup get() = currentGroup + groupSize(currentGroup) /** * Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by code * generation helpers. It is highly recommended that direct calls these be avoided as the runtime * assumes that the calls are generated by the compiler and contain only a minimum amount of state * validation. */ public sealed interface Composer { /** * A Compose compiler plugin API. DO NOT call directly. * * Changes calculated and recorded during composition and are sent to [applier] which makes the * physical changes to the node tree implied by a composition. * * Composition has two discrete phases, 1) calculate and record changes and 2) making the * changes via the [applier]. While a [Composable] functions is executing, none of the [applier] * methods are called. The recorded changes are sent to the [applier] all at once after all * [Composable] functions have completed. */ @ComposeCompilerApi public val applier: Applier<*> /** * A Compose compiler plugin API. DO NOT call directly. * * Reflects that a new part of the composition is being created, that is, the composition will * insert new nodes into the resulting tree. */ @ComposeCompilerApi public val inserting: Boolean /** * A Compose compiler plugin API. DO NOT call directly. * * Reflects whether the [Composable] function can skip. Even if a [Composable] function is * called with the same parameters it might still need to run because, for example, a new value * was provided for a [CompositionLocal] created by [staticCompositionLocalOf]. */ @ComposeCompilerApi public val skipping: Boolean /** * A Compose compiler plugin API. DO NOT call directly. * * Reflects whether the default parameter block of a [Composable] function is valid. This is * `false` if a [State] object read in the [startDefaults] group was modified since the last * time the [Composable] function was run. */ @ComposeCompilerApi public val defaultsInvalid: Boolean /** * A Compose internal property. DO NOT call directly. Use [currentRecomposeScope] instead. * * The invalidation current invalidation scope. An new invalidation scope is created whenever * [startRestartGroup] is called. when this scope's [RecomposeScope.invalidate] is called then * lambda supplied to [endRestartGroup]'s [ScopeUpdateScope] will be scheduled to be run. */ @InternalComposeApi public val recomposeScope: RecomposeScope? /** * A Compose compiler plugin API. DO NOT call directly. * * Return an object that can be used to uniquely identity of the current recomposition scope. * This identity will be the same even if the recompose scope instance changes. * * This is used internally by tooling track composable function invocations. */ @ComposeCompilerApi public val recomposeScopeIdentity: Any? /** * A Compose internal property. DO NOT call directly. Use [currentCompositeKeyHash] instead. * * This a hash value used to map externally stored state to the composition. For example, this * is used by saved instance state to preserve state across activity lifetime boundaries. * * This value is likely but not guaranteed to be unique. There are known cases, such as for * loops without a unique [key], where the runtime does not have enough information to make the * compound key hash unique. */ @Deprecated( "Prefer the higher-precision compositeKeyHashCode instead", ReplaceWith("compositeKeyHashCode"), ) @InternalComposeApi public val compoundKeyHash: Int get() = compositeKeyHashCode.hashCode() /** * A Compose internal property. DO NOT call directly. Use [currentCompositeKeyHashCode] instead. * * This a hash value used to map externally stored state to the composition. For example, this * is used by saved instance state to preserve state across activity lifetime boundaries. * * This value is likely but not guaranteed to be unique. There are known cases, such as for * loops without a unique [key], where the runtime does not have enough information to make the * compound key hash unique. */ @InternalComposeApi public val compositeKeyHashCode: CompositeKeyHashCode // Groups /** * A Compose compiler plugin API. DO NOT call directly. * * Start a replaceable group. A replaceable group is a group that cannot be moved during * execution and can only either inserted, removed, or replaced. For example, the group created * by most control flow constructs such as an `if` statement are replaceable groups. * * Warning: Versions of the compiler that generate calls to this function also contain subtle * bug that does not generate a group around a loop containing code that just creates composable * lambdas (AnimatedContent from androidx.compose.animation, for example) which makes replacing * the group unsafe and the this must treat this like a movable group. [startReplaceGroup] was * added that will replace the group as described above and is only called by versions of the * compiler that correctly generate code around loops that create lambdas. This method is kept * to maintain compatibility with code generated by older versions of the compose compiler * plugin. * * @param key A compiler generated key based on the source location of the call. */ @ComposeCompilerApi public fun startReplaceableGroup(key: Int) /** * A Compose compiler plugin API. DO NOT call directly. * * Called at the end of a replaceable group. * * @see startRestartGroup */ @ComposeCompilerApi public fun endReplaceableGroup() /** * A Compose compiler plugin API. DO NOT call directly. * * Start a replace group. A replace group is a group that cannot be moved during must only * either be inserted, removed, or replaced. For example, the group created by most control flow * constructs such as an `if` statement are replaceable groups. * * Note: This method replaces [startReplaceableGroup] which is only generated by older versions * of the compose compiler plugin that predate the addition of this method. The runtime is now * required to replace the group if a different group is detected instead of treating it like a * movable group. * * @param key A compiler generated key based on the source location of the call. * @see endReplaceGroup */ @ComposeCompilerApi public fun startReplaceGroup(key: Int) /** * A Compose compiler plugin API. DO NOT call directly. * * Called at the end of a replace group. * * @see startReplaceGroup */ @ComposeCompilerApi public fun endReplaceGroup() /** * A Compose compiler plugin API. DO NOT call directly. * * Start a movable group. A movable group is one that can be moved based on the value of * [dataKey] which is typically supplied by the [key][androidx.compose.runtime.key] pseudo * compiler function. * * A movable group implements the semantics of [key][androidx.compose.runtime.key] which allows * the state and nodes generated by a loop to move with the composition implied by the key * passed to [key][androidx.compose.runtime.key]. * * @param key a compiler generated key based on the source location of the call. * @param dataKey an additional object that is used as a second part of the key. This key * produced from the `keys` parameter supplied to the [key][androidx.compose.runtime.key] * pseudo compiler function. */ @ComposeCompilerApi public fun startMovableGroup(key: Int, dataKey: Any?) /** * A Compose compiler plugin API. DO NOT call directly. * * Called at the end of a movable group. * * @see startMovableGroup */ @ComposeCompilerApi public fun endMovableGroup() /** * A Compose compiler plugin API. DO NOT call directly. * * Called to start the group that calculates the default parameters of a [Composable] function. * * This method is called near the beginning of a [Composable] function with default parameters * and surrounds the remembered values or [Composable] calls necessary to produce the default * parameters. For example, for `model: Model = remember { DefaultModel() }` the call to * [remember] is called inside a [startDefaults] group. */ @ComposeCompilerApi public fun startDefaults() /** * A Compose compiler plugin API. DO NOT call directly. * * Called at the end of defaults group. * * @see startDefaults */ @ComposeCompilerApi public fun endDefaults() /** * A Compose compiler plugin API. DO NOT call directly. * * Called to record a group for a [Composable] function and starts a group that can be * recomposed on demand based on the lambda passed to * [updateScope][ScopeUpdateScope.updateScope] when [endRestartGroup] is called * * @param key A compiler generated key based on the source location of the call. * @return the instance of the composer to use for the rest of the function. */ @ComposeCompilerApi public fun startRestartGroup(key: Int): Composer /** * A Compose compiler plugin API. DO NOT call directly. * * Called to end a restart group. */ @ComposeCompilerApi public fun endRestartGroup(): ScopeUpdateScope? /** * A Compose internal API. DO NOT call directly. * * Request movable content be inserted at the current location. This will schedule with the root * composition parent a call to [insertMovableContent] with the correct [MovableContentState] if * one was released in another part of composition. */ @InternalComposeApi public fun insertMovableContent(value: MovableContent<*>, parameter: Any?) /** * A Compose internal API. DO NOT call directly. * * Perform a late composition that adds to the current late apply that will insert the given * references to [MovableContent] into the composition. If a [MovableContent] is paired then * this is a request to move a released [MovableContent] from a different location or from a * different composition. If it is not paired (i.e. the `second` [MovableContentStateReference] * is `null`) then new state for the [MovableContent] is inserted into the composition. */ @InternalComposeApi public fun insertMovableContentReferences( references: List> ) /** * A Compose compiler plugin API. DO NOT call directly. * * Record the source information string for a group. This must be immediately called after the * start of a group. * * @param sourceInformation An string value to that provides the compose tools enough * information to calculate the source location of calls to composable functions. */ public fun sourceInformation(sourceInformation: String) /** * A compose compiler plugin API. DO NOT call directly. * * Record a source information marker. This marker can be used in place of a group that would * have contained the information but was elided as the compiler plugin determined the group was * not necessary such as when a function is marked with [ReadOnlyComposable]. * * @param key A compiler generated key based on the source location of the call. * @param sourceInformation An string value to that provides the compose tools enough * information to calculate the source location of calls to composable functions. */ public fun sourceInformationMarkerStart(key: Int, sourceInformation: String) /** * A compose compiler plugin API. DO NOT call directly. * * Record the end of the marked source information range. */ public fun sourceInformationMarkerEnd() /** * A Compose compiler plugin API. DO NOT call directly. * * Skips the composer to the end of the current group. This generated by the compiler to when * the body of a [Composable] function can be skipped typically because the parameters to the * function are equal to the values passed to it in the previous composition. */ @ComposeCompilerApi public fun skipToGroupEnd() /** * A Compose compiler plugin API. DO NOT call directly. * * Deactivates the content to the end of the group by treating content as if it was deleted and * replaces all slot table entries for calls to [cache] to be [Empty]. This must be called as * the first call for a group. */ @ComposeCompilerApi public fun deactivateToEndGroup(changed: Boolean) /** * A Compose compiler plugin API. DO NOT call directly. * * Skips the current group. This called by the compiler to indicate that the current group can * be skipped, for example, this is generated to skip the [startDefaults] group the default * group is was not invalidated. */ @ComposeCompilerApi public fun skipCurrentGroup() // Nodes /** * A Compose compiler plugin API. DO NOT call directly. * * Start a group that tracks a the code that will create or update a node that is generated as * part of the tree implied by the composition. */ @ComposeCompilerApi public fun startNode() /** * A Compose compiler plugin API. DO NOT call directly. * * Start a group that tracks a the code that will create or update a node that is generated as * part of the tree implied by the composition. A reusable node can be reused in a reusable * group even if the group key is changed. */ @ComposeCompilerApi public fun startReusableNode() /** * A Compose compiler plugin API. DO NOT call directly. * * Report the [factory] that will be used to create the node that will be generated into the * tree implied by the composition. This will only be called if [inserting] is is `true`. * * @param factory a factory function that will generate a node that will eventually be supplied * to [applier] though [Applier.insertBottomUp] and [Applier.insertTopDown]. */ @ComposeCompilerApi public fun createNode(factory: () -> T) /** * A Compose compiler plugin API. DO NOT call directly. * * Report that the node is still being used. This will be called in the same location as the * corresponding [createNode] when [inserting] is `false`. */ @ComposeCompilerApi public fun useNode() /** * A Compose compiler plugin API. DO NOT call directly. * * Called at the end of a node group. */ @ComposeCompilerApi public fun endNode() /** * A Compose compiler plugin API. DO NOT call directly. * * Start a reuse group. Unlike a movable group, in a reuse group if the [dataKey] changes the * composition shifts into a reusing state cause the composer to act like it is inserting (e.g. * [cache] acts as if all values are invalid, [changed] always returns true, etc.) even though * it is recomposing until it encounters a reusable node. If the node is reusable it temporarily * shifts into recomposition for the node and then shifts back to reusing for the children. If a * non-reusable node is generated the composer shifts to inserting for the node and all of its * children. * * @param key An compiler generated key based on the source location of the call. * @param dataKey A key provided by the [ReusableContent] composable function that is used to * determine if the composition shifts into a reusing state for this group. */ @ComposeCompilerApi public fun startReusableGroup(key: Int, dataKey: Any?) /** * A Compose compiler plugin API. DO NOT call directly. * * Called at the end of a reusable group. */ @ComposeCompilerApi public fun endReusableGroup() /** * A Compose compiler plugin API. DO NOT call directly. * * Temporarily disable reusing if it is enabled. */ @ComposeCompilerApi public fun disableReusing() /** * A Compose compiler plugin API. DO NOT call directly. * * Reenable reusing if it was previously enabled before the last call to [disableReusing]. */ @ComposeCompilerApi public fun enableReusing() /** * A Compose compiler plugin API. DO NOT call directly. * * Return a marker for the current group that can be used in a call to [endToMarker]. */ @ComposeCompilerApi public val currentMarker: Int /** * Compose compiler plugin API. DO NOT call directly. * * Ends all the groups up to but not including the group that is the parent group when * [currentMarker] was called to produce [marker]. All groups ended must have been started with * either [startReplaceableGroup] or [startMovableGroup]. Ending other groups can cause the * state of the composer to become inconsistent. */ @ComposeCompilerApi public fun endToMarker(marker: Int) /** * A Compose compiler plugin API. DO NOT call directly. * * Schedule [block] to called with [value]. This is intended to update the node generated by * [createNode] to changes discovered by composition. * * @param value the new value to be set into some property of the node. * @param block the block that sets the some property of the node to [value]. */ @ComposeCompilerApi public fun apply(value: V, block: T.(V) -> Unit) // State /** * A Compose compiler plugin API. DO NOT call directly. * * Produce an object that will compare equal an iff [left] and [right] compare equal to some * [left] and [right] of a previous call to [joinKey]. This is used by [key] to handle multiple * parameters. Since the previous composition stored [left] and [right] in a "join key" object * this call is used to return the previous value without an allocation instead of blindly * creating a new value that will be immediately discarded. * * @param left the first part of a a joined key. * @param right the second part of a joined key. * @return an object that will compare equal to a value previously returned by [joinKey] iff * [left] and [right] compare equal to the [left] and [right] passed to the previous call. */ @ComposeCompilerApi public fun joinKey(left: Any?, right: Any?): Any /** * A Compose compiler plugin API. DO NOT call directly. * * Remember a value into the composition state. This is a primitive method used to implement * [remember]. * * @return [Composer.Empty] when [inserting] is `true` or the value passed to * [updateRememberedValue] from the previous composition. * @see cache */ @ComposeCompilerApi public fun rememberedValue(): Any? /** * A Compose compiler plugin API. DO NOT call directly. * * Update the remembered value correspond to the previous call to [rememberedValue]. The [value] * will be returned by [rememberedValue] for the next composition. */ @ComposeCompilerApi public fun updateRememberedValue(value: Any?) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Any?): Boolean /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Boolean): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Char): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Byte): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Short): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Int): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Float): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Long): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition. This is used, for * example, to check parameter values to determine if they have changed. * * This overload is provided to avoid boxing [value] to compare with a potentially boxed version * of [value] in the composition state. * * @param value the value to check * @return `true` if the value if [equals] of the previous value returns `false` when passed * [value]. */ @ComposeCompilerApi public fun changed(value: Double): Boolean = changed(value) /** * A Compose compiler plugin API. DO NOT call directly. * * Check [value] is different than the value used in the previous composition using `===` * instead of `==` equality. This is used, for example, to check parameter values to determine * if they have changed for values that use value equality but, for correct behavior, the * composer needs reference equality. * * @param value the value to check * @return `true` if the value is === equal to the previous value and returns `false` when * [value] is different. */ @ComposeCompilerApi public fun changedInstance(value: Any?): Boolean = changed(value) // Scopes /** * A Compose compiler plugin API. DO NOT call directly. * * Mark [scope] as used. [endReplaceableGroup] will return `null` unless [recordUsed] is called * on the corresponding [scope]. This is called implicitly when [State] objects are read during * composition is called when [currentRecomposeScope] is called in the [Composable] function. */ @InternalComposeApi public fun recordUsed(scope: RecomposeScope) /** * A Compose compiler plugin API. DO NOT call directly. * * Generated by the compile to determine if the composable function should be executed. It may * not execute if parameter has not changed and the nothing else is forcing the function to * execute (such as its scope was invalidated or a static composition local it was changed) or * the composition is pausable and the composition is pausing. * * @param parametersChanged `true` if the parameters to the composable function have changed. * This is also `true` if the composition is [inserting] or if content is being reused. * @param flags The `$changed` parameter that contains the forced recompose bit to allow the * composer to disambiguate when the parameters changed due the execution being forced or if * the parameters actually changed. This is only ambiguous in a [PausableComposition] and is * necessary to determine if the function can be paused. The bits, other than 0, are reserved * for future use (which would required the bit 31, which is unused in `$changed` values, to * be set to indicate that the flags carry additional information). Passing the `$changed` * flags directly, instead of masking the 0 bit, is more efficient as it allows less code to * be generated per call to `shouldExecute` which is every called in every restartable * function, as well as allowing for the API to be extended without a breaking changed. */ @InternalComposeApi public fun shouldExecute(parametersChanged: Boolean, flags: Int): Boolean // Internal API /** * A Compose internal function. DO NOT call directly. * * Record a function to call when changes to the corresponding tree are applied to the * [applier]. This is used to implement [SideEffect]. * * @param effect a lambda to invoke after the changes calculated up to this point have been * applied. */ @InternalComposeApi public fun recordSideEffect(effect: () -> Unit) /** * Returns the active set of CompositionLocals at the current position in the composition * hierarchy. This is a lower level API that can be used to export and access CompositionLocal * values outside of Composition. * * This API does not track reads of CompositionLocals and does not automatically dispatch new * values to previous readers when the value of a CompositionLocal changes. To use this API as * intended, you must set up observation manually. This means: * - For [non-static CompositionLocals][compositionLocalOf], composables reading this map need * to observe the snapshot state for CompositionLocals being read to be notified when their * values in this map change. * - For [static CompositionLocals][staticCompositionLocalOf], all composables including the * composable reading this map will be recomposed and you will need to re-obtain this map to * get the latest values. * * Most applications shouldn't use this API directly, and should instead use * [CompositionLocal.current]. */ public val currentCompositionLocalMap: CompositionLocalMap /** * A Compose internal function. DO NOT call directly. * * Return the [CompositionLocal] value associated with [key]. This is the primitive function * used to implement [CompositionLocal.current]. * * @param key the [CompositionLocal] value to be retrieved. */ @InternalComposeApi public fun consume(key: CompositionLocal): T /** * A Compose internal function. DO NOT call directly. * * Provide the given values for the associated [CompositionLocal] keys. This is the primitive * function used to implement [CompositionLocalProvider]. * * @param values an array of value to provider key pairs. */ @InternalComposeApi public fun startProviders(values: Array>) /** * A Compose internal function. DO NOT call directly. * * End the provider group. * * @see startProviders */ @InternalComposeApi public fun endProviders() /** * A Compose internal function. DO NOT call directly. * * Provide the given value for the associated [CompositionLocal] key. This is the primitive * function used to implement [CompositionLocalProvider]. * * @param value a value to provider key pairs. */ @InternalComposeApi public fun startProvider(value: ProvidedValue<*>) /** * A Compose internal function. DO NOT call directly. * * End the provider group. * * @see startProvider */ @InternalComposeApi public fun endProvider() /** * A tooling API function. DO NOT call directly. * * The data stored for the composition. This is used by Compose tools, such as the preview and * the inspector, to display or interpret the result of composition. */ public val compositionData: CompositionData /** * A tooling API function. DO NOT call directly. * * Called by the inspector to inform the composer that it should collect additional information * about call parameters. By default, only collect parameter information for scopes that are * [recordUsed] has been called on. If [collectParameterInformation] is called it will attempt * to collect all calls even if the runtime doesn't need them. * * WARNING: calling this will result in a significant number of additional allocations that are * typically avoided. */ public fun collectParameterInformation() /** * Schedules an [action] to be invoked when the recomposer finishes the next composition of a * frame (including the completion of subcompositions). If a frame is currently in-progress, * [action] will be invoked when the current frame fully finishes composing. If a frame isn't * currently in-progress, a new frame will be scheduled (if one hasn't been already) and * [action] will execute at the completion of the next frame's composition. If a new frame is * scheduled and there is no other work to execute, [action] will still execute. * * [action] will always execute on the applier thread. * * Note that [action] runs at the end of a frame scheduled by the recomposer. If a callback is * scheduled via this method during the initial composition, it will not execute until the * _next_ frame. * * @return A [CancellationHandle] that can be used to unregister the [action]. The returned * handle is thread-safe and may be cancelled from any thread. Cancelling the handle only * removes the callback from the queue. If [action] is currently executing, it will not be * cancelled by this handle. */ public fun scheduleFrameEndCallback(action: () -> Unit): CancellationHandle /** * A Compose internal function. DO NOT call directly. * * Build a composition context that can be used to created a subcomposition. A composition * reference is used to communicate information from this composition to the subcompositions * such as the all the [CompositionLocal]s provided at the point the reference is created. */ @InternalComposeApi public fun buildContext(): CompositionContext /** * A Compose internal function. DO NOT call directly. * * The coroutine context for the composition. This is used, for example, to implement * [LaunchedEffect]. This context is managed by the [Recomposer]. */ @InternalComposeApi public val applyCoroutineContext: CoroutineContext @TestOnly get /** The composition that is used to control this composer. */ public val composition: ControlledComposition @TestOnly get /** * Disable the collection of source information, that may introduce groups to store the source * information, in order to be able to more accurately calculate the actual number of groups a * composable function generates in a release build. * * This function is only safe to call in a test and will produce incorrect composition results * if called on a composer not under test. */ @TestOnly public fun disableSourceInformation() public companion object { /** * A special value used to represent no value was stored (e.g. an empty slot). This is * returned, for example by [Composer.rememberedValue] while it is [Composer.inserting] is * `true`. */ public val Empty: Any = object { override fun toString() = "Empty" } /** * Internal API for specifying a tracer used for instrumenting frequent operations, e.g. * recompositions. */ @InternalComposeTracingApi public fun setTracer(tracer: CompositionTracer?) { compositionTracer = tracer } /** * Set the mode for collecting composition stack traces. See [ComposeStackTraceMode] for * more information about available modes. The stack traces are disabled by default. * * Note: changing stack trace collection mode will not affect already running compositions. */ public fun setDiagnosticStackTraceMode(mode: ComposeStackTraceMode) { composeStackTraceMode = mode } /** * Enable composition stack traces based on the source information. When this flag is * enabled, composition will record source information at runtime. When crash occurs, * Compose will append a suppressed exception that contains a stack trace pointing to the * place in composition closest to the crash. * * @see [ComposeStackTraceMode.SourceInformation] for more information. */ @Deprecated(message = "Use setDiagnosticStackTraceMode instead") @ExperimentalComposeRuntimeApi public fun setDiagnosticStackTraceEnabled(enabled: Boolean) { composeStackTraceMode = if (enabled) ComposeStackTraceMode.SourceInformation else ComposeStackTraceMode.None } } } internal abstract class InternalComposer : Composer { internal abstract val areChildrenComposing: Boolean internal abstract val isComposing: Boolean internal abstract val hasPendingChanges: Boolean internal abstract val currentRecomposeScope: RecomposeScopeImpl? internal abstract val errorContext: CompositionErrorContextImpl? internal abstract val deferredChanges: Changes? internal abstract val sourceMarkersEnabled: Boolean internal abstract fun startReuseFromRoot() internal abstract fun endReuseFromRoot() internal abstract fun changesApplied() internal abstract fun forceRecomposeScopes(): Boolean internal abstract fun dispose() internal abstract fun deactivate() internal abstract fun verifyConsistent() internal abstract fun stacksSize(): Int internal abstract fun stackTraceForValue(value: Any?): ComposeStackTrace internal abstract fun parentStackTrace(): List internal abstract fun prepareCompose(block: () -> Unit) internal abstract fun composeContent( invalidationsRequested: ScopeMap, content: @Composable () -> Unit, shouldPause: ShouldPauseCallback?, ) internal abstract fun recompose( invalidationsRequested: ScopeMap, shouldPause: ShouldPauseCallback?, ): Boolean internal abstract fun tryImminentInvalidation( scope: RecomposeScopeImpl, instance: Any?, ): Boolean internal abstract fun updateComposerInvalidations( invalidationsRequested: ScopeMap ) @TestOnly internal abstract fun parentKey(): Int } /** * A Compose compiler plugin API. DO NOT call directly. * * Cache, that is remember, a value in the composition data of a composition. This is used to * implement [remember] and used by the compiler plugin to generate more efficient calls to * [remember] when it determines these optimizations are safe. */ @ComposeCompilerApi public inline fun Composer.cache(invalid: Boolean, block: @DisallowComposableCalls () -> T): T { @Suppress("UNCHECKED_CAST") return rememberedValue().let { if (invalid || it === Composer.Empty) { val value = block() updateRememberedValue(value) value } else it } as T } /** * A Compose internal function. DO NOT call directly. * * Records source information that can be used for tooling to determine the source location of the * corresponding composable function. By default, this function is declared as having no * side-effects. It is safe for code shrinking tools (such as R8 or ProGuard) to remove it. */ @ComposeCompilerApi public fun sourceInformation(composer: Composer, sourceInformation: String) { composer.sourceInformation(sourceInformation) } /** * A Compose internal function. DO NOT call directly. * * Records the start of a source information marker that can be used for tooling to determine the * source location of the corresponding composable function that otherwise don't require tracking * information such as [ReadOnlyComposable] functions. By default, this function is declared as * having no side-effects. It is safe for code shrinking tools (such as R8 or ProGuard) to remove * it. * * Important that both [sourceInformationMarkerStart] and [sourceInformationMarkerEnd] are removed * together or both kept. Removing only one will cause incorrect runtime behavior. */ @ComposeCompilerApi public fun sourceInformationMarkerStart(composer: Composer, key: Int, sourceInformation: String) { composer.sourceInformationMarkerStart(key, sourceInformation) } /** * Internal tracing API. * * Should be called without thread synchronization with occasional information loss. */ @InternalComposeTracingApi public interface CompositionTracer { public fun traceEventStart(key: Int, dirty1: Int, dirty2: Int, info: String): Unit public fun traceEventEnd(): Unit public fun isTraceInProgress(): Boolean } @OptIn(InternalComposeTracingApi::class) private var compositionTracer: CompositionTracer? = null internal var composeStackTraceMode = ComposeStackTraceMode.None /** * Internal tracing API. * * Should be called without thread synchronization with occasional information loss. */ @OptIn(InternalComposeTracingApi::class) @ComposeCompilerApi public fun isTraceInProgress(): Boolean = compositionTracer.let { it != null && it.isTraceInProgress() } @OptIn(InternalComposeTracingApi::class) @ComposeCompilerApi @Deprecated( message = "Use the overload with \$dirty metadata instead", ReplaceWith("traceEventStart(key, dirty1, dirty2, info)"), DeprecationLevel.HIDDEN, ) public fun traceEventStart(key: Int, info: String): Unit = traceEventStart(key, -1, -1, info) /** * Internal tracing API. * * Should be called without thread synchronization with occasional information loss. * * @param key is a group key generated by the compiler plugin for the function being traced. This * key is unique the function. * @param dirty1 $dirty metadata: forced-recomposition and function parameters 1..10 if present * @param dirty2 $dirty2 metadata: forced-recomposition and function parameters 11..20 if present * @param info is a user displayable string that describes the function for which this is the start * event. */ @OptIn(InternalComposeTracingApi::class) @ComposeCompilerApi public fun traceEventStart(key: Int, dirty1: Int, dirty2: Int, info: String) { compositionTracer?.traceEventStart(key, dirty1, dirty2, info) } /** * Internal tracing API. * * Should be called without thread synchronization with occasional information loss. */ @OptIn(InternalComposeTracingApi::class) @ComposeCompilerApi public fun traceEventEnd() { compositionTracer?.traceEventEnd() } /** * A Compose internal function. DO NOT call directly. * * Records the end of a source information marker that can be used for tooling to determine the * source location of the corresponding composable function that otherwise don't require tracking * information such as [ReadOnlyComposable] functions. By default, this function is declared as * having no side-effects. It is safe for code shrinking tools (such as R8 or ProGuard) to remove * it. * * Important that both [sourceInformationMarkerStart] and [sourceInformationMarkerEnd] are removed * together or both kept. Removing only one will cause incorrect runtime behavior. */ @ComposeCompilerApi public fun sourceInformationMarkerEnd(composer: Composer) { composer.sourceInformationMarkerEnd() } /** * A helper receiver scope class used by [ComposeNode] to help write code to initialized and update * a node. * * @see ComposeNode */ @JvmInline public value class Updater constructor(@PublishedApi internal val composer: Composer) { /** * Set the value property of the emitted node. * * Schedules [block] to be run when the node is first created or when [value] is different than * the previous composition. * * @see update */ @Deprecated("Boxes more than than the generic overload", level = DeprecationLevel.HIDDEN) @Suppress("NOTHING_TO_INLINE") public inline fun set(value: Int, noinline block: T.(value: Int) -> Unit): Unit = with(composer) { if (inserting || rememberedValue() != value) { updateRememberedValue(value) composer.apply(value, block) } } /** * Set the value property of the emitted node. * * Schedules [block] to be run when the node is first created or when [value] is different than * the previous composition. * * @see update */ public fun set(value: V, block: T.(value: V) -> Unit): Unit = with(composer) { if (inserting || rememberedValue() != value) { updateRememberedValue(value) composer.apply(value, block) } } /** * Update the value of a property of the emitted node. * * Schedules [block] to be run when [value] is different than the previous composition. It is * different than [set] in that it does not run when the node is created. This is used when * initial value set by the [ComposeNode] in the constructor callback already has the correct * value. For example, use [update} when [value] is passed into of the classes constructor * parameters. * * @see set */ @Deprecated("Boxes more than the generic overload", level = DeprecationLevel.HIDDEN) @Suppress("NOTHING_TO_INLINE") public inline fun update(value: Int, noinline block: T.(value: Int) -> Unit): Unit = with(composer) { val inserting = inserting if (inserting || rememberedValue() != value) { updateRememberedValue(value) if (!inserting) apply(value, block) } } /** * Update the value of a property of the emitted node. * * Schedules [block] to be run when [value] is different than the previous composition. It is * different than [set] in that it does not run when the node is created. This is used when * initial value set by the [ComposeNode] in the constructor callback already has the correct * value. For example, use [update} when [value] is passed into of the classes constructor * parameters. * * @see set */ public fun update(value: V, block: T.(value: V) -> Unit): Unit = with(composer) { val inserting = inserting if (inserting || rememberedValue() != value) { updateRememberedValue(value) if (!inserting) apply(value, block) } } /** * Initialize emitted node. * * Schedule [block] to be executed after the node is created. * * This is only executed once. The can be used to call a method or set a value on a node * instance that is required to be set after one or more other properties have been set. * * @see reconcile */ public fun init(block: T.() -> Unit) { if (composer.inserting) composer.apply(Unit) { block() } } /** * Initialize emitted node. * * Schedule [block] to be executed after the node is created. * * This is only executed once. The can be used to call a method or set a value on a node * instance that is required to be set after one or more other properties have been set. * * This is different from the other [init] overload in that it does not force creating a lambda * to capture [value]. */ public fun init(value: V, block: T.(V) -> Unit) { if (composer.inserting) composer.apply(value, block) } /** * Reconcile the node to the current state. * * This is used when [set] and [update] are insufficient to update the state of the node based * on changes passed to the function calling [ComposeNode]. * * Schedules [block] to execute. As this unconditionally schedules [block] to executed it might * be executed unnecessarily as no effort is taken to ensure it only executes when the values * [block] captures have changed. It is highly recommended that [set] and [update] be used * instead as they will only schedule their blocks to executed when the value passed to them has * changed. */ @Suppress("MemberVisibilityCanBePrivate") public fun reconcile(block: T.() -> Unit) { composer.apply(Unit) { this.block() } } } @JvmInline public value class SkippableUpdater constructor(@PublishedApi internal val composer: Composer) { public inline fun update(block: Updater.() -> Unit) { composer.startReplaceableGroup(0x1e65194f) Updater(composer).block() composer.endReplaceableGroup() } } internal fun SlotWriter.removeCurrentGroup(rememberManager: RememberManager) { // Notify the lifecycle manager of any observers leaving the slot table // The notification order should ensure that listeners are notified of leaving // in opposite order that they are notified of entering. // To ensure this order, we call `enters` as a pre-order traversal // of the group tree, and then call `leaves` in the inverse order. forAllDataInRememberOrder(currentGroup) { _, slot -> // even that in the documentation we claim ComposeNodeLifecycleCallback should be only // implemented on the nodes we do not really enforce it here as doing so will be expensive. if (slot is ComposeNodeLifecycleCallback) { rememberManager.releasing(slot) } if (slot is RememberObserverHolder) { rememberManager.forgetting(slot) } if (slot is RecomposeScopeImpl) { slot.release() } } removeGroup() } internal inline fun SlotWriter.withAfterAnchorInfo(anchor: Anchor?, cb: (Int, Int) -> R) { var priority = -1 var endRelativeAfter = -1 if (anchor != null && anchor.valid) { priority = anchorIndex(anchor.asGapAnchor()) endRelativeAfter = slotsSize - slotsEndAllIndex(priority) } cb(priority, endRelativeAfter) } internal val SlotWriter.isAfterFirstChild get() = currentGroup > parent + 1 internal val SlotReader.isAfterFirstChild get() = currentGroup > parent + 1 /** * Remember observer which is not removed during reuse/deactivate of the group. It is used to * preserve composition locals between group deactivation. */ internal interface ReusableRememberObserverHolder : RememberObserverHolder internal interface RememberObserverHolder { var wrapped: RememberObserver } // An arbitrary key value that marks the default parameter group internal const val defaultsKey = -127 @PublishedApi internal const val invocationKey: Int = 200 @PublishedApi internal val invocation: Any = OpaqueKey("provider") @PublishedApi internal const val providerKey: Int = 201 @PublishedApi internal val provider: Any = OpaqueKey("provider") @PublishedApi internal const val compositionLocalMapKey: Int = 202 @PublishedApi internal val compositionLocalMap: Any = OpaqueKey("compositionLocalMap") @PublishedApi internal const val providerValuesKey: Int = 203 @PublishedApi internal val providerValues: Any = OpaqueKey("providerValues") @PublishedApi internal const val providerMapsKey: Int = 204 @PublishedApi internal val providerMaps: Any = OpaqueKey("providers") @PublishedApi internal const val referenceKey: Int = 206 @PublishedApi internal val reference: Any = OpaqueKey("reference") @PublishedApi internal const val reuseKey: Int = 207 private const val invalidGroupLocation = -2 internal class ComposeRuntimeError(override val message: String) : IllegalStateException() @Suppress("BanInlineOptIn") @OptIn(ExperimentalContracts::class) internal inline fun runtimeCheck(value: Boolean, lazyMessage: () -> String) { contract { returns() implies value } if (!value) { composeImmediateRuntimeError(lazyMessage()) } } internal const val EnableDebugRuntimeChecks = false /** * A variation of [composeRuntimeError] that gets stripped from R8-minified builds. Use this for * more expensive checks or assertions along a hotpath that, if failed, would still lead to an * application crash that could be traced back to this assertion if removed from the final program * binary. */ internal inline fun debugRuntimeCheck(value: Boolean, lazyMessage: () -> String) { if (EnableDebugRuntimeChecks && !value) { composeImmediateRuntimeError(lazyMessage()) } } internal inline fun debugRuntimeCheck(value: Boolean) = debugRuntimeCheck(value) { "Check failed" } internal inline fun runtimeCheck(value: Boolean) = runtimeCheck(value) { "Check failed" } internal fun composeRuntimeError(message: String): Nothing { throw ComposeRuntimeError( "Compose Runtime internal error. Unexpected or incorrect use of the Compose " + "internal runtime API ($message). Please report to Google or use " + "https://goo.gle/compose-feedback" ) } // Unit variant of composeRuntimeError() so the call site doesn't add 3 extra // instructions to throw a KotlinNothingValueException internal fun composeImmediateRuntimeError(message: String) { throw ComposeRuntimeError( "Compose Runtime internal error. Unexpected or incorrect use of the Compose " + "internal runtime API ($message). Please report to Google or use " + "https://goo.gle/compose-feedback" ) } /** * Extract the state of movable content from the given writer. A new slot table is created and the * content is removed from [slots] (leaving a movable content group that, if composed over, will * create new content) and added to this new slot table. The invalidations that occur to recompose * scopes in the movable content state will be collected and forwarded to the new composition if the * state is used. */ internal fun extractMovableContentAtCurrent( composition: ControlledComposition, reference: MovableContentStateReference, slots: SlotWriter, applier: Applier<*>?, ): MovableContentState { val slotTable = SlotTable() if (slots.collectingSourceInformation) { slotTable.collectSourceInformation() } if (slots.collectingCalledInformation) { slotTable.collectCalledByInformation() } // If an applier is provided then we are extracting a state from the middle of an // already extracted state. If the group has nodes then the nodes need to be removed // from their parent so they can potentially be inserted into a destination. val currentGroup = slots.currentGroup if (applier != null && slots.nodeCount(currentGroup) > 0) { @Suppress("UNCHECKED_CAST") applier as Applier // Find the parent node by going up until the first node group var parentNodeGroup = slots.parent while (parentNodeGroup > 0 && !slots.isNode(parentNodeGroup)) { parentNodeGroup = slots.parent(parentNodeGroup) } // If we don't find a node group the nodes in the state have already been removed // as they are the nodes that were removed when the state was removed from the original // table. if (parentNodeGroup >= 0 && slots.isNode(parentNodeGroup)) { val node = slots.node(parentNodeGroup) var currentChild = parentNodeGroup + 1 val end = parentNodeGroup + slots.groupSize(parentNodeGroup) // Find the node index var nodeIndex = 0 while (currentChild < end) { val size = slots.groupSize(currentChild) if (currentChild + size > currentGroup) { break } nodeIndex += if (slots.isNode(currentChild)) 1 else slots.nodeCount(currentChild) currentChild += size } // Remove the nodes val count = if (slots.isNode(currentGroup)) 1 else slots.nodeCount(currentGroup) applier.down(node) applier.remove(nodeIndex, count) applier.up() } } // Transfer invalidations before moving the scopes, since we could have accumulated more after // creating the state. val anchor = reference.anchor if (anchor.valid) { val extracted = (composition as CompositionImpl).extractInvalidationsOfGroup { slots.inGroup(anchor.asGapAnchor(), it.asGapAnchor()) } reference.invalidations += extracted } // Write a table that as if it was written by a calling invokeMovableContentLambda because this // might be removed from the composition before the new composition can be composed to receive // it. When the new composition receives the state it must recompose over the state by calling // invokeMovableContentLambda. val anchors = slotTable.write { writer -> writer.beginInsert() // This is the prefix created by invokeMovableContentLambda writer.startGroup(movableContentKey, reference.content) writer.markGroup() writer.update(reference.parameter) // Move the content into current location val anchors = slots.moveTo(reference.anchor.asGapAnchor(), 1, writer) // skip the group that was just inserted. writer.skipGroup() // End the group that represents the call to invokeMovableContentLambda writer.endGroup() writer.endInsert() anchors } val state = MovableContentState(slotTable) if (RecomposeScopeImpl.hasAnchoredRecomposeScopes(slotTable, anchors)) { // If any recompose scopes are invalidated while the movable content is outside a // composition, ensure the reference is updated to contain the invalidation. val movableContentRecomposeScopeOwner = object : RecomposeScopeOwner { override fun invalidate( scope: RecomposeScopeImpl, instance: Any?, ): InvalidationResult { // Try sending this to the original owner first. val result = (composition as? RecomposeScopeOwner)?.invalidate(scope, instance) ?: InvalidationResult.IGNORED // If the original owner ignores this then we need to record it in the // reference if (result == InvalidationResult.IGNORED) { reference.invalidations += scope to instance return InvalidationResult.SCHEDULED } return result } // The only reason [recomposeScopeReleased] is called is when the recompose scope is // removed from the table. First, this never happens for content that is moving, and // 2) even if it did the only reason we tell the composer is to clear tracking // tables that contain this information which is not relevant here. override fun recomposeScopeReleased(scope: RecomposeScopeImpl) { // Nothing to do } // [recordReadOf] this is also something that would happen only during active // recomposition which doesn't happened to a slot table that is moving. override fun recordReadOf(value: Any) { // Nothing to do } } slotTable.write { writer -> RecomposeScopeImpl.adoptAnchoredScopes( slots = writer, anchors = anchors, newOwner = movableContentRecomposeScopeOwner, ) } } return state } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Recomposer.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.runtime import androidx.collection.MutableObjectList import androidx.collection.MutableScatterSet import androidx.collection.ScatterSet import androidx.collection.emptyObjectList import androidx.collection.emptyScatterSet import androidx.collection.mutableScatterMapOf import androidx.collection.mutableScatterSetOf import androidx.compose.runtime.collection.MultiValueMap import androidx.compose.runtime.collection.fastForEach import androidx.compose.runtime.collection.fastMap import androidx.compose.runtime.collection.mutableVectorOf import androidx.compose.runtime.collection.wrapIntoSet import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentSetOf import androidx.compose.runtime.internal.AtomicReference import androidx.compose.runtime.internal.SnapshotThreadLocal import androidx.compose.runtime.internal.logError import androidx.compose.runtime.internal.trace import androidx.compose.runtime.platform.SynchronizedObject import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import androidx.compose.runtime.snapshots.MutableSnapshot import androidx.compose.runtime.snapshots.ReaderKind import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotApplyResult import androidx.compose.runtime.snapshots.StateObjectImpl import androidx.compose.runtime.snapshots.TransparentObserverMutableSnapshot import androidx.compose.runtime.snapshots.TransparentObserverSnapshot import androidx.compose.runtime.snapshots.fastAll import androidx.compose.runtime.snapshots.fastAny import androidx.compose.runtime.snapshots.fastFilterIndexed import androidx.compose.runtime.snapshots.fastForEach import androidx.compose.runtime.snapshots.fastGroupBy import androidx.compose.runtime.snapshots.fastMap import androidx.compose.runtime.snapshots.fastMapNotNull import androidx.compose.runtime.tooling.ComposeStackTraceMode import androidx.compose.runtime.tooling.ComposeToolingApi import androidx.compose.runtime.tooling.CompositionData import androidx.compose.runtime.tooling.CompositionObserverHandle import androidx.compose.runtime.tooling.CompositionRegistrationObserver import androidx.compose.runtime.tooling.ObservableComposition import androidx.compose.runtime.tooling.observe import kotlin.collections.removeLast as removeLastKt import kotlin.coroutines.Continuation import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext import kotlin.coroutines.resume import kotlin.native.concurrent.ThreadLocal import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.takeWhile import kotlinx.coroutines.job import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext internal const val recomposerKey = 1000 // TODO: Can we use rootKey for this since all compositions will have an eventual Recomposer parent? private inline val RecomposerCompoundHashKey get() = CompositeKeyHashCode(recomposerKey) /** * Runs [block] with a new, active [Recomposer] applying changes in the calling [CoroutineContext]. * The [Recomposer] will be [closed][Recomposer.close] after [block] returns. * [withRunningRecomposer] will return once the [Recomposer] is [Recomposer.State.ShutDown] and all * child jobs launched by [block] have [joined][Job.join]. */ public suspend fun withRunningRecomposer( block: suspend CoroutineScope.(recomposer: Recomposer) -> R ): R = coroutineScope { val recomposer = Recomposer(coroutineContext) // Will be cancelled when recomposerJob cancels launch { recomposer.runRecomposeAndApplyChanges() } block(recomposer).also { recomposer.close() recomposer.join() } } /** * Read-only information about a [Recomposer]. Used when code should only monitor the activity of a * [Recomposer], and not attempt to alter its state or create new compositions from it. */ public interface RecomposerInfo { /** The current [State] of the [Recomposer]. See each [State] value for its meaning. */ // TODO: Mirror the currentState/StateFlow API change here once we can safely add // default interface methods. https://youtrack.jetbrains.com/issue/KT-47000 public val state: Flow /** * `true` if the [Recomposer] has been assigned work to do and it is currently performing that * work or awaiting an opportunity to do so. */ public val hasPendingWork: Boolean /** * The running count of the number of times the [Recomposer] awoke and applied changes to one or * more [Composer]s. This count is unaffected if the composer awakes and recomposed but * composition did not produce changes to apply. */ public val changeCount: Long /** * Get flow of error states captured in composition. This flow is only available when recomposer * is in hot reload mode. * * @return a flow of error states captured during composition */ @ComposeToolingApi public val errorState: StateFlow get() = DefaultErrorStateFlow /** * Register an observer to be notified when a composition is added to or removed from the given * [Recomposer]. When this method is called, the observer will be notified of all currently * registered compositions per the documentation in * [CompositionRegistrationObserver.onCompositionRegistered]. */ @ExperimentalComposeRuntimeApi public fun observe(observer: CompositionRegistrationObserver): CompositionObserverHandle? = null private companion object { @ComposeToolingApi private val DefaultErrorStateFlow: StateFlow = MutableStateFlow(null) } } /** Read only information about [Recomposer] error state. */ @ComposeToolingApi public interface RecomposerErrorInformation { /** Exception which forced recomposition to halt. */ public val cause: Throwable /** * Whether composition can recover from the error by itself. If the error is not recoverable, * recomposer will not react to invalidate calls until state is reloaded. */ public val isRecoverable: Boolean } /** * Read only information about [Recomposer] error state. This is an internal API only kept for * backward compatibility. */ // TODO(b/469471141): Remove when Live Edit no longer depends on this API. @InternalComposeApi internal interface RecomposerErrorInfo { /** Exception which forced recomposition to halt. */ val cause: Throwable /** * Whether composition can recover from the error by itself. If the error is not recoverable, * recomposer will not react to invalidate calls until state is reloaded. */ val recoverable: Boolean } /** * The scheduler for performing recomposition and applying updates to one or more [Composition]s. */ // RedundantVisibilityModifier suppressed because metalava picks up internal function overrides // if 'internal' is not explicitly specified - b/171342041 // NotCloseable suppressed because this is Kotlin-only common code; [Auto]Closeable not available. @Suppress("RedundantVisibilityModifier", "NotCloseable") @OptIn(InternalComposeApi::class) public class Recomposer(effectCoroutineContext: CoroutineContext) : CompositionContext() { /** * This is a running count of the number of times the recomposer awoke and applied changes to * one or more composers. This count is unaffected if the composer awakes and recomposed but * composition did not produce changes to apply. */ public var changeCount: Long = 0L private set private val broadcastFrameClock = BroadcastFrameClock { onNewFrameAwaiter() } private val nextFrameEndCallbackQueue = NextFrameEndCallbackQueue { onNewFrameAwaiter() } /** Valid operational states of a [Recomposer]. */ public enum class State { /** * [cancel] was called on the [Recomposer] and all cleanup work has completed. The * [Recomposer] is no longer available for use. */ ShutDown, /** * [cancel] was called on the [Recomposer] and it is no longer available for use. Cleanup * work has not yet been fully completed and composition effect coroutines may still be * running. */ ShuttingDown, /** * The [Recomposer] is not tracking invalidations for known composers and it will not * recompose them in response to changes. Call [runRecomposeAndApplyChanges] to await and * perform work. This is the initial state of a newly constructed [Recomposer]. */ Inactive, /** * The [Recomposer] is [Inactive] but at least one effect associated with a managed * composition is awaiting a frame. This frame will not be produced until the [Recomposer] * is [running][runRecomposeAndApplyChanges]. */ InactivePendingWork, /** * The [Recomposer] is tracking composition and snapshot invalidations but there is * currently no work to do. */ Idle, /** * The [Recomposer] has been notified of pending work it must perform and is either actively * performing it or awaiting the appropriate opportunity to perform it. This work may * include invalidated composers that must be recomposed, snapshot state changes that must * be presented to known composers to check for invalidated compositions, or coroutines * awaiting a frame using the Recomposer's [MonotonicFrameClock]. */ PendingWork, } private val stateLock = makeSynchronizedObject() // Begin properties guarded by stateLock private var runnerJob: Job? = null private var closeCause: Throwable? = null private val _knownCompositions = mutableListOf() private var _knownCompositionsCache: List? = null private var snapshotInvalidations = MutableScatterSet() private val compositionInvalidations = mutableVectorOf() private val compositionsAwaitingApply = mutableListOf() private val movableContentAwaitingInsert = mutableListOf() private val movableContentRemoved = MultiValueMap, MovableContentStateReference>() private val movableContentNestedStatesAvailable = NestedContentMap() private val movableContentStatesAvailable = mutableScatterMapOf() private val movableContentNestedExtractionsPending = MultiValueMap() private var failedCompositions: MutableList? = null private var compositionsRemoved: MutableScatterSet? = null private var workContinuation: CancellableContinuation? = null private var concurrentCompositionsOutstanding = 0 private var isClosed: Boolean = false private var errorState = MutableStateFlow(null) private var frameClockPaused: Boolean = false // End properties guarded by stateLock private val _state = MutableStateFlow(State.Inactive) private val pausedScopes = SnapshotThreadLocal?>() /** * A [Job] used as a parent of any effects created by this [Recomposer]'s compositions. Its * cleanup is used to advance to [State.ShuttingDown] or [State.ShutDown]. * * Initialized after other state above, since it is possible for [Job.invokeOnCompletion] to run * synchronously during construction if the [Recomposer] is constructed with a completed or * cancelled [Job]. */ private val effectJob = Job(effectCoroutineContext[Job]).apply { invokeOnCompletion { throwable -> // Since the running recompose job is operating in a disjoint job if present, // kick it out and make sure no new ones start if we have one. val cancellation = CancellationException("Recomposer effect job completed", throwable) var continuationToResume: CancellableContinuation? = null synchronized(stateLock) { val runnerJob = runnerJob if (runnerJob != null) { _state.value = State.ShuttingDown // If the recomposer is closed we will let the runnerJob return from // runRecomposeAndApplyChanges normally and consider ourselves shut down // immediately. if (!isClosed) { // This is the job hosting frameContinuation; no need to resume it // otherwise runnerJob.cancel(cancellation) } else if (workContinuation != null) { continuationToResume = workContinuation } workContinuation = null runnerJob.invokeOnCompletion { runnerJobCause -> synchronized(stateLock) { closeCause = throwable?.apply { runnerJobCause ?.takeIf { it !is CancellationException } ?.let { addSuppressed(it) } } _state.value = State.ShutDown } } } else { closeCause = cancellation _state.value = State.ShutDown } } continuationToResume?.resume(Unit) } } /** The [effectCoroutineContext] is derived from the parameter of the same name. */ override val effectCoroutineContext: CoroutineContext = effectCoroutineContext + broadcastFrameClock + effectJob private val hasBroadcastFrameClockAwaitersLocked: Boolean get() = !frameClockPaused && broadcastFrameClock.hasAwaiters private val hasNextFrameEndAwaitersLocked: Boolean get() = !frameClockPaused && nextFrameEndCallbackQueue.hasAwaiters private val hasBroadcastFrameClockAwaiters: Boolean get() = synchronized(stateLock) { hasBroadcastFrameClockAwaitersLocked } @OptIn(ExperimentalComposeRuntimeApi::class) private var registrationObservers: MutableObjectList? = null /** * Determine the new value of [_state]. Call only while locked on [stateLock]. If it returns a * continuation, that continuation should be resumed after releasing the lock. */ private fun deriveStateLocked(): CancellableContinuation? { if (_state.value <= State.ShuttingDown) { clearKnownCompositionsLocked() snapshotInvalidations = MutableScatterSet() compositionInvalidations.clear() compositionsAwaitingApply.clear() movableContentAwaitingInsert.clear() failedCompositions = null workContinuation?.cancel() workContinuation = null errorState.value = null return null } val newState = when { errorState.value != null -> { State.Inactive } runnerJob == null -> { snapshotInvalidations = MutableScatterSet() compositionInvalidations.clear() if (hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked) State.InactivePendingWork else State.Inactive } compositionInvalidations.isNotEmpty() || snapshotInvalidations.isNotEmpty() || compositionsAwaitingApply.isNotEmpty() || movableContentAwaitingInsert.isNotEmpty() || concurrentCompositionsOutstanding > 0 || hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked || movableContentRemoved.isNotEmpty() -> State.PendingWork else -> State.Idle } _state.value = newState return if (newState == State.PendingWork) { workContinuation.also { workContinuation = null } } else null } private fun onNewFrameAwaiter() { synchronized(stateLock) { deriveStateLocked().also { if (_state.value <= State.ShuttingDown) throw CancellationException( "Recomposer shutdown; frame clock awaiter will never resume", closeCause, ) } } ?.resume(Unit) } /** `true` if there is still work to do for an active caller of [runRecomposeAndApplyChanges] */ private val shouldKeepRecomposing: Boolean get() = synchronized(stateLock) { !isClosed } || effectJob.children.any { it.isActive } /** The current [State] of this [Recomposer]. See each [State] value for its meaning. */ @Deprecated("Replaced by currentState as a StateFlow", ReplaceWith("currentState")) public val state: Flow get() = currentState /** The current [State] of this [Recomposer], available synchronously. */ public val currentState: StateFlow get() = _state // A separate private object to avoid the temptation of casting a RecomposerInfo // to a Recomposer if Recomposer itself were to implement RecomposerInfo. private inner class RecomposerInfoImpl : RecomposerInfo { override val state: Flow get() = this@Recomposer.currentState override val hasPendingWork: Boolean get() = this@Recomposer.hasPendingWork override val changeCount: Long get() = this@Recomposer.changeCount @ComposeToolingApi override val errorState: StateFlow get() = this@Recomposer.errorState @ComposeToolingApi val currentError: RecomposerErrorInformation? get() = synchronized(stateLock) { this@Recomposer.errorState.value } @OptIn(ExperimentalComposeRuntimeApi::class) override fun observe(observer: CompositionRegistrationObserver): CompositionObserverHandle = this@Recomposer.observe(observer) fun invalidateGroupsWithKey(key: Int) { val compositions: List = knownCompositions() compositions .fastMapNotNull { it as? CompositionImpl } .fastForEach { it.invalidateGroupsWithKey(key) } } fun saveStateAndDisposeForHotReload(): List { val compositions: List = knownCompositions() return compositions .fastMapNotNull { it as? CompositionImpl } .fastMap { HotReloadable(it).apply { clearContent() } } } fun resetErrorState(): RecomposerErrorState? = this@Recomposer.resetErrorState() fun retryFailedCompositions() = this@Recomposer.retryFailedCompositions() } private class HotReloadable(private val composition: CompositionImpl) { private var composable: @Composable () -> Unit = composition.composable fun clearContent() { if (composition.isRoot) { composition.setContent {} } } fun resetContent() { composition.composable = composable } fun recompose() { if (composition.isRoot) { composition.setContent(composable) } } } @OptIn(ComposeToolingApi::class) private class RecomposerErrorState( override val cause: Throwable, override val isRecoverable: Boolean, ) : RecomposerErrorInfo, RecomposerErrorInformation { override val recoverable: Boolean get() = isRecoverable } private val recomposerInfo = RecomposerInfoImpl() /** Obtain a read-only [RecomposerInfo] for this [Recomposer]. */ public fun asRecomposerInfo(): RecomposerInfo = recomposerInfo /** * Propagate all invalidations from `snapshotInvalidations` to all the known compositions. * * @return `true` if the frame has work to do (e.g. [hasFrameWorkLocked]) */ private fun recordComposerModifications(): Boolean { var compositions: List = emptyList() val changes = synchronized(stateLock) { if (snapshotInvalidations.isEmpty()) return hasFrameWorkLocked compositions = knownCompositionsLocked() snapshotInvalidations.wrapIntoSet().also { snapshotInvalidations = MutableScatterSet() } } var complete = false try { run { compositions.fastForEach { composition -> composition.recordModificationsOf(changes) // Stop dispatching if the recomposer if we detect the recomposer // is shutdown. if (_state.value <= State.ShuttingDown) return@run } } complete = true } finally { if (!complete) { // If the previous loop was not complete, we have not sent all of theses // changes to all the composers so try again after the exception that caused // the early exit is handled and we can then retry sending the changes. synchronized(stateLock) { snapshotInvalidations.addAll(changes) } } } return synchronized(stateLock) { if (deriveStateLocked() != null) { error("called outside of runRecomposeAndApplyChanges") } hasFrameWorkLocked } } private fun registerRunnerJob(callingJob: Job) { synchronized(stateLock) { closeCause?.let { throw it } if (_state.value <= State.ShuttingDown) error("Recomposer shut down") if (runnerJob != null) error("Recomposer already running") runnerJob = callingJob if (deriveStateLocked() != null) { composeImmediateRuntimeError("called outside of runRecomposeAndApplyChanges") } } } /** * Await the invalidation of any associated [Composer]s, recompose them, and apply their changes * to their associated [Composition]s if recomposition is successful. * * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no * more invalid composers awaiting recomposition. * * This method will not return unless the [Recomposer] is [close]d and all effects in managed * compositions complete. Unhandled failure exceptions from child coroutines will be thrown by * this method. */ public suspend fun runRecomposeAndApplyChanges(): Unit = recompositionRunner { parentFrameClock -> val toRecompose = mutableListOf() val toInsert = mutableListOf() val toApply = mutableListOf() val toLateApply = mutableScatterSetOf() val toComplete = mutableScatterSetOf() val modifiedValues = MutableScatterSet() val modifiedValuesSet = modifiedValues.wrapIntoSet() val alreadyComposed = mutableScatterSetOf() fun clearRecompositionState() { synchronized(stateLock) { toRecompose.clear() toInsert.clear() toApply.fastForEach { it.abandonChanges() recordFailedCompositionLocked(it) } toApply.clear() toLateApply.forEach { it.abandonChanges() recordFailedCompositionLocked(it) } toLateApply.clear() toComplete.forEach { it.changesApplied() } toComplete.clear() modifiedValues.clear() alreadyComposed.forEach { it.abandonChanges() recordFailedCompositionLocked(it) } alreadyComposed.clear() } } fun fillToInsert() { toInsert.clear() synchronized(stateLock) { movableContentAwaitingInsert.fastForEach { toInsert += it } movableContentAwaitingInsert.clear() } } while (shouldKeepRecomposing) { awaitWorkAvailable() // Don't await a new frame if we don't have frame-scoped work if (!recordComposerModifications()) continue // Align work with the next frame to coalesce changes. // Note: it is possible to resume from the above with no recompositions pending, // instead someone might be awaiting our frame clock dispatch below. // We use the cached frame clock from above not just so that we don't locate it // each time, but because we've installed the broadcastFrameClock as the scope // clock above for user code to locate. parentFrameClock.withFrameNanos { frameTime -> // Dispatch MonotonicFrameClock frames first; this may produce new // composer invalidations that we must handle during the same frame. if (hasBroadcastFrameClockAwaiters) { trace("Recomposer:animation") { // Propagate the frame time to anyone who is awaiting from the // recomposer clock. broadcastFrameClock.sendFrame(frameTime) // Ensure any global changes are observed Snapshot.sendApplyNotifications() } } trace("Recomposer:recompose") { // Drain any composer invalidations from snapshot changes and record // composers to work on recordComposerModifications() synchronized(stateLock) { compositionInvalidations.forEach { toRecompose += it } compositionInvalidations.clear() } // Perform recomposition for any invalidated composers modifiedValues.clear() alreadyComposed.clear() while (toRecompose.isNotEmpty() || toInsert.isNotEmpty()) { try { toRecompose.fastForEach { composition -> performRecompose(composition, modifiedValues)?.let { toApply += it } alreadyComposed.add(composition) } } catch (e: Throwable) { processCompositionError(e, recoverable = true) clearRecompositionState() return@withFrameNanos } finally { toRecompose.clear() } // Find any trailing recompositions that need to be composed because // of a value change by a composition. This can happen, for example, if // a CompositionLocal changes in a parent and was read in a child // composition that was otherwise valid. if ( modifiedValues.isNotEmpty() || compositionInvalidations.isNotEmpty() ) { synchronized(stateLock) { knownCompositionsLocked().fastForEach { value -> if ( value !in alreadyComposed && value.observesAnyOf(modifiedValuesSet) ) { toRecompose += value } } // Composable lambda is a special kind of value that is not // observed // by the snapshot system, but invalidates composition scope // directly instead. compositionInvalidations.removeIf { value -> if (value !in alreadyComposed && value !in toRecompose) { toRecompose += value true } else { false } } } } if (toRecompose.isEmpty()) { try { fillToInsert() while (toInsert.isNotEmpty()) { toLateApply += performInsertValues(toInsert, modifiedValues) fillToInsert() } } catch (e: Throwable) { processCompositionError(e, recoverable = true) clearRecompositionState() return@withFrameNanos } } } // This is an optimization to avoid reallocating TransparentSnapshot for // each observeChanges within `apply`. Many modifiers use observation in // `onAttach` and other lifecycle methods, and allocations can be mitigated // by updating read observer in the snapshot allocated here. withTransparentSnapshot { if (toApply.isNotEmpty()) { changeCount++ // Perform apply changes try { // We could do toComplete += toApply but doing it like below // avoids unnecessary allocations since toApply is a mutable // list // toComplete += toApply toApply.fastForEach { composition -> toComplete.add(composition) } toApply.fastForEach { composition -> composition.applyChanges() } } catch (e: Throwable) { processCompositionError(e) clearRecompositionState() return@withFrameNanos } finally { toApply.clear() } } if (toLateApply.isNotEmpty()) { try { toComplete += toLateApply toLateApply.forEach { composition -> composition.applyLateChanges() } } catch (e: Throwable) { processCompositionError(e) clearRecompositionState() return@withFrameNanos } finally { toLateApply.clear() } } if (toComplete.isNotEmpty()) { try { toComplete.forEach { composition -> composition.changesApplied() } } catch (e: Throwable) { processCompositionError(e) clearRecompositionState() return@withFrameNanos } finally { toComplete.clear() } } } synchronized(stateLock) { runtimeCheck(deriveStateLocked() == null) { "unexpected to get continuation here" } } // Ensure any state objects that were written during apply changes, e.g. // nodes with state-backed properties, get sent apply notifications to // invalidate anything observing the nodes. Call this method instead of // sendApplyNotifications to ensure that objects that were _created_ in this // snapshot are also considered changed after this point. Snapshot.notifyObjectsInitialized() alreadyComposed.clear() modifiedValues.clear() compositionsRemoved = null } } discardUnusedMovableContentState() nextFrameEndCallbackQueue.markFrameComplete() } } private fun processCompositionError( e: Throwable, failedInitialComposition: ControlledComposition? = null, recoverable: Boolean = false, ) { if (_hotReloadEnabled.get() && e !is ComposeRuntimeError) { synchronized(stateLock) { logError("Error was captured in composition while live edit was enabled.", e) compositionsAwaitingApply.clear() compositionInvalidations.clear() snapshotInvalidations = MutableScatterSet() movableContentAwaitingInsert.clear() movableContentRemoved.clear() movableContentStatesAvailable.clear() errorState.value = RecomposerErrorState(isRecoverable = recoverable, cause = e) if (failedInitialComposition != null) { recordFailedCompositionLocked(failedInitialComposition) } if (deriveStateLocked() != null) { composeImmediateRuntimeError( "expected to go to inactive state due to composition error" ) } } } else { // withFrameNanos uses `runCatching` to ensure that crashes are not propagated to // AndroidUiDispatcher. This means that errors that happen during recomposition might // be delayed by a frame and swallowed if composed into inconsistent state caused by // the error. // Common case is subcomposition: if measure occurs after recomposition has thrown, // composeInitial will throw because of corrupted composition while original exception // won't be recorded. synchronized(stateLock) { logError("Error was captured in composition.", e) val errorState = errorState.value if (errorState == null) { // Record exception if current error state is empty. this.errorState.value = RecomposerErrorState(isRecoverable = false, cause = e) } else { // Re-throw original cause if we recorded it previously. throw errorState.cause } } throw e } } private inline fun withTransparentSnapshot(block: () -> Unit) { val currentSnapshot = Snapshot.current val snapshot = if (currentSnapshot is MutableSnapshot) { TransparentObserverMutableSnapshot( currentSnapshot, null, null, mergeParentObservers = true, ownsParentSnapshot = false, ) } else { TransparentObserverSnapshot( currentSnapshot, null, mergeParentObservers = true, ownsParentSnapshot = false, ) } try { snapshot.enter(block) } finally { snapshot.dispose() } } /** * Returns a cached copy of the list of known compositions that can be iterated safely without * holding the `stateLock`. */ private fun knownCompositions(): List { return synchronized(stateLock) { knownCompositionsLocked() } } private fun knownCompositionsLocked(): List { val cache = _knownCompositionsCache if (cache != null) return cache val compositions = _knownCompositions val newCache = if (compositions.isEmpty()) emptyList() else ArrayList(compositions) _knownCompositionsCache = newCache return newCache } @OptIn(ExperimentalComposeRuntimeApi::class) private fun clearKnownCompositionsLocked() { knownCompositionsLocked().fastForEach { composition -> unregisterCompositionLocked(composition) } _knownCompositions.clear() _knownCompositionsCache = emptyList() } private fun removeKnownCompositionLocked(composition: ControlledComposition) { if (_knownCompositions.remove(composition)) { _knownCompositionsCache = null unregisterCompositionLocked(composition) } } private fun addKnownCompositionLocked(composition: ControlledComposition) { _knownCompositions += composition _knownCompositionsCache = null } @OptIn(ExperimentalComposeRuntimeApi::class) private fun registerCompositionLocked(composition: ControlledComposition) { registrationObservers?.forEach { if (composition is ObservableComposition) { it.onCompositionRegistered(composition) } } } @OptIn(ExperimentalComposeRuntimeApi::class) private fun unregisterCompositionLocked(composition: ControlledComposition) { registrationObservers?.forEach { if (composition is ObservableComposition) { it.onCompositionUnregistered(composition) } } } @OptIn(ExperimentalComposeRuntimeApi::class) internal fun addCompositionRegistrationObserver( observer: CompositionRegistrationObserver ): CompositionObserverHandle { synchronized(stateLock) { val observers = registrationObservers ?: MutableObjectList().also { registrationObservers = it } observers += observer _knownCompositions.fastForEach { composition -> if (composition is ObservableComposition) { observer.onCompositionRegistered(composition) } } } return object : CompositionObserverHandle { override fun dispose() { synchronized(stateLock) { registrationObservers?.remove(observer) } } } } private fun resetErrorState(): RecomposerErrorState? { var error: RecomposerErrorState? = null synchronized(stateLock) { error = errorState.value if (error != null) { errorState.value = null deriveStateLocked() } else { null } } ?.resume(Unit) return error } private fun retryFailedCompositions() { val compositionsToRetry = synchronized(stateLock) { failedCompositions.also { failedCompositions = null } } ?: return try { while (compositionsToRetry.isNotEmpty()) { val composition = compositionsToRetry.removeLastKt() if (composition !is CompositionImpl) continue composition.invalidateAll() composition.setContent(composition.composable) if (errorState.value != null) break } } finally { if (compositionsToRetry.isNotEmpty()) { // If we did not complete the last list then add the remaining compositions back // into the failedCompositions list synchronized(stateLock) { compositionsToRetry.fastForEach { recordFailedCompositionLocked(it) } } } } } private fun recordFailedCompositionLocked(composition: ControlledComposition) { val failedCompositions = failedCompositions ?: mutableListOf().also { failedCompositions = it } if (composition !in failedCompositions) { failedCompositions += composition } removeKnownCompositionLocked(composition) } private val hasSchedulingWork: Boolean get() = synchronized(stateLock) { snapshotInvalidations.isNotEmpty() || compositionInvalidations.isNotEmpty() || hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked } private suspend fun awaitWorkAvailable() { if (!hasSchedulingWork) { // NOTE: Do not remove the `` from the next line even if the IDE reports it as // redundant. Removing this causes reports it cannot infer the type. (KT-79553) @Suppress("RemoveExplicitTypeArguments") // See note above suspendCancellableCoroutine { co -> synchronized(stateLock) { if (hasSchedulingWork) { co } else { workContinuation = co null } } ?.resume(Unit) } } } @OptIn(ExperimentalComposeApi::class) private suspend fun recompositionRunner( block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit ) { val parentFrameClock = coroutineContext.monotonicFrameClock withContext(broadcastFrameClock) { // Enforce mutual exclusion of callers; register self as current runner val callingJob = coroutineContext.job registerRunnerJob(callingJob) // Observe snapshot changes and propagate them to known composers only from // this caller's dispatcher, never working with the same composer in parallel. // unregisterApplyObserver is called as part of the big finally below val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ -> synchronized(stateLock) { if (_state.value >= State.Idle) { val snapshotInvalidations = snapshotInvalidations changed.fastForEach { if ( it is StateObjectImpl && !it.isReadIn(ReaderKind.Composition) ) { // continue if we know that state is never read in // composition return@fastForEach } snapshotInvalidations.add(it) } deriveStateLocked() } else null } ?.resume(Unit) } addRunning(recomposerInfo) try { // Invalidate all registered composers when we start since we weren't observing // snapshot changes on their behalf. Assume anything could have changed. knownCompositions().fastForEach { it.invalidateAll() } coroutineScope { block(parentFrameClock) } } finally { unregisterApplyObserver.dispose() synchronized(stateLock) { if (runnerJob === callingJob) { runnerJob = null } if (deriveStateLocked() != null) { composeImmediateRuntimeError( "called outside of runRecomposeAndApplyChanges" ) } } removeRunning(recomposerInfo) } } } /** * Permanently shut down this [Recomposer] for future use. [currentState] will immediately * reflect [State.ShuttingDown] (or a lower state) before this call returns. All ongoing * recompositions will stop, new composer invalidations with this [Recomposer] at the root will * no longer occur, and any [LaunchedEffect]s currently running in compositions managed by this * [Recomposer] will be cancelled. Any [rememberCoroutineScope] scopes from compositions managed * by this [Recomposer] will also be cancelled. See [join] to await the completion of all of * these outstanding tasks. */ public fun cancel() { // Move to State.ShuttingDown immediately rather than waiting for effectJob to join // if we're cancelling to shut down the Recomposer. This permits other client code // to use `state.first { it < State.Idle }` or similar to reliably and immediately detect // that the recomposer can no longer be used. // It looks like a CAS loop would be more appropriate here, but other occurrences // of taking stateLock assume that the state cannot change without holding it. synchronized(stateLock) { if (_state.value >= State.Idle) { _state.value = State.ShuttingDown } } effectJob.cancel() } /** * Close this [Recomposer]. Once all effects launched by managed compositions complete, any * active call to [runRecomposeAndApplyChanges] will return normally and this [Recomposer] will * be [State.ShutDown]. See [join] to await the completion of all of these outstanding tasks. */ public fun close() { if (effectJob.complete()) { synchronized(stateLock) { isClosed = true } } } /** Await the completion of a [cancel] operation. */ public suspend fun join() { currentState.first { it == State.ShutDown } } /** * Schedules an [action] to be invoked when the recomposer finishes the next composition of a * frame (including the completion of subcompositions). If a frame is currently in-progress, * [action] will be invoked when the current frame fully finishes composing. If a frame isn't * currently in-progress, a new frame will be scheduled (if one hasn't been already) and * [action] will execute at the completion of the next frame's composition. If a new frame is * scheduled and there is no other work to execute, [action] will still execute. * * [action] will always execute on the applier thread. * * @return A [CancellationHandle] that can be used to unregister the [action]. The returned * handle is thread-safe and may be cancelled from any thread. Cancelling the handle only * removes the callback from the queue. If [action] is currently executing, it will not be * cancelled by this handle. */ public override fun scheduleFrameEndCallback(action: () -> Unit): CancellationHandle { return nextFrameEndCallbackQueue.scheduleFrameEndCallback(action) } internal override fun composeInitial( composition: ControlledComposition, content: @Composable () -> Unit, ) { val composerWasComposing = composition.isComposing val newComposition = synchronized(stateLock) { if (_state.value > State.ShuttingDown) { val new = composition !in knownCompositionsLocked() if (new) { registerCompositionLocked(composition) } new } else { true } } try { composing(composition, null) { composition.composeContent(content) } } catch (e: Throwable) { if (newComposition) { synchronized(stateLock) { unregisterCompositionLocked(composition) } } processCompositionError(e, composition, recoverable = true) return } synchronized(stateLock) { if (_state.value > State.ShuttingDown) { if (composition !in knownCompositionsLocked()) { addKnownCompositionLocked(composition) } } else { unregisterCompositionLocked(composition) } } // TODO(b/143755743) if (!composerWasComposing) { Snapshot.notifyObjectsInitialized() } try { performInitialMovableContentInserts(composition) } catch (e: Throwable) { processCompositionError(e, composition, recoverable = true) return } try { composition.applyChanges() composition.applyLateChanges() } catch (e: Throwable) { processCompositionError(e) return } if (!composerWasComposing) { // Ensure that any state objects created during applyChanges are seen as changed // if modified after this call. Snapshot.notifyObjectsInitialized() } } internal override fun composeInitialPaused( composition: ControlledComposition, shouldPause: ShouldPauseCallback, content: @Composable () -> Unit, ): ScatterSet { return try { composition.pausable(shouldPause) { composeInitial(composition, content) pausedScopes.get() ?: emptyScatterSet() } } finally { pausedScopes.set(null) } } internal override fun recomposePaused( composition: ControlledComposition, shouldPause: ShouldPauseCallback, invalidScopes: ScatterSet, ): ScatterSet { return try { recordComposerModifications() composition.recordModificationsOf(invalidScopes.wrapIntoSet()) composition.pausable(shouldPause) { val needsApply = performRecompose(composition, null) if (needsApply != null) { performInitialMovableContentInserts(composition) needsApply.applyChanges() needsApply.applyLateChanges() } pausedScopes.get() ?: emptyScatterSet() } } finally { pausedScopes.set(null) } } override fun reportPausedScope(scope: RecomposeScopeImpl) { val scopes = pausedScopes.get() ?: run { val newScopes = mutableScatterSetOf() pausedScopes.set(newScopes) newScopes } scopes.add(scope) } private fun performInitialMovableContentInserts(composition: ControlledComposition) { synchronized(stateLock) { if (!movableContentAwaitingInsert.fastAny { it.composition == composition }) return } val toInsert = mutableListOf() fun fillToInsert() { toInsert.clear() synchronized(stateLock) { val iterator = movableContentAwaitingInsert.iterator() while (iterator.hasNext()) { val value = iterator.next() if (value.composition == composition) { toInsert.add(value) iterator.remove() } } } } fillToInsert() while (toInsert.isNotEmpty()) { performInsertValues(toInsert, null) fillToInsert() } } private fun performRecompose( composition: ControlledComposition, modifiedValues: MutableScatterSet?, ): ControlledComposition? { if ( composition.isComposing || composition.isDisposed || compositionsRemoved?.contains(composition) == true ) return null return if ( composing(composition, modifiedValues) { if (modifiedValues?.isNotEmpty() == true) { // Record write performed by a previous composition as if they happened during // composition. composition.prepareCompose { modifiedValues.forEach { composition.recordWriteOf(it) } } } composition.recompose() } ) composition else null } @OptIn(ExperimentalComposeApi::class) private fun performInsertValues( references: List, modifiedValues: MutableScatterSet?, ): List { val tasks = references.fastGroupBy { it.composition } for ((composition, refs) in tasks) { runtimeCheck(!composition.isComposing) composing(composition, modifiedValues) { // Map insert movable content to movable content states that have been released // during `performRecompose`. val pairs = synchronized(stateLock) { refs .fastMap { reference -> reference to movableContentRemoved.removeLast(reference.content).also { if (it != null) { movableContentNestedStatesAvailable.usedContainer(it) } } } .let { pairs -> // Check for any nested states if ( pairs.fastAny { it.second == null && it.first.content in movableContentNestedStatesAvailable } ) { // We have at least one nested state we could use, if a state // is available for the container then schedule the state to be // removed from the container when it is released. pairs.fastMap { pair -> if (pair.second == null) { val nestedContentReference = movableContentNestedStatesAvailable.removeLast( pair.first.content ) if (nestedContentReference == null) return@fastMap pair val content = nestedContentReference.content val container = nestedContentReference.container movableContentNestedExtractionsPending.add( container, content, ) pair.first to content } else pair } } else pairs } } // Avoid mixing creating new content with moving content as the moved content // may release content when it is moved as it is recomposed when move. val toInsert = if ( pairs.fastAll { it.second == null } || pairs.fastAll { it.second != null } ) { pairs } else { // Return the content not moving to the awaiting list. These will come back // here in the next iteration of the caller's loop and either have content // to move or by still needing to create the content. val toReturn = pairs.fastMapNotNull { item -> if (item.second == null) item.first else null } synchronized(stateLock) { movableContentAwaitingInsert += toReturn } // Only insert the moving content this time pairs.fastFilterIndexed { _, item -> item.second != null } } // toInsert is guaranteed to be not empty as, // 1) refs is guaranteed to be not empty as a condition of groupBy // 2) pairs is guaranteed to be not empty as it is a map of refs // 3) toInsert is guaranteed to not be empty because the toReturn and toInsert // lists have at least one item by the condition of the guard in the if // expression. If one would be empty the condition is true and the filter is not // performed. As both have at least one item toInsert has at least one item. If // the filter is not performed the list is pairs which has at least one item. composition.insertMovableContent(toInsert) } } return tasks.keys.toList() } private fun discardUnusedMovableContentState() { val unusedValues = synchronized(stateLock) { if (movableContentRemoved.isNotEmpty()) { val references = movableContentRemoved.values() movableContentRemoved.clear() movableContentNestedStatesAvailable.clear() movableContentNestedExtractionsPending.clear() val unusedValues = references.fastMap { it to movableContentStatesAvailable[it] } movableContentStatesAvailable.clear() unusedValues } else emptyObjectList() } unusedValues.forEach { (reference, state) -> if (state != null) { reference.composition.disposeUnusedMovableContent(state) } } } private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit { return { value -> composition.recordReadOf(value) } } private fun writeObserverOf( composition: ControlledComposition, modifiedValues: MutableScatterSet?, ): (Any) -> Unit { return { value -> composition.recordWriteOf(value) modifiedValues?.add(value) } } private inline fun composing( composition: ControlledComposition, modifiedValues: MutableScatterSet?, block: () -> T, ): T { val snapshot = Snapshot.takeMutableSnapshot( readObserverOf(composition), writeObserverOf(composition, modifiedValues), ) try { return snapshot.enter(block) } finally { applyAndCheck(snapshot) } } private fun applyAndCheck(snapshot: MutableSnapshot) { try { val applyResult = snapshot.apply() if (applyResult is SnapshotApplyResult.Failure) { error( "Unsupported concurrent change during composition. A state object was " + "modified by composition as well as being modified outside composition." ) } } finally { snapshot.dispose() } } /** * `true` if this [Recomposer] has any pending work scheduled, regardless of whether or not it * is currently [running][runRecomposeAndApplyChanges]. */ public val hasPendingWork: Boolean get() = synchronized(stateLock) { snapshotInvalidations.isNotEmpty() || compositionInvalidations.isNotEmpty() || concurrentCompositionsOutstanding > 0 || compositionsAwaitingApply.isNotEmpty() || hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked || movableContentRemoved.isNotEmpty() } private val hasFrameWorkLocked: Boolean get() = compositionInvalidations.isNotEmpty() || hasBroadcastFrameClockAwaitersLocked || hasNextFrameEndAwaitersLocked || movableContentRemoved.isNotEmpty() /** * Suspends until the currently pending recomposition frame is complete. Any recomposition for * this recomposer triggered by actions before this call begins will be complete and applied (if * recomposition was successful) when this call returns. * * If [runRecomposeAndApplyChanges] is not currently running the [Recomposer] is considered idle * and this method will not suspend. */ public suspend fun awaitIdle() { currentState.takeWhile { it > State.Idle }.collect() } /** * Pause broadcasting the frame clock while recomposing. This effectively pauses animations, or * any other use of the [withFrameNanos], while the frame clock is paused. * * [pauseCompositionFrameClock] should be called when the recomposer is not being displayed for * some reason such as not being the current activity in Android, for example. * * Calls to [pauseCompositionFrameClock] are thread-safe and idempotent (calling it when the * frame clock is already paused is a no-op). */ public fun pauseCompositionFrameClock() { synchronized(stateLock) { frameClockPaused = true } } /** * Resume broadcasting the frame clock after is has been paused. Pending calls to * [withFrameNanos] will start receiving frame clock broadcasts at the beginning of the frame * and a frame will be requested if there are pending calls to [withFrameNanos] if a frame has * not already been scheduled. * * Calls to [resumeCompositionFrameClock] are thread-safe and idempotent (calling it when the * frame clock is running is a no-op). */ public fun resumeCompositionFrameClock() { synchronized(stateLock) { if (frameClockPaused) { frameClockPaused = false deriveStateLocked() } else null } ?.resume(Unit) } // Recomposer always starts with a constant compound hash internal override val compositeKeyHashCode: CompositeKeyHashCode get() = RecomposerCompoundHashKey internal override val collectingCallByInformation: Boolean get() = _hotReloadEnabled.get() // Collecting parameter happens at the level of a composer; starts as false internal override val collectingParameterInformation: Boolean get() = false internal override val collectingSourceInformation: Boolean get() = composeStackTraceMode == ComposeStackTraceMode.SourceInformation internal override val stackTraceEnabled: Boolean get() = composeStackTraceMode != ComposeStackTraceMode.None internal override fun recordInspectionTable(table: MutableSet) { // TODO: The root recomposer might be a better place to set up inspection // than the current configuration with an CompositionLocal } internal override fun registerComposition(composition: ControlledComposition) { // Do nothing. } internal override fun unregisterComposition(composition: ControlledComposition) { synchronized(stateLock) { removeKnownCompositionLocked(composition) compositionInvalidations -= composition compositionsAwaitingApply -= composition } } internal override fun invalidate(composition: ControlledComposition) { synchronized(stateLock) { if (composition !in compositionInvalidations) { compositionInvalidations += composition deriveStateLocked() } else null } ?.resume(Unit) } internal override fun invalidateScope(scope: RecomposeScopeImpl) { synchronized(stateLock) { snapshotInvalidations.add(scope) deriveStateLocked() } ?.resume(Unit) } internal override fun insertMovableContent(reference: MovableContentStateReference) { synchronized(stateLock) { movableContentAwaitingInsert += reference deriveStateLocked() } ?.resume(Unit) } internal override fun deletedMovableContent(reference: MovableContentStateReference) { synchronized(stateLock) { movableContentRemoved.add(reference.content, reference) if (reference.nestedReferences != null) { val container = reference fun recordNestedStatesOf(reference: MovableContentStateReference) { reference.nestedReferences?.fastForEach { nestedReference -> movableContentNestedStatesAvailable.add( nestedReference.content, NestedMovableContent(nestedReference, container), ) recordNestedStatesOf(nestedReference) } } recordNestedStatesOf(reference) } deriveStateLocked() } ?.resume(Unit) } internal override fun movableContentStateReleased( reference: MovableContentStateReference, data: MovableContentState, applier: Applier<*>, ) { synchronized(stateLock) { movableContentStatesAvailable[reference] = data val extractions = movableContentNestedExtractionsPending[reference] if (extractions.isNotEmpty()) { val states = data.slotStorage.extractNestedStates(applier, extractions) states.forEach { reference, state -> movableContentStatesAvailable[reference] = state } } } } internal override fun reportRemovedComposition(composition: ControlledComposition) { synchronized(stateLock) { val compositionsRemoved = compositionsRemoved ?: mutableScatterSetOf().also { compositionsRemoved = it } compositionsRemoved.add(composition) } } override fun movableContentStateResolve( reference: MovableContentStateReference ): MovableContentState? = synchronized(stateLock) { movableContentStatesAvailable.remove(reference) } override val composition: Composition? get() = null /** * hack: the companion object is thread local in Kotlin/Native to avoid freezing * [_runningRecomposers] with the current memory model. As a side effect, recomposers are now * forced to be single threaded in Kotlin/Native targets. * * This annotation WILL BE REMOVED with the new memory model of Kotlin/Native. */ @ThreadLocal public companion object { private val _runningRecomposers = MutableStateFlow(persistentSetOf()) private val _hotReloadEnabled = AtomicReference(false) /** * An observable [Set] of [RecomposerInfo]s for currently * [running][runRecomposeAndApplyChanges] [Recomposer]s. Emitted sets are immutable. */ public val runningRecomposers: StateFlow> get() = _runningRecomposers internal fun setHotReloadEnabled(value: Boolean) { _hotReloadEnabled.set(value) } private fun addRunning(info: RecomposerInfoImpl) { while (true) { val old = _runningRecomposers.value val new = old.add(info) if (old === new || _runningRecomposers.compareAndSet(old, new)) break } } private fun removeRunning(info: RecomposerInfoImpl) { while (true) { val old = _runningRecomposers.value val new = old.remove(info) if (old === new || _runningRecomposers.compareAndSet(old, new)) break } } internal fun saveStateAndDisposeForHotReload(): Any { // NOTE: when we move composition/recomposition onto multiple threads, we will want // to ensure that we pause recompositions before this call. _hotReloadEnabled.set(true) return _runningRecomposers.value.flatMap { it.saveStateAndDisposeForHotReload() } } internal fun loadStateAndComposeForHotReload(token: Any) { // NOTE: when we move composition/recomposition onto multiple threads, we will want // to ensure that we pause recompositions before this call. _hotReloadEnabled.set(true) _runningRecomposers.value.forEach { it.resetErrorState() } @Suppress("UNCHECKED_CAST") val holders = token as List holders.fastForEach { it.resetContent() } holders.fastForEach { it.recompose() } _runningRecomposers.value.forEach { it.retryFailedCompositions() } } @OptIn(ComposeToolingApi::class) internal fun invalidateGroupsWithKey(key: Int) { _hotReloadEnabled.set(true) _runningRecomposers.value.forEach { if (it.currentError?.isRecoverable == false) { return@forEach } it.resetErrorState() it.invalidateGroupsWithKey(key) it.retryFailedCompositions() } } /** This is an internal API only kept for backward compatibility. */ @OptIn(ComposeToolingApi::class) internal fun getCurrentErrors(): List = _runningRecomposers.value.mapNotNull { it.currentError as? RecomposerErrorInfo } @OptIn(ComposeToolingApi::class) internal fun getRecomposerErrors(): List = _runningRecomposers.value.mapNotNull { it.currentError } internal fun clearErrors() { _runningRecomposers.value.mapNotNull { it.resetErrorState() } } } } /** Sentinel used by [ProduceFrameSignal] */ private val ProduceAnotherFrame = Any() private val FramePending = Any() /** * Multiple producer, single consumer conflated signal that tells concurrent composition when it * should try to produce another frame. This class is intended to be used along with a lock shared * between producers and consumer. */ private class ProduceFrameSignal { private var pendingFrameContinuation: Any? = null /** * Suspend until a frame is requested. After this method returns the signal is in a * [FramePending] state which must be acknowledged by a call to [takeFrameRequestLocked] once * all data that will be used to produce the frame has been claimed. */ suspend fun awaitFrameRequest(lock: SynchronizedObject) { synchronized(lock) { if (pendingFrameContinuation === ProduceAnotherFrame) { pendingFrameContinuation = FramePending return } } suspendCancellableCoroutine { co -> synchronized(lock) { if (pendingFrameContinuation === ProduceAnotherFrame) { pendingFrameContinuation = FramePending co } else { pendingFrameContinuation = co null } } ?.resume(Unit) } } /** * Signal from the frame request consumer that the frame is beginning with data that was * available up until this point. (Synchronizing access to that data is up to the caller.) */ fun takeFrameRequestLocked() { checkPrecondition(pendingFrameContinuation === FramePending) { "frame not pending" } pendingFrameContinuation = null } fun requestFrameLocked(): Continuation? = when (val co = pendingFrameContinuation) { is Continuation<*> -> { pendingFrameContinuation = FramePending @Suppress("UNCHECKED_CAST") co as Continuation } ProduceAnotherFrame, FramePending -> null null -> { pendingFrameContinuation = ProduceAnotherFrame null } else -> error("invalid pendingFrameContinuation $co") } } @OptIn(InternalComposeApi::class) private class NestedContentMap { private val contentMap = MultiValueMap, NestedMovableContent>() private val containerMap = MultiValueMap>() fun add(content: MovableContent, nestedContent: NestedMovableContent) { contentMap.add(content, nestedContent) containerMap.add(nestedContent.container, content) } fun clear() { contentMap.clear() containerMap.clear() } fun removeLast(key: MovableContent) = contentMap.removeLast(key).also { if (contentMap.isEmpty()) containerMap.clear() } operator fun contains(key: MovableContent) = key in contentMap fun usedContainer(reference: MovableContentStateReference) { containerMap.forEachValue(reference) { value -> contentMap.removeValueIf(value) { it.container == reference } } } } @InternalComposeApi private class NestedMovableContent( val content: MovableContentStateReference, val container: MovableContentStateReference, ) ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/SnapshotState.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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. */ @file:JvmName("SnapshotStateKt") @file:JvmMultifileClass package androidx.compose.runtime import androidx.compose.runtime.snapshots.GlobalSnapshot import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotId import androidx.compose.runtime.snapshots.SnapshotMutableState import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateSet import androidx.compose.runtime.snapshots.StateFactoryMarker import androidx.compose.runtime.snapshots.StateObjectImpl import androidx.compose.runtime.snapshots.StateRecord import androidx.compose.runtime.snapshots.currentSnapshot import androidx.compose.runtime.snapshots.overwritable import androidx.compose.runtime.snapshots.readable import androidx.compose.runtime.snapshots.toSnapshotId import androidx.compose.runtime.snapshots.withCurrent import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.reflect.KProperty /** * Return a new [MutableState] initialized with the passed in [value] * * The MutableState class is a single value holder whose reads and writes are observed by Compose. * Additionally, writes to it are transacted as part of the [Snapshot] system. * * @param value the initial value for the [MutableState] * @param policy a policy to controls how changes are handled in mutable snapshots. * @sample androidx.compose.runtime.samples.SimpleStateSample * @sample androidx.compose.runtime.samples.DestructuredStateSample * @sample androidx.compose.runtime.samples.observeUserSample * @sample androidx.compose.runtime.samples.stateSample * @see State * @see MutableState * @see SnapshotMutationPolicy * @see mutableIntStateOf * @see mutableLongStateOf * @see mutableFloatStateOf * @see mutableDoubleStateOf */ @StateFactoryMarker public fun mutableStateOf( value: T, policy: SnapshotMutationPolicy = structuralEqualityPolicy(), ): MutableState = createSnapshotMutableState(value, policy) /** * A value holder where reads to the [value] property during the execution of a [Composable] * function, the current [RecomposeScope] will be subscribed to changes of that value. * * @see [MutableState] * @see [mutableStateOf] */ @Stable public interface State { public val value: T } /** * Permits property delegation of `val`s using `by` for [State]. * * @sample androidx.compose.runtime.samples.DelegatedReadOnlyStateSample */ @Suppress("NOTHING_TO_INLINE") public inline operator fun State.getValue(thisObj: Any?, property: KProperty<*>): T = value /** * A mutable value holder where reads to the [value] property during the execution of a [Composable] * function, the current [RecomposeScope] will be subscribed to changes of that value. When the * [value] property is written to and changed, a recomposition of any subscribed [RecomposeScope]s * will be scheduled. If [value] is written to with the same value, no recompositions will be * scheduled. * * @see [State] * @see [mutableStateOf] */ @Stable public interface MutableState : State { override var value: T public operator fun component1(): T public operator fun component2(): (T) -> Unit } /** * Permits property delegation of `var`s using `by` for [MutableState]. * * @sample androidx.compose.runtime.samples.DelegatedStateSample */ @Suppress("NOTHING_TO_INLINE") public inline operator fun MutableState.setValue( thisObj: Any?, property: KProperty<*>, value: T, ) { this.value = value } /** Returns platform specific implementation based on [SnapshotMutableStateImpl]. */ internal expect fun createSnapshotMutableState( value: T, policy: SnapshotMutationPolicy, ): SnapshotMutableState /** * A single value holder whose reads and writes are observed by Compose. * * Additionally, writes to it are transacted as part of the [Snapshot] system. * * @param value the wrapped value * @param policy a policy to control how changes are handled in a mutable snapshot. * @see mutableStateOf * @see SnapshotMutationPolicy */ internal open class SnapshotMutableStateImpl( value: T, override val policy: SnapshotMutationPolicy, ) : StateObjectImpl(), SnapshotMutableState { @Suppress("UNCHECKED_CAST") override var value: T get() = next.readable(this).value set(value) = next.withCurrent { if (!policy.equivalent(it.value, value)) { next.overwritable(this, it) { this.value = value } } } private var next: StateStateRecord = currentSnapshot().let { snapshot -> StateStateRecord(snapshot.snapshotId, value).also { if (snapshot !is GlobalSnapshot) { it.next = StateStateRecord(Snapshot.PreexistingSnapshotId.toSnapshotId(), value) } } } override val firstStateRecord: StateRecord get() = next override fun prependStateRecord(value: StateRecord) { @Suppress("UNCHECKED_CAST") next = value as StateStateRecord } @Suppress("UNCHECKED_CAST") override fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord, ): StateRecord? { val previousRecord = previous as StateStateRecord val currentRecord = current as StateStateRecord val appliedRecord = applied as StateStateRecord return if (policy.equivalent(currentRecord.value, appliedRecord.value)) current else { val merged = policy.merge(previousRecord.value, currentRecord.value, appliedRecord.value) if (merged != null) { appliedRecord.create(appliedRecord.snapshotId).also { it.value = merged } } else { null } } } override fun toString(): String = next.withCurrent { "MutableState(value=${it.value})@${hashCode()}" } private class StateStateRecord(snapshotId: SnapshotId, myValue: T) : StateRecord(snapshotId) { override fun assign(value: StateRecord) { @Suppress("UNCHECKED_CAST") this.value = (value as StateStateRecord).value } override fun create() = StateStateRecord(currentSnapshot().snapshotId, value) override fun create(snapshotId: SnapshotId) = StateStateRecord(currentSnapshot().snapshotId, value) var value: T = myValue } /** * The componentN() operators allow state objects to be used with the property destructuring * syntax * * ``` * var (foo, setFoo) = remember { mutableStateOf(0) } * setFoo(123) // set * foo == 123 // get * ``` */ override operator fun component1(): T = value override operator fun component2(): (T) -> Unit = { value = it } /** * A function used by the debugger to display the value of the current value of the mutable * state object without triggering read observers. */ @Suppress("unused") val debuggerDisplayValue: T @JvmName("getDebuggerDisplayValue") get() = next.withCurrent { it }.value } /** * Create a instance of [MutableList] that is observable and can be snapshot. * * @sample androidx.compose.runtime.samples.stateListSample * @see mutableStateOf * @see mutableListOf * @see MutableList * @see Snapshot.takeSnapshot */ @StateFactoryMarker public fun mutableStateListOf(): SnapshotStateList = SnapshotStateList() /** * Create an instance of [MutableList] that is observable and can be snapshot. * * @see mutableStateOf * @see mutableListOf * @see MutableList * @see Snapshot.takeSnapshot */ @StateFactoryMarker public fun mutableStateListOf(vararg elements: T): SnapshotStateList = SnapshotStateList().also { it.addAll(elements.toList()) } /** * Create an instance of [MutableList] from a collection that is observable and can be snapshot. */ public fun Collection.toMutableStateList(): SnapshotStateList = SnapshotStateList().also { it.addAll(this) } /** * Create a instance of [MutableMap] that is observable and can be snapshot. * * @sample androidx.compose.runtime.samples.stateMapSample * @see mutableStateOf * @see mutableMapOf * @see MutableMap * @see Snapshot.takeSnapshot */ @StateFactoryMarker public fun mutableStateMapOf(): SnapshotStateMap = SnapshotStateMap() /** * Create a instance of [MutableMap] that is observable and can be snapshot. * * @see mutableStateOf * @see mutableMapOf * @see MutableMap * @see Snapshot.takeSnapshot */ @StateFactoryMarker public fun mutableStateMapOf(vararg pairs: Pair): SnapshotStateMap = SnapshotStateMap().apply { putAll(pairs.toMap()) } /** * Create an instance of [MutableMap] from a collection of pairs that is observable and can be * snapshot. */ @Suppress("unused") public fun Iterable>.toMutableStateMap(): SnapshotStateMap = SnapshotStateMap().also { it.putAll(this.toMap()) } /** * Create a instance of [MutableSet] that is observable and can be snapshot. * * The returned set iteration order is in the order the items were inserted into the set. * * @sample androidx.compose.runtime.samples.stateSetSample * @see mutableStateOf * @see mutableSetOf * @see MutableSet * @see Snapshot.takeSnapshot */ @StateFactoryMarker public fun mutableStateSetOf(): SnapshotStateSet = SnapshotStateSet() /** * Create an instance of [MutableSet] that is observable and can be snapshot. * * The returned set iteration order is in the order the items were inserted into the set. * * @see mutableStateOf * @see mutableSetOf * @see MutableSet * @see Snapshot.takeSnapshot */ @StateFactoryMarker public fun mutableStateSetOf(vararg elements: T): SnapshotStateSet = SnapshotStateSet().also { it.addAll(elements.toSet()) } /** * [remember] a [mutableStateOf] [newValue] and update its value to [newValue] on each recomposition * of the [rememberUpdatedState] call. * * [rememberUpdatedState] should be used when parameters or values computed during composition are * referenced by a long-lived lambda or object expression. Recomposition will update the resulting * [State] without recreating the long-lived lambda or object, allowing that object to persist * without cancelling and resubscribing, or relaunching a long-lived operation that may be expensive * or prohibitive to recreate and restart. This may be common when working with [DisposableEffect] * or [LaunchedEffect], for example: * * @sample androidx.compose.runtime.samples.rememberUpdatedStateSampleWithDisposableEffect * * [LaunchedEffect]s often describe state machines that should not be reset and restarted if a * parameter or event callback changes, but they should have the current value available when * needed. For example: * * @sample androidx.compose.runtime.samples.rememberUpdatedStateSampleWithLaunchedEffect * * By using [rememberUpdatedState] a composable function can update these operations in progress. */ @Composable public fun rememberUpdatedState(newValue: T): State = remember { mutableStateOf(newValue) }.apply { value = newValue } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Effects.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.runtime import androidx.compose.runtime.internal.PlatformOptimizedCancellationException import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import androidx.compose.runtime.tooling.ComposeToolingApi import androidx.compose.runtime.tooling.ComposeToolingFlags import androidx.compose.runtime.tooling.CompositionErrorContextImpl import kotlin.concurrent.Volatile import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.jvm.JvmField import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.launch /** * Schedule [effect] to run when the current composition completes successfully and applies changes. * [SideEffect] can be used to apply side effects to objects managed by the composition that are not * backed by [snapshots][androidx.compose.runtime.snapshots.Snapshot] so as not to leave those * objects in an inconsistent state if the current composition operation fails. * * [effect] will always be run on the composition's apply dispatcher and appliers are never run * concurrent with themselves, one another, applying changes to the composition tree, or running * [RememberObserver] event callbacks. [SideEffect]s are always run after [RememberObserver] event * callbacks. * * A [SideEffect] runs after **every** recomposition. To launch an ongoing task spanning potentially * many recompositions, see [LaunchedEffect]. To manage an event subscription or other object * lifecycle, see [DisposableEffect]. */ @Composable @NonRestartableComposable @ExplicitGroupsComposable @OptIn(InternalComposeApi::class) public fun SideEffect(effect: () -> Unit) { currentComposer.recordSideEffect(effect) } /** * Receiver scope for [DisposableEffect] that offers the [onDispose] clause that should be the last * statement in any call to [DisposableEffect]. */ public class DisposableEffectScope { /** * Provide [onDisposeEffect] to the [DisposableEffect] to run when it leaves the composition or * its key changes. */ public inline fun onDispose(crossinline onDisposeEffect: () -> Unit): DisposableEffectResult = object : DisposableEffectResult { override fun dispose() { onDisposeEffect() } } } public interface DisposableEffectResult { public fun dispose() } private val InternalDisposableEffectScope = DisposableEffectScope() private class DisposableEffectImpl( private val effect: DisposableEffectScope.() -> DisposableEffectResult ) : RememberObserver { private var onDispose: DisposableEffectResult? = null override fun onRemembered() { onDispose = InternalDisposableEffectScope.effect() } override fun onForgotten() { onDispose?.dispose() onDispose = null } override fun onAbandoned() { // Nothing to do as [onRemembered] was not called. } } private const val DisposableEffectNoParamError = "DisposableEffect must provide one or more 'key' parameters that define the identity of " + "the DisposableEffect and determine when its previous effect should be disposed and " + "a new effect started for the new key." private const val LaunchedEffectNoParamError = "LaunchedEffect must provide one or more 'key' parameters that define the identity of " + "the LaunchedEffect and determine when its previous effect coroutine should be cancelled " + "and a new effect launched for the new key." /** * A side effect of composition that must be reversed or cleaned up if the [DisposableEffect] leaves * the composition. * * It is an error to call [DisposableEffect] without at least one `key` parameter. */ // This deprecated-error function shadows the varargs overload so that the varargs version // is not used without key parameters. @Composable @NonRestartableComposable @Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER") @Deprecated(DisposableEffectNoParamError, level = DeprecationLevel.ERROR) public fun DisposableEffect(effect: DisposableEffectScope.() -> DisposableEffectResult): Unit = error(DisposableEffectNoParamError) /** * A side effect of composition that must run for any new unique value of [key1] and must be * reversed or cleaned up if [key1] changes or if the [DisposableEffect] leaves the composition. * * A [DisposableEffect]'s _key_ is a value that defines the identity of the [DisposableEffect]. If a * key changes, the [DisposableEffect] must [dispose][DisposableEffectScope.onDispose] its current * [effect] and reset by calling [effect] again. Examples of keys include: * * Observable objects that the effect subscribes to * * Unique request parameters to an operation that must cancel and retry if those parameters change * * [DisposableEffect] may be used to initialize or subscribe to a key and reinitialize when a * different key is provided, performing cleanup for the old operation before initializing the new. * For example: * * @sample androidx.compose.runtime.samples.disposableEffectSample * * A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause as * the final statement in its [effect] block. If your operation does not require disposal it might * be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should be * managed by the composition. * * There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call * to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run on * the composition's apply dispatcher and appliers are never run concurrent with themselves, one * another, applying changes to the composition tree, or running [RememberObserver] event callbacks. */ @Composable @NonRestartableComposable public fun DisposableEffect( key1: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult, ) { remember(key1) { DisposableEffectImpl(effect) } } /** * A side effect of composition that must run for any new unique value of [key1] or [key2] and must * be reversed or cleaned up if [key1] or [key2] changes, or if the [DisposableEffect] leaves the * composition. * * A [DisposableEffect]'s _key_ is a value that defines the identity of the [DisposableEffect]. If a * key changes, the [DisposableEffect] must [dispose][DisposableEffectScope.onDispose] its current * [effect] and reset by calling [effect] again. Examples of keys include: * * Observable objects that the effect subscribes to * * Unique request parameters to an operation that must cancel and retry if those parameters change * * [DisposableEffect] may be used to initialize or subscribe to a key and reinitialize when a * different key is provided, performing cleanup for the old operation before initializing the new. * For example: * * @sample androidx.compose.runtime.samples.disposableEffectSample * * A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause as * the final statement in its [effect] block. If your operation does not require disposal it might * be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should be * managed by the composition. * * There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call * to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run on * the composition's apply dispatcher and appliers are never run concurrent with themselves, one * another, applying changes to the composition tree, or running [RememberObserver] event callbacks. */ @Composable @NonRestartableComposable public fun DisposableEffect( key1: Any?, key2: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult, ) { remember(key1, key2) { DisposableEffectImpl(effect) } } /** * A side effect of composition that must run for any new unique value of [key1], [key2] or [key3] * and must be reversed or cleaned up if [key1], [key2] or [key3] changes, or if the * [DisposableEffect] leaves the composition. * * A [DisposableEffect]'s _key_ is a value that defines the identity of the [DisposableEffect]. If a * key changes, the [DisposableEffect] must [dispose][DisposableEffectScope.onDispose] its current * [effect] and reset by calling [effect] again. Examples of keys include: * * Observable objects that the effect subscribes to * * Unique request parameters to an operation that must cancel and retry if those parameters change * * [DisposableEffect] may be used to initialize or subscribe to a key and reinitialize when a * different key is provided, performing cleanup for the old operation before initializing the new. * For example: * * @sample androidx.compose.runtime.samples.disposableEffectSample * * A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause as * the final statement in its [effect] block. If your operation does not require disposal it might * be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should be * managed by the composition. * * There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call * to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run on * the composition's apply dispatcher and appliers are never run concurrent with themselves, one * another, applying changes to the composition tree, or running [RememberObserver] event callbacks. */ @Composable @NonRestartableComposable public fun DisposableEffect( key1: Any?, key2: Any?, key3: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult, ) { remember(key1, key2, key3) { DisposableEffectImpl(effect) } } /** * A side effect of composition that must run for any new unique value of [keys] and must be * reversed or cleaned up if any [keys] change or if the [DisposableEffect] leaves the composition. * * A [DisposableEffect]'s _key_ is a value that defines the identity of the [DisposableEffect]. If a * key changes, the [DisposableEffect] must [dispose][DisposableEffectScope.onDispose] its current * [effect] and reset by calling [effect] again. Examples of keys include: * * Observable objects that the effect subscribes to * * Unique request parameters to an operation that must cancel and retry if those parameters change * * [DisposableEffect] may be used to initialize or subscribe to a key and reinitialize when a * different key is provided, performing cleanup for the old operation before initializing the new. * For example: * * @sample androidx.compose.runtime.samples.disposableEffectSample * * A [DisposableEffect] **must** include an [onDispose][DisposableEffectScope.onDispose] clause as * the final statement in its [effect] block. If your operation does not require disposal it might * be a [SideEffect] instead, or a [LaunchedEffect] if it launches a coroutine that should be * managed by the composition. * * There is guaranteed to be one call to [dispose][DisposableEffectScope.onDispose] for every call * to [effect]. Both [effect] and [dispose][DisposableEffectScope.onDispose] will always be run on * the composition's apply dispatcher and appliers are never run concurrent with themselves, one * another, applying changes to the composition tree, or running [RememberObserver] event callbacks. */ @Composable @NonRestartableComposable @Suppress("ArrayReturn") public fun DisposableEffect( vararg keys: Any?, effect: DisposableEffectScope.() -> DisposableEffectResult, ) { remember(*keys) { DisposableEffectImpl(effect) } } internal class LaunchedEffectImpl( private val parentCoroutineContext: CoroutineContext, private val task: suspend CoroutineScope.() -> Unit, ) : RememberObserver, CoroutineExceptionHandler { private val scope: CoroutineScope private var job: Job? = null init { var context = parentCoroutineContext + this @OptIn(ComposeToolingApi::class) if (ComposeToolingFlags.isVerboseTracingEnabled) { context += LaunchedEffectTracingContext } scope = CoroutineScope(context) } override fun onRemembered() { // This should never happen but is left here for safety job?.cancel("Old job was still running!") job = scope.launch(block = task) } override fun onForgotten() { job?.cancel(LeftCompositionCancellationException()) job = null } override fun onAbandoned() { job?.cancel(LeftCompositionCancellationException()) job = null } // CoroutineExceptionHandler implementation to save on allocations override val key: CoroutineContext.Key<*> get() = CoroutineExceptionHandler.Key override fun handleException(context: CoroutineContext, exception: Throwable) { context[CompositionErrorContextImpl]?.apply { exception.attachComposeStackTrace(this@LaunchedEffectImpl) } parentCoroutineContext[CoroutineExceptionHandler]?.handleException(context, exception) ?: throw exception } } /** * When [LaunchedEffect] enters the composition it will launch [block] into the composition's * [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] when the [LaunchedEffect] * leaves the composition. * * It is an error to call [LaunchedEffect] without at least one `key` parameter. */ // This deprecated-error function shadows the varargs overload so that the varargs version // is not used without key parameters. @Deprecated(LaunchedEffectNoParamError, level = DeprecationLevel.ERROR) @Suppress("DeprecatedCallableAddReplaceWith", "UNUSED_PARAMETER") @Composable public fun LaunchedEffect(block: suspend CoroutineScope.() -> Unit): Unit = error(LaunchedEffectNoParamError) /** * When [LaunchedEffect] enters the composition it will launch [block] into the composition's * [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when * [LaunchedEffect] is recomposed with a different [key1]. The coroutine will be * [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition. * * This function should **not** be used to (re-)launch ongoing tasks in response to callback events * by way of storing callback data in [MutableState] passed to [key1]. Instead, see * [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs * scoped to the composition in response to event callbacks. */ @Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) public fun LaunchedEffect(key1: Any?, block: suspend CoroutineScope.() -> Unit) { val applyContext = currentComposer.applyCoroutineContext remember(key1) { LaunchedEffectImpl(applyContext, block) } } /** * When [LaunchedEffect] enters the composition it will launch [block] into the composition's * [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when * [LaunchedEffect] is recomposed with a different [key1] or [key2]. The coroutine will be * [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition. * * This function should **not** be used to (re-)launch ongoing tasks in response to callback events * by way of storing callback data in [MutableState] passed to [key]. Instead, see * [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs * scoped to the composition in response to event callbacks. */ @Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) public fun LaunchedEffect(key1: Any?, key2: Any?, block: suspend CoroutineScope.() -> Unit) { val applyContext = currentComposer.applyCoroutineContext remember(key1, key2) { LaunchedEffectImpl(applyContext, block) } } /** * When [LaunchedEffect] enters the composition it will launch [block] into the composition's * [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when * [LaunchedEffect] is recomposed with a different [key1], [key2] or [key3]. The coroutine will be * [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition. * * This function should **not** be used to (re-)launch ongoing tasks in response to callback events * by way of storing callback data in [MutableState] passed to [key]. Instead, see * [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs * scoped to the composition in response to event callbacks. */ @Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) public fun LaunchedEffect( key1: Any?, key2: Any?, key3: Any?, block: suspend CoroutineScope.() -> Unit, ) { val applyContext = currentComposer.applyCoroutineContext remember(key1, key2, key3) { LaunchedEffectImpl(applyContext, block) } } private class LeftCompositionCancellationException : PlatformOptimizedCancellationException("The coroutine scope left the composition") /** * When [LaunchedEffect] enters the composition it will launch [block] into the composition's * [CoroutineContext]. The coroutine will be [cancelled][Job.cancel] and **re-launched** when * [LaunchedEffect] is recomposed with any different [keys]. The coroutine will be * [cancelled][Job.cancel] when the [LaunchedEffect] leaves the composition. * * This function should **not** be used to (re-)launch ongoing tasks in response to callback events * by way of storing callback data in [MutableState] passed to [key]. Instead, see * [rememberCoroutineScope] to obtain a [CoroutineScope] that may be used to launch ongoing jobs * scoped to the composition in response to event callbacks. */ @Composable @NonRestartableComposable @Suppress("ArrayReturn") @OptIn(InternalComposeApi::class) public fun LaunchedEffect(vararg keys: Any?, block: suspend CoroutineScope.() -> Unit) { val applyContext = currentComposer.applyCoroutineContext remember(*keys) { LaunchedEffectImpl(applyContext, block) } } // Maintenance note: this class once was used by the inlined implementation of // rememberCoroutineScope and must be maintained for binary compatibility. The new implementation // of RememberedCoroutineScope implements RememberObserver directly, since as of this writing the // compose runtime no longer implicitly treats objects incidentally stored in the slot table (e.g. // previous parameter values from a skippable invocation, remember keys, etc.) as eligible // RememberObservers. This dramatically reduces the risk of receiving unexpected RememberObserver // lifecycle callbacks when a reference to a RememberObserver is leaked into user code and we can // omit wrapper RememberObservers such as this one. @PublishedApi internal class CompositionScopedCoroutineScopeCanceller(val coroutineScope: CoroutineScope) : RememberObserver { override fun onRemembered() { // Nothing to do } override fun onForgotten() { val coroutineScope = coroutineScope if (coroutineScope is RememberedCoroutineScope) { coroutineScope.cancelIfCreated() } else { coroutineScope.cancel(LeftCompositionCancellationException()) } } override fun onAbandoned() { val coroutineScope = coroutineScope if (coroutineScope is RememberedCoroutineScope) { coroutineScope.cancelIfCreated() } else { coroutineScope.cancel(LeftCompositionCancellationException()) } } } private class CancelledCoroutineContext : CoroutineContext.Element { override val key: CoroutineContext.Key<*> get() = Key companion object Key : CoroutineContext.Key } private class ForgottenCoroutineScopeException : PlatformOptimizedCancellationException("rememberCoroutineScope left the composition") internal class RememberedCoroutineScope( private val parentContext: CoroutineContext, private val overlayContext: CoroutineContext, ) : CoroutineScope, RememberObserver { private val lock = makeSynchronizedObject(this) // The goal of this implementation is to make cancellation as cheap as possible if the // coroutineContext property was never accessed, consisting only of taking a monitor lock and // setting a volatile field. @Volatile private var _coroutineContext: CoroutineContext? = null override val coroutineContext: CoroutineContext get() { var localCoroutineContext = _coroutineContext if ( localCoroutineContext == null || localCoroutineContext === CancelledCoroutineContext ) { val traceContext = parentContext[CompositionErrorContextImpl] val exceptionHandler = if (traceContext != null) { // If trace context is present, override exception handler, so all child // jobs would have the composable trace appended. // On exception, call overlay -> parent and throw if neither are present. CoroutineExceptionHandler { c, e -> traceContext.apply { e.attachComposeStackTrace(this@RememberedCoroutineScope) } overlayContext[CoroutineExceptionHandler]?.handleException(c, e) ?: parentContext[CoroutineExceptionHandler]?.handleException(c, e) ?: throw e } } else { EmptyCoroutineContext } // Yes, we're leaking our lock here by using the instance of the object // that also gets handled by user code as a CoroutineScope as an intentional // tradeoff for avoiding the allocation of a dedicated lock object. // Since we only use it here for this lazy initialization and control flow // does not escape the creation of the CoroutineContext while holding the lock, // the splash damage should be acceptable. synchronized(lock) { localCoroutineContext = _coroutineContext if (localCoroutineContext == null) { val parentContext = parentContext val childJob = Job(parentContext[Job]) localCoroutineContext = parentContext + childJob + overlayContext + exceptionHandler } else if (localCoroutineContext === CancelledCoroutineContext) { // Lazily initialize the child job here, already cancelled. // Assemble the CoroutineContext exactly as otherwise expected. val parentContext = parentContext val cancelledChildJob = Job(parentContext[Job]).apply { cancel(ForgottenCoroutineScopeException()) } localCoroutineContext = parentContext + cancelledChildJob + overlayContext + exceptionHandler @OptIn(ComposeToolingApi::class) if (ComposeToolingFlags.isVerboseTracingEnabled) { localCoroutineContext += RememberedCoroutineScopeTracingContext } } _coroutineContext = localCoroutineContext } } return localCoroutineContext!! } fun cancelIfCreated() { // Take the lock unconditionally; this is internal API only used by internal // RememberObserver implementations that are not leaked to user code; we can assume // this won't be called repeatedly. If this assumption is violated we'll simply create a // redundant exception. synchronized(lock) { val context = _coroutineContext if (context == null) { _coroutineContext = CancelledCoroutineContext } else { // Ignore optimizing the case where we might be cancelling an already cancelled job; // only internal callers such as RememberObservers will invoke this method. context.cancel(ForgottenCoroutineScopeException()) } } } override fun onRemembered() { // Do nothing } override fun onForgotten() { cancelIfCreated() } override fun onAbandoned() { cancelIfCreated() } companion object { @JvmField val CancelledCoroutineContext: CoroutineContext = CancelledCoroutineContext() } } @PublishedApi @OptIn(InternalComposeApi::class) internal fun createCompositionCoroutineScope( coroutineContext: CoroutineContext, composer: Composer, ): CoroutineScope = if (coroutineContext[Job] != null) { CoroutineScope( Job().apply { completeExceptionally( IllegalArgumentException( "CoroutineContext supplied to " + "rememberCoroutineScope may not include a parent job" ) ) } ) } else { val applyContext = composer.applyCoroutineContext RememberedCoroutineScope(applyContext, coroutineContext) } /** * Return a [CoroutineScope] bound to this point in the composition using the optional * [CoroutineContext] provided by [getContext]. [getContext] will only be called once and the same * [CoroutineScope] instance will be returned across recompositions. * * This scope will be [cancelled][CoroutineScope.cancel] when this call leaves the composition. The * [CoroutineContext] returned by [getContext] may not contain a [Job] as this scope is considered * to be a child of the composition. * * The default dispatcher of this scope if one is not provided by the context returned by * [getContext] will be the applying dispatcher of the composition's [Recomposer]. * * Use this scope to launch jobs in response to callback events such as clicks or other user * interaction where the response to that event needs to unfold over time and be cancelled if the * composable managing that process leaves the composition. Jobs should never be launched into * **any** coroutine scope as a side effect of composition itself. For scoped ongoing jobs initiated * by composition, see [LaunchedEffect]. * * This function will not throw if preconditions are not met, as composable functions do not yet * fully support exceptions. Instead the returned scope's [CoroutineScope.coroutineContext] will * contain a failed [Job] with the associated exception and will not be capable of launching child * jobs. */ @Composable public inline fun rememberCoroutineScope( crossinline getContext: @DisallowComposableCalls () -> CoroutineContext = { EmptyCoroutineContext } ): CoroutineScope { val composer = currentComposer return remember { createCompositionCoroutineScope(getContext(), composer) } } private object LaunchedEffectTracingContext : TracingContext("Compose:LaunchedEffect") private object RememberedCoroutineScopeTracingContext : TracingContext("Compose:coroutineScope") internal expect abstract class TracingContext(name: String) : CoroutineContext.Element { override val key: CoroutineContext.Key<*> companion object Key : CoroutineContext.Key } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/DerivedState.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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. */ @file:JvmName("SnapshotStateKt") @file:JvmMultifileClass package androidx.compose.runtime import androidx.collection.MutableObjectIntMap import androidx.collection.ObjectIntMap import androidx.collection.emptyObjectIntMap import androidx.compose.runtime.collection.MutableVector import androidx.compose.runtime.internal.IntRef import androidx.compose.runtime.internal.SnapshotThreadLocal import androidx.compose.runtime.internal.identityHashCode import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.runtime.snapshots.SnapshotId import androidx.compose.runtime.snapshots.SnapshotIdZero import androidx.compose.runtime.snapshots.StateFactoryMarker import androidx.compose.runtime.snapshots.StateObject import androidx.compose.runtime.snapshots.StateObjectImpl import androidx.compose.runtime.snapshots.StateRecord import androidx.compose.runtime.snapshots.current import androidx.compose.runtime.snapshots.currentSnapshot import androidx.compose.runtime.snapshots.newWritableRecord import androidx.compose.runtime.snapshots.sync import androidx.compose.runtime.snapshots.withCurrent import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName import kotlin.math.min /** * A [State] that is derived from one or more other states. * * @see derivedStateOf */ internal interface DerivedState : State { /** Provides a current [Record]. */ val currentRecord: Record /** * Mutation policy that controls how changes are handled after state dependencies update. If the * policy is `null`, the derived state update is triggered regardless of the value produced and * it is up to observer to invalidate it correctly. */ val policy: SnapshotMutationPolicy? interface Record { /** * The value of the derived state retrieved without triggering a notification to read * observers. */ val currentValue: T /** * Map of the dependencies used to produce [value] or [currentValue] to nested read level. * * This map can be used to determine if the state could affect value of this derived state, * when a [StateObject] appears in the apply observer set. */ val dependencies: ObjectIntMap } } private val calculationBlockNestedLevel = SnapshotThreadLocal() private inline fun withCalculationNestedLevel(block: (IntRef) -> T): T { val ref = calculationBlockNestedLevel.get() ?: IntRef(0).also { calculationBlockNestedLevel.set(it) } return block(ref) } private class DerivedSnapshotState( private val calculation: () -> T, override val policy: SnapshotMutationPolicy?, ) : StateObjectImpl(), DerivedState { private var first: ResultRecord = ResultRecord(currentSnapshot().snapshotId) class ResultRecord(snapshotId: SnapshotId) : StateRecord(snapshotId), DerivedState.Record { companion object { val Unset = Any() } var validSnapshotId: SnapshotId = SnapshotIdZero var validSnapshotWriteCount: Int = 0 override var dependencies: ObjectIntMap = emptyObjectIntMap() var result: Any? = Unset var resultHash: Int = 0 override fun assign(value: StateRecord) { @Suppress("UNCHECKED_CAST") val other = value as ResultRecord dependencies = other.dependencies result = other.result resultHash = other.resultHash } override fun create(): StateRecord = create(currentSnapshot().snapshotId) override fun create(snapshotId: SnapshotId): StateRecord = ResultRecord(snapshotId) fun isValid(derivedState: DerivedState<*>, snapshot: Snapshot): Boolean { val snapshotChanged = sync { validSnapshotId != snapshot.snapshotId || validSnapshotWriteCount != snapshot.writeCount } val isValid = result !== Unset && (!snapshotChanged || resultHash == readableHash(derivedState, snapshot)) if (isValid && snapshotChanged) { sync { validSnapshotId = snapshot.snapshotId validSnapshotWriteCount = snapshot.writeCount } } return isValid } fun readableHash(derivedState: DerivedState<*>, snapshot: Snapshot): Int { var hash = 7 val dependencies = sync { dependencies } if (dependencies.isNotEmpty()) { notifyObservers(derivedState) { dependencies.forEach { stateObject, readLevel -> if (readLevel != 1) { return@forEach } // Find the first record without triggering an observer read. val record = if (stateObject is DerivedSnapshotState<*>) { // eagerly access the parent derived states without recording the // read // that way we can be sure derived states in deps were recalculated, // and are updated to the last values stateObject.current(snapshot) } else { current(stateObject.firstStateRecord, snapshot) } hash = 31 * hash + identityHashCode(record) hash = 31 * hash + record.snapshotId.hashCode() } } } return hash } override val currentValue: T @Suppress("UNCHECKED_CAST") get() = result as T } /** * Get current record in snapshot. Forces recalculation if record is invalid to refresh state * value. * * @return latest state record for the derived state. */ fun current(snapshot: Snapshot): StateRecord = currentRecord(current(first, snapshot), snapshot, false, calculation) private fun currentRecord( readable: ResultRecord, snapshot: Snapshot, forceDependencyReads: Boolean, calculation: () -> T, ): ResultRecord { if (readable.isValid(this, snapshot)) { // If the dependency is not recalculated, emulate nested state reads // for correct invalidation later if (forceDependencyReads) { notifyObservers(this) { val dependencies = readable.dependencies withCalculationNestedLevel { calculationLevelRef -> val invalidationNestedLevel = calculationLevelRef.element dependencies.forEach { dependency, nestedLevel -> calculationLevelRef.element = invalidationNestedLevel + nestedLevel snapshot.readObserver?.invoke(dependency) } calculationLevelRef.element = invalidationNestedLevel } } } return readable } val newDependencies = MutableObjectIntMap() val result = withCalculationNestedLevel { calculationLevelRef -> val nestedCalculationLevel = calculationLevelRef.element notifyObservers(this) { calculationLevelRef.element = nestedCalculationLevel + 1 val result = Snapshot.observe( { if (it === this) error("A derived state calculation cannot read itself") if (it is StateObject) { val readNestedLevel = calculationLevelRef.element newDependencies[it] = min( readNestedLevel - nestedCalculationLevel, newDependencies.getOrDefault(it, Int.MAX_VALUE), ) } }, null, calculation, ) calculationLevelRef.element = nestedCalculationLevel result } } val record = sync { val currentSnapshot = Snapshot.current if ( readable.result !== ResultRecord.Unset && @Suppress("UNCHECKED_CAST") policy?.equivalent(result, readable.result as T) == true ) { readable.dependencies = newDependencies readable.resultHash = readable.readableHash(this, currentSnapshot) readable } else { val writable = first.newWritableRecord(this, currentSnapshot) writable.dependencies = newDependencies writable.resultHash = writable.readableHash(this, currentSnapshot) writable.result = result writable } } if (calculationBlockNestedLevel.get()?.element == 0) { Snapshot.notifyObjectsInitialized() sync { val currentSnapshot = Snapshot.current record.validSnapshotId = currentSnapshot.snapshotId record.validSnapshotWriteCount = currentSnapshot.writeCount } } return record } override val firstStateRecord: StateRecord get() = first override fun prependStateRecord(value: StateRecord) { @Suppress("UNCHECKED_CAST") first = value as ResultRecord } override val value: T get() { // Unlike most state objects, the record list of a derived state can change during a // read // because reading updates the cache. To account for this, instead of calling readable, // which sends the read notification, the read observer is notified directly and current // value is used instead which doesn't notify. This allow the read observer to read the // value and only update the cache once. Snapshot.current.readObserver?.invoke(this) // Read observer could advance the snapshot, so get current snapshot again val snapshot = Snapshot.current val record = current(first, snapshot) @Suppress("UNCHECKED_CAST") return currentRecord(record, snapshot, true, calculation).result as T } override val currentRecord: DerivedState.Record get() { val snapshot = Snapshot.current val record = current(first, snapshot) return currentRecord(record, snapshot, false, calculation) } override fun toString(): String = first.withCurrent { "DerivedState(value=${displayValue()})@${hashCode()}" } /** * A function used by the debugger to display the value of the current value of the mutable * state object without triggering read observers. */ @Suppress("unused") val debuggerDisplayValue: T? @JvmName("getDebuggerDisplayValue") get() = first.withCurrent { @Suppress("UNCHECKED_CAST") if (it.isValid(this, Snapshot.current)) it.result as T else null } private fun displayValue(): String { first.withCurrent { if (it.isValid(this, Snapshot.current)) { return it.result.toString() } return "" } } } /** * Creates a [State] object whose [State.value] is the result of [calculation]. The result of * calculation will be cached in such a way that calling [State.value] repeatedly will not cause * [calculation] to be executed multiple times, but reading [State.value] will cause all [State] * objects that got read during the [calculation] to be read in the current [Snapshot], meaning that * this will correctly subscribe to the derived state objects if the value is being read in an * observed context such as a [Composable] function. Derived states without mutation policy trigger * updates on each dependency change. To avoid invalidation on update, provide suitable * [SnapshotMutationPolicy] through [derivedStateOf] overload. * * @sample androidx.compose.runtime.samples.DerivedStateSample * @param calculation the calculation to create the value this state object represents. */ @StateFactoryMarker public fun derivedStateOf(calculation: () -> T): State = DerivedSnapshotState(calculation, null) /** * Creates a [State] object whose [State.value] is the result of [calculation]. The result of * calculation will be cached in such a way that calling [State.value] repeatedly will not cause * [calculation] to be executed multiple times, but reading [State.value] will cause all [State] * objects that got read during the [calculation] to be read in the current [Snapshot], meaning that * this will correctly subscribe to the derived state objects if the value is being read in an * observed context such as a [Composable] function. * * @sample androidx.compose.runtime.samples.DerivedStateSample * @param policy mutation policy to control when changes to the [calculation] result trigger update. * @param calculation the calculation to create the value this state object represents. */ @StateFactoryMarker public fun derivedStateOf(policy: SnapshotMutationPolicy, calculation: () -> T): State = DerivedSnapshotState(calculation, policy) /** Observe the recalculations performed by derived states. */ internal interface DerivedStateObserver { /** Called before a calculation starts. */ fun start(derivedState: DerivedState<*>) /** Called after the started calculation is complete. */ fun done(derivedState: DerivedState<*>) } private val derivedStateObservers = SnapshotThreadLocal>() internal fun derivedStateObservers(): MutableVector = derivedStateObservers.get() ?: MutableVector(0).also { derivedStateObservers.set(it) } private inline fun notifyObservers(derivedState: DerivedState<*>, block: () -> R): R { val observers = derivedStateObservers() observers.forEach { it.start(derivedState) } return try { block() } finally { observers.forEach { it.done(derivedState) } } } /** * Observe the recalculations performed by any derived state that is recalculated during the * execution of [block]. * * @param observer called for every calculation of a derived state in the [block]. * @param block the block of code to observe. */ internal inline fun observeDerivedStateRecalculations( observer: DerivedStateObserver, block: () -> R, ) { val observers = derivedStateObservers() try { observers.add(observer) block() } finally { observers.removeAt(observers.lastIndex) } } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/Snapshot.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.runtime.snapshots import androidx.collection.MutableScatterSet import androidx.collection.mutableScatterSetOf import androidx.compose.runtime.Composable import androidx.compose.runtime.DisallowComposableCalls import androidx.compose.runtime.ExperimentalComposeRuntimeApi import androidx.compose.runtime.InternalComposeApi import androidx.compose.runtime.checkPrecondition import androidx.compose.runtime.collection.wrapIntoSet import androidx.compose.runtime.internal.AtomicInt import androidx.compose.runtime.internal.JvmDefaultWithCompatibility import androidx.compose.runtime.internal.SnapshotThreadLocal import androidx.compose.runtime.internal.currentThreadId import androidx.compose.runtime.platform.SynchronizedObject import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import androidx.compose.runtime.requirePrecondition import androidx.compose.runtime.snapshots.Snapshot.Companion.takeMutableSnapshot import androidx.compose.runtime.snapshots.Snapshot.Companion.takeSnapshot import androidx.compose.runtime.snapshots.tooling.creatingSnapshot import androidx.compose.runtime.snapshots.tooling.dispatchObserverOnApplied import androidx.compose.runtime.snapshots.tooling.dispatchObserverOnPreDispose import androidx.compose.runtime.tooling.verboseTrace import kotlin.contracts.ExperimentalContracts import kotlin.contracts.InvocationKind import kotlin.contracts.contract /** * A snapshot of the values return by mutable states and other state objects. All state object will * have the same value in the snapshot as they had when the snapshot was created unless they are * explicitly changed in the snapshot. * * To enter a snapshot call [enter]. The snapshot is the current snapshot as returned by * [currentSnapshot] until the control returns from the lambda (or until a nested [enter] is * called). All state objects will return the values associated with this snapshot, locally in the * thread, until [enter] returns. All other threads are unaffected. * * Snapshots can be nested by calling [takeNestedSnapshot]. * * @see takeSnapshot * @see takeMutableSnapshot * @see androidx.compose.runtime.mutableStateOf * @see androidx.compose.runtime.mutableStateListOf * @see androidx.compose.runtime.mutableStateMapOf */ public sealed class Snapshot( snapshotId: SnapshotId, /** A set of all the snapshots that should be treated as invalid. */ internal open var invalid: SnapshotIdSet, ) { @Deprecated("Use id: Long constructor instead", level = DeprecationLevel.HIDDEN) protected constructor(id: Int, invalid: SnapshotIdSet) : this(id.toSnapshotId(), invalid) /** * The snapshot id of the snapshot. This is a unique number from a monotonically increasing * value for each snapshot taken. * * [id] will is identical to [snapshotId] if the value of [snapshotId] is less than or equal to * [Int.MAX_VALUE]. For [snapshotId] value greater than [Int.MAX_VALUE], this value will return * a negative value. */ @Deprecated("Use snapshotId instead", replaceWith = ReplaceWith("snapshotId")) public open val id: Int get() = snapshotId.toInt() /** * The snapshot id of the snapshot. This is a unique number from a monotonically increasing * value for each snapshot taken. */ public open var snapshotId: SnapshotId = snapshotId internal set internal open var writeCount: Int get() = 0 @Suppress("UNUSED_PARAMETER") set(value) { error("Updating write count is not supported for this snapshot") } /** * The root snapshot for this snapshot. For non-nested snapshots this is always `this`. For * nested snapshot it is the parent's [root]. */ public abstract val root: Snapshot /** True if any change to a state object in this snapshot will throw. */ public abstract val readOnly: Boolean /** * Dispose the snapshot. Neglecting to dispose a snapshot will result in difficult to diagnose * memory leaks as it indirectly causes all state objects to maintain its value for the * un-disposed snapshot. */ public open fun dispose() { disposed = true sync { releasePinnedSnapshotLocked() } } /** * Take a snapshot of the state values in this snapshot. The resulting [Snapshot] is read-only. * All nested snapshots need to be disposed by calling [dispose] before resources associated * with this snapshot can be collected. Nested snapshots are still valid after the parent has * been disposed. */ public abstract fun takeNestedSnapshot(readObserver: ((Any) -> Unit)? = null): Snapshot /** * Whether there are any pending changes in this snapshot. These changes are not visible until * the snapshot is applied. */ public abstract fun hasPendingChanges(): Boolean /** * Enter the snapshot. In [block] all state objects have the value associated with this * snapshot. The value of [currentSnapshot] will be this snapshot until this [block] returns or * a nested call to [enter] is called. When [block] returns, the previous current snapshot is * restored if there was one. * * All changes to state objects inside [block] are isolated to this snapshot and are not visible * to other snapshot or as global state. If this is a [readOnly] snapshot, any changes to state * objects will throw an [IllegalStateException]. * * For a [MutableSnapshot], changes made to a snapshot inside [block] can be applied atomically * to the global state (or to its parent snapshot if it is a nested snapshot) by calling * [MutableSnapshot.apply]. * * @see androidx.compose.runtime.mutableStateOf * @see androidx.compose.runtime.mutableStateListOf * @see androidx.compose.runtime.mutableStateMapOf */ public inline fun enter(block: () -> T): T { val previous = makeCurrent() try { return block() } finally { restoreCurrent(previous) } } @PublishedApi internal open fun makeCurrent(): Snapshot? { val previous = threadSnapshot.get() threadSnapshot.set(this) return previous } @PublishedApi internal open fun restoreCurrent(snapshot: Snapshot?) { threadSnapshot.set(snapshot) } /** * Enter the snapshot, returning the previous [Snapshot] for leaving this snapshot later using * [unsafeLeave]. Prefer [enter] or [asContextElement] instead of using [unsafeEnter] directly * to prevent mismatched [unsafeEnter]/[unsafeLeave] calls. * * After returning all state objects have the value associated with this snapshot. The value of * [currentSnapshot] will be this snapshot until [unsafeLeave] is called with the returned * [Snapshot] or another call to [unsafeEnter] or [enter] is made. * * All changes to state objects until another snapshot is entered or this snapshot is left are * isolated to this snapshot and are not visible to other snapshot or as global state. If this * is a [readOnly] snapshot, any changes to state objects will throw an [IllegalStateException]. * * For a [MutableSnapshot], changes made to a snapshot can be applied atomically to the global * state (or to its parent snapshot if it is a nested snapshot) by calling * [MutableSnapshot.apply]. */ public fun unsafeEnter(): Snapshot? = makeCurrent() /** Leave the snapshot, restoring the [oldSnapshot] before returning. See [unsafeEnter]. */ public fun unsafeLeave(oldSnapshot: Snapshot?) { checkPrecondition(threadSnapshot.get() === this) { "Cannot leave snapshot; $this is not the current snapshot" } restoreCurrent(oldSnapshot) } internal var disposed = false /* * Handle to use when unpinning this snapshot. -1 if this snapshot has been unpinned. */ @Suppress("LeakingThis") private var pinningTrackingHandle = if (snapshotId != INVALID_SNAPSHOT) trackPinning(snapshotId, invalid) else -1 internal inline val isPinned get() = pinningTrackingHandle >= 0 /* * The read observer for the snapshot if there is one. */ @PublishedApi internal abstract val readObserver: ((Any) -> Unit)? /** The write observer for the snapshot if there is one. */ internal abstract val writeObserver: ((Any) -> Unit)? /** Called when a nested snapshot of this snapshot is activated */ internal abstract fun nestedActivated(snapshot: Snapshot) /** Called when a nested snapshot of this snapshot is deactivated */ internal abstract fun nestedDeactivated(snapshot: Snapshot) /** Record that state was modified in the snapshot. */ internal abstract fun recordModified(state: StateObject) /** The set of state objects that have been modified in this snapshot. */ internal abstract val modified: MutableScatterSet? /** * Notify the snapshot that all objects created in this snapshot to this point should be * considered initialized. If any state object is modified after this point it will appear as * modified in the snapshot. Any applicable snapshot write observer will be called for the * object and the object will be part of the a set of mutated objects sent to any applicable * snapshot apply observer. * * Unless [notifyObjectsInitialized] is called, state objects created in a snapshot are not * considered modified by the snapshot even if they are modified after construction. */ internal abstract fun notifyObjectsInitialized() /** * Closes the snapshot by removing the snapshot id (an any previous id's) from the list of open * snapshots and unpinning snapshots that no longer are referenced by this snapshot. */ internal fun closeAndReleasePinning() { sync { closeLocked() releasePinnedSnapshotsForCloseLocked() } } /** * Closes the snapshot by removing the snapshot id (and any previous ids) from the list of open * snapshots. Does not release pinned snapshots. See [releasePinnedSnapshotsForCloseLocked] for * the second half of [closeAndReleasePinning]. * * Call while holding a `sync {}` lock. */ internal open fun closeLocked() { openSnapshots = openSnapshots.clear(snapshotId) } /** * Releases all pinned snapshots required to perform a clean [closeAndReleasePinning]. * * Call while holding a `sync {}` lock. * * See [closeAndReleasePinning], [closeLocked]. */ internal open fun releasePinnedSnapshotsForCloseLocked() { releasePinnedSnapshotLocked() } internal fun validateNotDisposed() { requirePrecondition(!disposed) { "Cannot use a disposed snapshot" } } internal fun releasePinnedSnapshotLocked() { if (pinningTrackingHandle >= 0) { releasePinningLocked(pinningTrackingHandle) pinningTrackingHandle = -1 } } internal fun takeoverPinnedSnapshot(): Int = pinningTrackingHandle.also { pinningTrackingHandle = -1 } public companion object { /** * Return the thread's active snapshot. If no thread snapshot is active then the current * global snapshot is used. */ public val current: Snapshot get() = currentSnapshot() /** Return `true` if the thread is currently in the context of a snapshot. */ public val isInSnapshot: Boolean get() = threadSnapshot.get() != null /** * Returns whether any threads are currently in the process of notifying observers about * changes to the global snapshot. */ public val isApplyObserverNotificationPending: Boolean get() = pendingApplyObserverCount.get() > 0 /** * All new state objects initial state records should be [PreexistingSnapshotId] which then * allows snapshots outside the creating snapshot to access the object with its initial * state. */ @Suppress("ConstPropertyName") public const val PreexistingSnapshotId: Int = 1 /** * Take a snapshot of the current value of all state objects. The values are preserved until * [Snapshot.dispose] is called on the result. * * The [readObserver] parameter can be used to track when all state objects are read when in * [Snapshot.enter]. A snapshot apply observer can be registered using * [Snapshot.registerApplyObserver] to observe modification of state objects. * * An active snapshot (after it is created but before [Snapshot.dispose] is called) requires * resources to track the values in the snapshot. Once a snapshot is no longer needed it * should disposed by calling [Snapshot.dispose]. * * Leaving a snapshot active could cause hard to diagnose memory leaks values as are * maintained by state objects for these unneeded snapshots. Take care to always call * [Snapshot.dispose] on all snapshots when they are no longer needed. * * Composition uses both of these to implicitly subscribe to changes to state object and * automatically update the composition when state objects read during composition change. * * A nested snapshot can be taken of a snapshot which is an independent read-only copy of * the snapshot and can be disposed independently. This is used by [takeSnapshot] when in a * read-only snapshot for API consistency allowing the result of [takeSnapshot] to be * disposed leaving the parent snapshot active. * * @param readObserver called when any state object is read in the lambda passed to * [Snapshot.enter] or in the [Snapshot.enter] of any nested snapshot. * @see Snapshot * @see Snapshot.registerApplyObserver */ public fun takeSnapshot(readObserver: ((Any) -> Unit)? = null): Snapshot = currentSnapshot().takeNestedSnapshot(readObserver) /** * Take a snapshot of the current value of all state objects that also allows the state to * be changed and later atomically applied when [MutableSnapshot.apply] is called. The * values are preserved until [Snapshot.dispose] is called on the result. The global state * will either see all the changes made as one atomic change, when [MutableSnapshot .apply] * is called, or none of the changes if the mutable state object is disposed before being * applied. * * The values in a snapshot can be modified by calling [Snapshot.enter] and then, in its * lambda, modify any state object. The new values of the state objects will only become * visible to the global state when [MutableSnapshot.apply] is called. * * An active snapshot (after it is created but before [Snapshot.dispose] is called) requires * resources to track the values in the snapshot. Once a snapshot is no longer needed it * should disposed by calling [Snapshot.dispose]. * * Leaving a snapshot active could cause hard to diagnose memory leaks as values are * maintained by state objects for these unneeded snapshots. Take care to always call * [Snapshot.dispose] on all snapshots when they are no longer needed. * * A nested snapshot can be taken by calling [Snapshot.takeNestedSnapshot], for a read-only * snapshot, or [MutableSnapshot.takeNestedMutableSnapshot] for a snapshot that can be * changed. Nested mutable snapshots are applied to the this, the parent snapshot, when * their [MutableSnapshot.apply] is called. Their applied changes will be visible to in this * snapshot but will not be visible other snapshots (including other nested snapshots) or * the global state until this snapshot is applied by calling [MutableSnapshot.apply]. * * Once [MutableSnapshot.apply] is called on this, the parent snapshot, all calls to * [MutableSnapshot.apply] on an active nested snapshot will fail. * * Changes to a mutable snapshot are isolated, using snapshot isolation, from all other * snapshots. Their changes are only visible as global state or to new snapshots once * [MutableSnapshot.apply] is called. * * Applying a snapshot can fail if currently visible changes to the state object conflicts * with a change made in the snapshot. * * When in a mutable snapshot, [takeMutableSnapshot] creates a nested snapshot of the * current mutable snapshot. If the current snapshot is read-only, an exception is thrown. * The current snapshot is the result of calling [currentSnapshot] which is updated by * calling [Snapshot.enter] which makes the [Snapshot] the current snapshot while in its * lambda. * * Composition uses mutable snapshots to allow changes made in a [Composable] functions to * be temporarily isolated from the global state and is later applied to the global state * when the composition is applied. If [MutableSnapshot.apply] fails applying this snapshot, * the snapshot and the changes calculated during composition are disposed and a new * composition is scheduled to be calculated again. * * @param readObserver called when any state object is read in the lambda passed to * [Snapshot.enter] or in the [Snapshot.enter] of any nested snapshots. * * Composition, layout and draw use [readObserver] to implicitly subscribe to changes to * state objects to know when to update. * * @param writeObserver called when a state object is created or just before it is written * to the first time in the snapshot or a nested mutable snapshot. This might be called * several times for the same object if nested mutable snapshots are created. * * Composition uses [writeObserver] to track when a state object is modified during * composition in order to invalidate the reads that have not yet occurred. This allows a * single pass of composition for state objects that are written to before they are read * (such as modifying the value of a dynamic ambient provider). * * @see Snapshot.takeSnapshot * @see Snapshot * @see MutableSnapshot */ public fun takeMutableSnapshot( readObserver: ((Any) -> Unit)? = null, writeObserver: ((Any) -> Unit)? = null, ): MutableSnapshot = (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot( readObserver, writeObserver, ) ?: error("Cannot create a mutable snapshot of an read-only snapshot") /** * Escape the current snapshot, if there is one. All state objects will have the value * associated with the global while the [block] lambda is executing. * * @return the result of [block] */ public inline fun global(block: () -> T): T { val previous = removeCurrent() try { return block() } finally { restoreCurrent(previous) } } /** * Take a [MutableSnapshot] and run [block] within it. When [block] returns successfully, * attempt to [MutableSnapshot.apply] the snapshot. Returns the result of [block] or throws * [SnapshotApplyConflictException] if snapshot changes attempted by [block] could not be * applied. * * Prior to returning, any changes made to snapshot state (e.g. state holders returned by * [androidx.compose.runtime.mutableStateOf] are not visible to other threads. When * [withMutableSnapshot] returns successfully those changes will be made visible to other * threads and any snapshot observers (e.g. [androidx.compose.runtime.snapshotFlow]) will be * notified of changes. * * [block] must not suspend if [withMutableSnapshot] is called from a suspend function. */ // TODO: determine a good way to prevent/discourage suspending in an inlined [block] public inline fun withMutableSnapshot(block: () -> R): R = takeMutableSnapshot().run { var hasError = false try { enter(block) } catch (e: Throwable) { hasError = true throw e } finally { if (!hasError) { apply().check() } dispose() } } /** * Observe reads and or write of state objects in the current thread. * * This only affects the current snapshot (if any) and any new snapshots create from * [Snapshot.takeSnapshot] and [takeMutableSnapshot]. It will not affect any snapshots * previous created even if [Snapshot.enter] is called in [block]. * * @param readObserver called when any state object is read. * @param writeObserver called when a state object is created or just before it is written * to the first time in the snapshot or a nested mutable snapshot. This might be called * several times for the same object if nested mutable snapshots are created. * @param block the code the [readObserver] and [writeObserver] will be observing. Once * [block] returns, the [readObserver] and [writeObserver] will no longer be called. */ public fun observe( readObserver: ((Any) -> Unit)? = null, writeObserver: ((Any) -> Unit)? = null, block: () -> T, ): T = observeInternal(readObserver, writeObserver, block) @Suppress("NOTHING_TO_INLINE") // marked as inline to use as part of SnapshotStateObserver without adding extra function // call overhead. internal inline fun observeInternal( noinline readObserver: ((Any) -> Unit)? = null, noinline writeObserver: ((Any) -> Unit)? = null, noinline block: () -> T, ): T { if (readObserver == null && writeObserver == null) { // No observer change, just execute the block return block() } val previous = threadSnapshot.get() if (previous is TransparentObserverMutableSnapshot && previous.canBeReused) { // Change observers in place without allocating new snapshots. val previousReadObserver = previous.readObserver val previousWriteObserver = previous.writeObserver try { previous.readObserver = mergedReadObserver(readObserver, previousReadObserver) previous.writeObserver = mergedWriteObserver(writeObserver, previousWriteObserver) return block() } finally { previous.readObserver = previousReadObserver previous.writeObserver = previousWriteObserver } } else { // The snapshot is not already transparent, observe in a new transparent snapshot val snapshot = when { previous == null || previous is MutableSnapshot -> { TransparentObserverMutableSnapshot( parentSnapshot = previous as? MutableSnapshot, specifiedReadObserver = readObserver, specifiedWriteObserver = writeObserver, mergeParentObservers = true, ownsParentSnapshot = false, ) } readObserver == null -> { return block() } else -> { previous.takeNestedSnapshot(readObserver) } } try { return snapshot.enter(block) } finally { snapshot.dispose() } } } @Suppress("unused") // left here for binary compatibility @PublishedApi internal fun createNonObservableSnapshot(): Snapshot = createTransparentSnapshotWithNoParentReadObserver( previousSnapshot = threadSnapshot.get() ) @PublishedApi internal val currentThreadSnapshot: Snapshot? get() = threadSnapshot.get() private inline val TransparentObserverMutableSnapshot.canBeReused: Boolean get() = threadId == currentThreadId() private inline val TransparentObserverSnapshot.canBeReused: Boolean get() = threadId == currentThreadId() @PublishedApi internal fun makeCurrentNonObservable(previous: Snapshot?): Snapshot = when { previous is TransparentObserverMutableSnapshot && previous.canBeReused -> { previous.readObserver = null previous } previous is TransparentObserverSnapshot && previous.canBeReused -> { previous.readObserver = null previous } else -> { val snapshot = createTransparentSnapshotWithNoParentReadObserver( previousSnapshot = previous ) snapshot.makeCurrent() snapshot } } @PublishedApi internal fun restoreNonObservable( previous: Snapshot?, nonObservable: Snapshot, observer: ((Any) -> Unit)?, ) { if (previous === nonObservable) { when (previous) { is TransparentObserverMutableSnapshot -> { previous.readObserver = observer } is TransparentObserverSnapshot -> { previous.readObserver = observer } else -> { error("Non-transparent snapshot was reused: $previous") } } } else { nonObservable.restoreCurrent(previous) nonObservable.dispose() } } /** * Passed [block] will be run with all the currently set snapshot read observers disabled. */ @Suppress("BanInlineOptIn") // Treat Kotlin Contracts as non-experimental. @OptIn(ExperimentalContracts::class) public inline fun withoutReadObservation(block: @DisallowComposableCalls () -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } val previousSnapshot = currentThreadSnapshot val observer = previousSnapshot?.readObserver val newSnapshot = makeCurrentNonObservable(previousSnapshot) try { return block() } finally { restoreNonObservable(previousSnapshot, newSnapshot, observer) } } /** * Register an apply listener that is called back when snapshots are applied to the global * state. * * @return [ObserverHandle] to unregister [observer]. */ public fun registerApplyObserver(observer: (Set, Snapshot) -> Unit): ObserverHandle { // Ensure observer does not see changes before this call. advanceGlobalSnapshot(emptyLambda) sync { applyObservers += observer } return ObserverHandle { sync { applyObservers -= observer } } } /** * Register an observer of the first write to the global state of a global state object * since the last call to [sendApplyNotifications]. * * Composition uses this to schedule a new composition whenever a state object that was read * in composition is modified. * * State objects can be sent to the apply observer that have not been sent to global write * observers. This happens for state objects inside [MutableSnapshot] that is later applied * by calling [MutableSnapshot.apply]. * * This should only be used to determine if a call to [sendApplyNotifications] should be * scheduled to be called. * * @return [ObserverHandle] to unregister [observer]. */ public fun registerGlobalWriteObserver(observer: ((Any) -> Unit)): ObserverHandle { sync { globalWriteObservers += observer } advanceGlobalSnapshot() return ObserverHandle { sync { globalWriteObservers -= observer } advanceGlobalSnapshot() } } /** * Notify the snapshot that all objects created in this snapshot to this point should be * considered initialized. If any state object is are modified passed this point it will * appear as modified in the snapshot and any applicable snapshot write observer will be * called for the object and the object will be part of the a set of mutated objects sent to * any applicable snapshot apply observer. * * Unless [notifyObjectsInitialized] is called, state objects created in a snapshot are not * considered modified by the snapshot even if they are modified after construction. * * Compose uses this between phases of composition to allow observing changes to state * objects create in a previous phase. */ public fun notifyObjectsInitialized(): Unit = currentSnapshot().notifyObjectsInitialized() /** * Send any pending apply notifications for state objects changed outside a snapshot. * * Apply notifications for state objects modified outside snapshot are deferred until method * is called. This method is implicitly called whenever a non-nested [MutableSnapshot] is * applied making its changes visible to all new, non-nested snapshots. * * Composition schedules this to be called after changes to state objects are detected an * observer registered with [registerGlobalWriteObserver]. */ public fun sendApplyNotifications() { val changes = sync { globalSnapshot.hasPendingChanges() } if (changes) advanceGlobalSnapshot() } @InternalComposeApi public fun openSnapshotCount(): Int = openSnapshots.toList().size @PublishedApi internal fun removeCurrent(): Snapshot? { val previous = threadSnapshot.get() if (previous != null) threadSnapshot.set(null) return previous } @PublishedApi internal fun restoreCurrent(previous: Snapshot?) { if (previous != null) threadSnapshot.set(previous) } } } /** * Pin the snapshot and invalid set. * * @return returns a handle that should be passed to [releasePinningLocked] when the snapshot closes * or is disposed. */ internal fun trackPinning(snapshotId: SnapshotId, invalid: SnapshotIdSet): Int { val pinned = invalid.lowest(snapshotId) return sync { pinningTable.add(pinned) } } /** Release the [handle] returned by [trackPinning] */ internal fun releasePinningLocked(handle: Int) { pinningTable.remove(handle) } /** * A snapshot of the values return by mutable states and other state objects. All state object will * have the same value in the snapshot as they had when the snapshot was created unless they are * explicitly changed in the snapshot. * * To enter a snapshot call [enter]. The snapshot is the current snapshot as returned by * [currentSnapshot] until the control returns from the lambda (or until a nested [enter] is called. * All state objects will return the values associated with this snapshot, locally in the thread, * until [enter] returns. All other threads are unaffected. * * All changes made in a [MutableSnapshot] are snapshot isolated from all other snapshots and their * changes can only be seen globally, or by new shots, after [MutableSnapshot.apply] as been called. * * Snapshots can be nested by calling [takeNestedSnapshot] or * [MutableSnapshot.takeNestedMutableSnapshot]. * * @see Snapshot.takeMutableSnapshot * @see androidx.compose.runtime.mutableStateOf * @see androidx.compose.runtime.mutableStateListOf * @see androidx.compose.runtime.mutableStateMapOf */ public open class MutableSnapshot internal constructor( snapshotId: SnapshotId, invalid: SnapshotIdSet, override val readObserver: ((Any) -> Unit)?, override val writeObserver: ((Any) -> Unit)?, ) : Snapshot(snapshotId, invalid) { /** * Whether there are any pending changes in this snapshot. These changes are not visible until * the snapshot is applied. */ override fun hasPendingChanges(): Boolean = modified?.isNotEmpty() == true /** * Take a mutable snapshot of the state values in this snapshot. Entering this snapshot by * calling [enter] allows state objects to be modified that are not visible to the this, the * parent snapshot, until the [apply] is called. * * Applying a nested snapshot, by calling [apply], applies its change to, this, the parent * snapshot. For a change to be visible globally, all the parent snapshots need to be applied * until the root snapshot is applied to the global state. * * All nested snapshots need to be disposed by calling [dispose] before resources associated * with this snapshot can be collected. Nested active snapshots are still valid after the parent * has been disposed but calling [apply] will fail. */ @OptIn(ExperimentalComposeRuntimeApi::class) public open fun takeNestedMutableSnapshot( readObserver: ((Any) -> Unit)? = null, writeObserver: ((Any) -> Unit)? = null, ): MutableSnapshot { validateNotDisposed() validateNotAppliedOrPinned() return creatingSnapshot(this, readObserver, writeObserver, readonly = false) { actualReadObserver, actualWriteObserver -> advance { sync { val newId = nextSnapshotId nextSnapshotId += 1 openSnapshots = openSnapshots.set(newId) val currentInvalid = invalid this.invalid = currentInvalid.set(newId) NestedMutableSnapshot( newId, currentInvalid.addRange(snapshotId + 1, newId), mergedReadObserver(actualReadObserver, this.readObserver), mergedWriteObserver(actualWriteObserver, this.writeObserver), this, ) } } } } /** * Apply the changes made to state objects in this snapshot to the global state, or to the * parent snapshot if this is a nested mutable snapshot. * * Once this method returns all changes made to this snapshot are atomically visible as the * global state of the state object or to the parent snapshot. * * While a snapshot is active (after it is created but before [apply] or [dispose] is called) * requires resources to track the values in the snapshot. Once a snapshot is no longer needed * it should be either applied by calling [apply] or disposed by calling [dispose]. A snapshot * that has been had is [apply] called can also have [dispose] called on it. However, calling * [apply] after calling [dispose] will throw an exception. * * Leaving a snapshot active could cause hard to diagnose memory leaks values are maintained by * state objects for unneeded snapshots. Take care to always call [dispose] on any snapshot. */ public open fun apply(): SnapshotApplyResult { // NOTE: the this algorithm is currently does not guarantee serializable snapshots as it // doesn't prevent crossing writes as described here https://arxiv.org/pdf/1412.2324.pdf // Just removing the snapshot from the active snapshot set is enough to make it part of the // next snapshot, however, this should only be done after first determining that there are // no // colliding writes are being applied. // A write is considered colliding if any write occurred in a state object in a snapshot // applied since the snapshot was taken. val modified = modified val optimisticMerges = if (modified != null) { val globalSnapshot = globalSnapshot optimisticMerges( globalSnapshot.snapshotId, this, openSnapshots.clear(globalSnapshot.snapshotId), ) } else null var observers = emptyList<(Set, Snapshot) -> Unit>() var globalModified: MutableScatterSet? = null sync { validateOpen(this) if (modified == null || modified.size == 0) { closeLocked() val globalSnapshot = globalSnapshot val previousModified = globalSnapshot.modified resetGlobalSnapshotLocked(globalSnapshot, emptyLambda) if (previousModified != null && previousModified.isNotEmpty()) { observers = applyObservers globalModified = previousModified } } else { val globalSnapshot = globalSnapshot val result = innerApplyLocked( nextSnapshotId, modified, optimisticMerges, openSnapshots.clear(globalSnapshot.snapshotId), ) if (result != SnapshotApplyResult.Success) return result closeLocked() // Take a new global snapshot that includes this one. val previousModified = globalSnapshot.modified resetGlobalSnapshotLocked(globalSnapshot, emptyLambda) this.modified = null globalSnapshot.modified = null observers = applyObservers globalModified = previousModified } } // Mark as applied applied = true // Notify any apply observers that changes applied were seen if (globalModified != null) { val nonNullGlobalModified = globalModified!!.wrapIntoSet() if (nonNullGlobalModified.isNotEmpty()) { verboseTrace("Compose:applyObservers") { observers.fastForEach { it(nonNullGlobalModified, this) } } } } if (modified != null && modified.isNotEmpty()) { val modifiedSet = modified.wrapIntoSet() verboseTrace("Compose:applyObservers") { observers.fastForEach { it(modifiedSet, this) } } } dispatchObserverOnApplied(this, modified) // Wait to release pinned snapshots until after running observers. // This permits observers to safely take a nested snapshot of the one that was just applied // before unpinning records that need to be retained in this case. sync { releasePinnedSnapshotsForCloseLocked() checkAndOverwriteUnusedRecordsLocked() globalModified?.forEach { processForUnusedRecordsLocked(it) } modified?.forEach { processForUnusedRecordsLocked(it) } merged?.fastForEach { processForUnusedRecordsLocked(it) } merged = null } return SnapshotApplyResult.Success } override val readOnly: Boolean get() = false override val root: Snapshot get() = this override fun dispose() { if (!disposed) { super.dispose() nestedDeactivated(this) dispatchObserverOnPreDispose(this) } } @OptIn(ExperimentalComposeRuntimeApi::class) override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot { validateNotDisposed() validateNotAppliedOrPinned() val previousId = snapshotId return creatingSnapshot( if (this is GlobalSnapshot) null else this, readObserver = readObserver, writeObserver = null, readonly = true, ) { actualReadObserver, _ -> advance { sync { val readonlyId = nextSnapshotId.also { nextSnapshotId += 1 } openSnapshots = openSnapshots.set(readonlyId) NestedReadonlySnapshot( snapshotId = readonlyId, invalid = invalid.addRange(previousId + 1, readonlyId), readObserver = mergedReadObserver(actualReadObserver, this.readObserver), parent = this, ) } } } } override fun nestedActivated(snapshot: Snapshot) { snapshots++ } override fun nestedDeactivated(snapshot: Snapshot) { requirePrecondition(snapshots > 0) { "no pending nested snapshots" } if (--snapshots == 0) { if (!applied) { abandon() } } } override fun notifyObjectsInitialized() { if (applied || disposed) return advance() } override fun closeLocked() { // Remove itself and previous ids from the open set. openSnapshots = openSnapshots.clear(snapshotId).andNot(previousIds) } override fun releasePinnedSnapshotsForCloseLocked() { releasePreviouslyPinnedSnapshotsLocked() super.releasePinnedSnapshotsForCloseLocked() } private fun validateNotApplied() { checkPrecondition(!applied) { "Unsupported operation on a snapshot that has been applied" } } private fun validateNotAppliedOrPinned() { checkPrecondition(!applied || isPinned) { "Unsupported operation on a disposed or applied snapshot" } } /** * Abandon the snapshot. This does NOT [closeAndReleasePinning], which must be done as an * additional step by callers. */ private fun abandon() { val modified = modified if (modified != null) { validateNotApplied() // Mark all state records created in this snapshot as invalid. This allows the snapshot // id to be forgotten as no state records will refer to it. this.modified = null val id = snapshotId modified.forEach { state -> var current: StateRecord? = state.firstStateRecord while (current != null) { if (current.snapshotId == id || current.snapshotId in previousIds) { current.snapshotId = INVALID_SNAPSHOT } current = current.next } } } // The snapshot can now be closed. closeAndReleasePinning() } internal fun innerApplyLocked( nextId: SnapshotId, modified: MutableScatterSet, optimisticMerges: Map?, invalidSnapshots: SnapshotIdSet, ): SnapshotApplyResult { // This must be called in a synchronized block // If there are modifications, we need to ensure none of the them have collisions. // A record is guaranteed not to collide if no other write was performed to it since this // snapshot was taken. No writes to a state object occurred if, ignoring this snapshot, // the readable records for the snapshots remain the same. If they are different then // there is a potential collision, and the state object is asked if it can resolve // it. If it can, the updated state record is used for the apply. // Determining if there is a collision and resolving it requires finding: // 1) the applying record state (i.e., the record being applied by this snapshot) // 2) the current record state (i.e., the record seen by the global snapshot) // 3) the previous state record (i.e., the record originally copied) // The applying record is the readable record this snapshot observes. It is found by calling // readable with this snapshot's ignore set and id (which what the state object also does). // The current record can be found by asking what would the next snapshot observes. This is // found by calling readable with the invalidSnapshots (the set of all currently open // snapshots) which is what the next snapshot would have in its invalid, and nextId, which // is the id the next snapshot will have. // The previous record can be found by looking for the record that this snapshot would // observe had it not modified it. This is done by excluding the snapshot itself from // invalid (an all previous ids the snapshot had because it advanced) while still using its // id to find the record. // Once these records are found, a record is in a merge conflict if both the applying record // and the current record have a different record and neither of them is the previous // record. // If the record is not changed outside this snapshot (the most likely scenario), then // there is no conflict, and there is no reason to determine the applying record. For this // reason, the current and previous are determined first the applied record is only // determined if there is a conflict (as it is assumed applied record is different from the // previous record since this code would not execute if they were equal). // A state object's mutation policy controls how conflicts are resolved. By default, all // conflicts cannot be resolved and the snapshot will not be applied. However, given the // previous, current, and next records, sometimes conflicts can be resolved (e.g. similar to // merge conflicts in a git commit) and, if so, a new value can be provided by the mutation // policy that merges the changes. If all changed objects can be merged, then the snapshot // will apply but with the new, merged values (e.g., conflict-free data types are an example // of types that can be merged). var mergedRecords: MutableList>? = null val start = this.invalid.set(this.snapshotId).or(this.previousIds) var statesToRemove: MutableList? = null modified.forEach { state -> val first = state.firstStateRecord // If either current or previous cannot be calculated the object was created // in a nested snapshot that was committed then changed. val current = readable(first, nextId, invalidSnapshots) ?: return@forEach val previous = readable(first, this.snapshotId, start) ?: return@forEach if (previous.snapshotId == PreexistingSnapshotId.toSnapshotId()) { // A previous record might not be found if the state object was created in a // nested snapshot that didn't have any other modifications. The `apply()` for // a nested snapshot considers such snapshots no-op snapshots and just closes them // which allows this object's previous record to be missing or be the record created // during initial construction. In these cases taking applied is the right choice // this indicates there was no conflicting writes. return@forEach } if (current != previous) { val applied = readable(first, this.snapshotId, this.invalid) ?: readError() val merged = optimisticMerges?.get(current) ?: run { state.mergeRecords(previous, current, applied) } when (merged) { null -> return SnapshotApplyResult.Failure(this) applied -> { // Nothing to do the merge policy says that the current changes // obscure the current value so ignore the conflict } current -> { (mergedRecords ?: mutableListOf>().also { mergedRecords = it }) .add(state to current.create(snapshotId)) // If we revert to current then the state is no longer modified. (statesToRemove ?: mutableListOf().also { statesToRemove = it }) .add(state) } else -> { (mergedRecords ?: mutableListOf>().also { mergedRecords = it }) .add( if (merged != previous) state to merged else state to previous.create(snapshotId) ) } } } } mergedRecords?.let { // Ensure we have a new snapshot id advance() // Update all the merged records to have the new id. it.fastForEach { merged -> val (state, stateRecord) = merged stateRecord.snapshotId = nextId sync { stateRecord.next = state.firstStateRecord state.prependStateRecord(stateRecord) } } } statesToRemove?.let { list -> list.fastForEach { modified.remove(it) } val mergedList = merged merged = if (mergedList == null) list else mergedList + list } return SnapshotApplyResult.Success } internal inline fun advance(block: () -> T): T { recordPrevious(snapshotId) return block().also { // Only advance this snapshot if it's possible for it to be applied later, // otherwise we don't need to bother. // This simplifies tracking of open snapshots when an apply observer takes // a nested snapshot of the snapshot that was just applied. if (!applied && !disposed) { val previousId = snapshotId sync { snapshotId = nextSnapshotId.also { nextSnapshotId += 1 } openSnapshots = openSnapshots.set(snapshotId) } invalid = invalid.addRange(previousId + 1, snapshotId) } } } internal fun advance(): Unit = advance {} internal fun recordPrevious(id: SnapshotId) { sync { previousIds = previousIds.set(id) } } internal fun recordPreviousPinnedSnapshot(id: Int) { if (id >= 0) previousPinnedSnapshots += id } internal fun recordPreviousPinnedSnapshots(handles: IntArray) { // Avoid unnecessary copies implied by the `+` below. if (handles.isEmpty()) return val pinned = previousPinnedSnapshots previousPinnedSnapshots = if (pinned.isEmpty()) handles else pinned + handles } private fun releasePreviouslyPinnedSnapshotsLocked() { for (index in previousPinnedSnapshots.indices) { releasePinningLocked(previousPinnedSnapshots[index]) } } internal fun recordPreviousList(snapshots: SnapshotIdSet) { sync { previousIds = previousIds.or(snapshots) } } override fun recordModified(state: StateObject) { (modified ?: mutableScatterSetOf().also { modified = it }).add(state) } override var writeCount: Int = 0 override var modified: MutableScatterSet? = null internal var merged: List? = null /** * A set of the id's previously associated with this snapshot. When this snapshot closes then * these ids must be removed from the global as well. */ internal var previousIds: SnapshotIdSet = SnapshotIdSet.EMPTY /** A list of the pinned snapshots handles that must be released by this snapshot */ internal var previousPinnedSnapshots: IntArray = EmptyIntArray /** * The number of pending nested snapshots of this snapshot. To simplify the code, this snapshot * it, itself, counted as its own nested snapshot. */ private var snapshots = 1 /** Tracks whether the snapshot has been applied. */ internal var applied = false private companion object { private val EmptyIntArray = IntArray(0) } } /** * The result of a applying a mutable snapshot. [Success] indicates that the snapshot was * successfully applied and is now visible as the global state of the state object (or visible in * the parent snapshot for a nested snapshot). [Failure] indicates one or more state objects were * modified by both this snapshot and in the global (or parent) snapshot, and the changes from this * snapshot are **not** visible in the global or parent snapshot. */ public sealed class SnapshotApplyResult { /** * Check the result of an apply. If the result is [Success] then this does does nothing. If the * result is [Failure] then a [SnapshotApplyConflictException] exception is thrown. Once [check] * as been called the snapshot is disposed. */ public abstract fun check() /** True if the result is [Success]. */ public abstract val succeeded: Boolean public object Success : SnapshotApplyResult() { /** * Check the result of a snapshot apply. Calling [check] on a [Success] result is a noop. */ override fun check() {} override val succeeded: Boolean get() = true } public class Failure(public val snapshot: Snapshot) : SnapshotApplyResult() { /** * Check the result of a snapshot apply. Calling [check] on a [Failure] result throws a * [SnapshotApplyConflictException] exception. */ override fun check() { snapshot.dispose() throw SnapshotApplyConflictException(snapshot) } override val succeeded: Boolean get() = false } } /** * The type returned by observer registration methods that unregisters the observer when it is * disposed. */ @Suppress("CallbackName") public fun interface ObserverHandle { /** Dispose the observer causing it to be unregistered from the snapshot system. */ public fun dispose() } /** * Return the thread's active snapshot. If no thread snapshot is active then the current global * snapshot is used. */ internal fun currentSnapshot(): Snapshot = threadSnapshot.get() ?: globalSnapshot /** * An exception that is thrown when [SnapshotApplyResult.check] is called on a result of a * [MutableSnapshot.apply] that fails to apply. */ public class SnapshotApplyConflictException(@Suppress("unused") public val snapshot: Snapshot) : Exception() /** Snapshot local value of a state object. */ public abstract class StateRecord( /** The snapshot id of the snapshot in which the record was created. */ internal var snapshotId: SnapshotId ) { public constructor() : this(currentSnapshot().snapshotId) @Deprecated("Use snapshotId: Long constructor instead") public constructor(id: Int) : this(id.toSnapshotId()) /** * Reference of the next state record. State records are stored in a linked list. * * Changes to [next] must preserve all existing records to all threads even during * intermediately changes. For example, it is safe to add the beginning or end of the list but * adding to the middle requires care. First the new record must have its [next] updated then * the [next] of its new predecessor can then be set to point to it. This implies that records * that are already in the list cannot be moved in the list as this the change must be atomic to * all threads that cannot happen without a lock which this list cannot afford. * * It is unsafe to remove a record as it might be in the process of being reused (see * [usedLocked]). If a record is removed care must be taken to ensure that it is not being * claimed by some other thread. This would require changes to [usedLocked]. */ internal var next: StateRecord? = null /** Copy the value into this state record from another for the same state object. */ public abstract fun assign(value: StateRecord) /** * Create a new state record for the same state object. Consider also implementing the [create] * overload that provides snapshotId for faster record construction when snapshot id is known. */ public abstract fun create(): StateRecord /** * Create a new state record for the same state object and provided [snapshotId]. This allows to * implement an optimized version of [create] to avoid accessing [currentSnapshot] when snapshot * id is known. The default implementation provides a backwards compatible behavior, and should * be overridden if [StateRecord] subclass supports this optimization. */ @Deprecated("Use snapshotId: Long version instead", level = DeprecationLevel.HIDDEN) public open fun create(snapshotId: Int): StateRecord = create().also { it.snapshotId = snapshotId.toSnapshotId() } /** * Create a new state record for the same state object and provided [snapshotId]. This allows to * implement an optimized version of [create] to avoid accessing [currentSnapshot] when snapshot * id is known. The default implementation provides a backwards compatible behavior, and should * be overridden if [StateRecord] subclass supports this optimization. */ public open fun create(snapshotId: SnapshotId): StateRecord = create().also { it.snapshotId = snapshotId } } /** * Interface implemented by all snapshot aware state objects. Used by this module to maintain the * state records of a state object. */ @JvmDefaultWithCompatibility public interface StateObject { /** The first state record in a linked list of state records. */ public val firstStateRecord: StateRecord /** * Add a new state record to the beginning of a list. After this call [firstStateRecord] should * be [value]. */ public fun prependStateRecord(value: StateRecord) /** * Produce a merged state based on the conflicting state changes. * * This method must not modify any of the records received and should treat the state records as * immutable, even the [applied] record. * * @param previous the state record that was used to create the [applied] record and is a state * that also (though indirectly) produced the [current] record. * @param current the state record of the parent snapshot or global state. * @param applied the state record that is being applied of the parent snapshot or global state. * @return the modified state or `null` if the values cannot be merged. If the states cannot be * merged the current apply will fail. Any of the parameters can be returned as a result. If * it is not one of the parameter values then it *must* be a new value that is created by * calling [StateRecord.create] on one of the records passed and then can be modified to have * the merged value before being returned. If a new record is returned [MutableSnapshot.apply] * will update the internal snapshot id and call [prependStateRecord] if the record is used. */ public fun mergeRecords( previous: StateRecord, current: StateRecord, applied: StateRecord, ): StateRecord? = null } /** * A snapshot whose state objects cannot be modified. If a state object is modified when in a * read-only snapshot a [IllegalStateException] is thrown. */ internal class ReadonlySnapshot internal constructor( snapshotId: SnapshotId, invalid: SnapshotIdSet, override val readObserver: ((Any) -> Unit)?, ) : Snapshot(snapshotId, invalid) { /** * The number of nested snapshots that are active. To simplify the code, this snapshot counts * itself as a nested snapshot. */ private var snapshots = 1 override val readOnly: Boolean get() = true override val root: Snapshot get() = this override fun hasPendingChanges(): Boolean = false override val writeObserver: ((Any) -> Unit)? get() = null override var modified: MutableScatterSet? get() = null @Suppress("UNUSED_PARAMETER") set(value) = unsupported() @OptIn(ExperimentalComposeRuntimeApi::class) override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot { validateOpen(this) return creatingSnapshot( parent = this, readObserver = readObserver, writeObserver = null, readonly = true, ) { actualReadObserver, _ -> NestedReadonlySnapshot( snapshotId = snapshotId, invalid = invalid, readObserver = mergedReadObserver(actualReadObserver, this.readObserver), parent = this, ) } } override fun notifyObjectsInitialized() { // Nothing to do for read-only snapshots } override fun dispose() { if (!disposed) { nestedDeactivated(this) super.dispose() dispatchObserverOnPreDispose(this) } } override fun nestedActivated(snapshot: Snapshot) { snapshots++ } override fun nestedDeactivated(snapshot: Snapshot) { if (--snapshots == 0) { // A read-only snapshot can be just be closed as it has no modifications. closeAndReleasePinning() } } override fun recordModified(state: StateObject) { reportReadonlySnapshotWrite() } } internal class NestedReadonlySnapshot( snapshotId: SnapshotId, invalid: SnapshotIdSet, override val readObserver: ((Any) -> Unit)?, val parent: Snapshot, ) : Snapshot(snapshotId, invalid) { init { parent.nestedActivated(this) } override val readOnly get() = true override val root: Snapshot get() = parent.root @OptIn(ExperimentalComposeRuntimeApi::class) override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?) = creatingSnapshot( parent = this, readObserver = readObserver, writeObserver = null, readonly = true, ) { actualReadObserver, _ -> NestedReadonlySnapshot( snapshotId = snapshotId, invalid = invalid, readObserver = mergedReadObserver(actualReadObserver, this.readObserver), parent = parent, ) } override fun notifyObjectsInitialized() { // Nothing to do for read-only snapshots } override fun hasPendingChanges(): Boolean = false override fun dispose() { if (!disposed) { if (snapshotId != parent.snapshotId) { closeAndReleasePinning() } parent.nestedDeactivated(this) super.dispose() dispatchObserverOnPreDispose(this) } } override val modified: MutableScatterSet? get() = null override val writeObserver: ((Any) -> Unit)? get() = null override fun recordModified(state: StateObject) = reportReadonlySnapshotWrite() override fun nestedDeactivated(snapshot: Snapshot) = unsupported() override fun nestedActivated(snapshot: Snapshot) = unsupported() } private val emptyLambda: (invalid: SnapshotIdSet) -> Unit = {} /** * A snapshot object that simplifies the code by treating the global state as a mutable snapshot. */ internal class GlobalSnapshot(snapshotId: SnapshotId, invalid: SnapshotIdSet) : MutableSnapshot( snapshotId, invalid, null, { state -> sync { globalWriteObservers.fastForEach { it(state) } } }, ) { @OptIn(ExperimentalComposeRuntimeApi::class) override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot = creatingSnapshot( parent = null, readonly = true, readObserver = readObserver, writeObserver = null, ) { actualReadObserver, _ -> takeNewSnapshot { invalid -> ReadonlySnapshot( snapshotId = sync { nextSnapshotId.also { nextSnapshotId += 1 } }, invalid = invalid, readObserver = actualReadObserver, ) } } @OptIn(ExperimentalComposeRuntimeApi::class) override fun takeNestedMutableSnapshot( readObserver: ((Any) -> Unit)?, writeObserver: ((Any) -> Unit)?, ): MutableSnapshot = creatingSnapshot( parent = null, readonly = false, readObserver = readObserver, writeObserver = writeObserver, ) { actualReadObserver, actualWriteObserver -> takeNewSnapshot { invalid -> MutableSnapshot( snapshotId = sync { nextSnapshotId.also { nextSnapshotId += 1 } }, invalid = invalid, // It is intentional that the global read observers are not merged with mutable // snapshots read observers. readObserver = actualReadObserver, // It is intentional that global write observers are not merged with mutable // snapshots write observers. writeObserver = actualWriteObserver, ) } } override fun notifyObjectsInitialized() { advanceGlobalSnapshot() } override fun nestedDeactivated(snapshot: Snapshot) = unsupported() override fun nestedActivated(snapshot: Snapshot) = unsupported() override fun apply(): SnapshotApplyResult = error("Cannot apply the global snapshot directly. Call Snapshot.advanceGlobalSnapshot") override fun dispose() { sync { releasePinnedSnapshotLocked() } } } /** A nested mutable snapshot created by [MutableSnapshot.takeNestedMutableSnapshot]. */ internal class NestedMutableSnapshot( snapshotId: SnapshotId, invalid: SnapshotIdSet, readObserver: ((Any) -> Unit)?, writeObserver: ((Any) -> Unit)?, val parent: MutableSnapshot, ) : MutableSnapshot(snapshotId, invalid, readObserver, writeObserver) { private var deactivated = false init { parent.nestedActivated(this) } override val root: Snapshot get() = parent.root override fun dispose() { if (!disposed) { super.dispose() deactivate() } } override fun apply(): SnapshotApplyResult { if (parent.applied || parent.disposed) return SnapshotApplyResult.Failure(this) // Applying a nested mutable snapshot applies its changes to the parent snapshot. // See MutableSnapshot.apply() for implantation notes. // The apply observer notification are for applying to the global scope so it is elided // here making this code a bit simpler than MutableSnapshot.apply. val modified = modified val id = snapshotId val optimisticMerges = if (modified != null) optimisticMerges(parent.snapshotId, this, parent.invalid) else null sync { validateOpen(this) if (modified == null || modified.size == 0) { closeAndReleasePinning() } else { val result = innerApplyLocked(parent.snapshotId, modified, optimisticMerges, parent.invalid) if (result != SnapshotApplyResult.Success) return result parent.modified?.apply { addAll(modified) } ?: modified.also { // Ensure modified reference is only used by one snapshot parent.modified = it this.modified = null } } // Ensure the parent is newer than the current snapshot if (parent.snapshotId < id) { parent.advance() } // Make the snapshot visible in the parent snapshot parent.invalid = parent.invalid.clear(id).andNot(previousIds) // Ensure the ids associated with this snapshot are also applied by the parent. parent.recordPrevious(id) parent.recordPreviousPinnedSnapshot(takeoverPinnedSnapshot()) parent.recordPreviousList(previousIds) parent.recordPreviousPinnedSnapshots(previousPinnedSnapshots) } applied = true deactivate() dispatchObserverOnApplied(this, modified) return SnapshotApplyResult.Success } private fun deactivate() { if (!deactivated) { deactivated = true parent.nestedDeactivated(this) } } } /** A pseudo snapshot that doesn't introduce isolation but does introduce observers. */ internal class TransparentObserverMutableSnapshot( private val parentSnapshot: MutableSnapshot?, specifiedReadObserver: ((Any) -> Unit)?, specifiedWriteObserver: ((Any) -> Unit)?, private val mergeParentObservers: Boolean, private val ownsParentSnapshot: Boolean, ) : MutableSnapshot( INVALID_SNAPSHOT, SnapshotIdSet.EMPTY, mergedReadObserver( specifiedReadObserver, parentSnapshot?.readObserver ?: globalSnapshot.readObserver, mergeParentObservers, ), mergedWriteObserver( specifiedWriteObserver, parentSnapshot?.writeObserver ?: globalSnapshot.writeObserver, ), ) { override var readObserver: ((Any) -> Unit)? = super.readObserver override var writeObserver: ((Any) -> Unit)? = super.writeObserver internal val threadId: Long = currentThreadId() private val currentSnapshot: MutableSnapshot get() = parentSnapshot ?: globalSnapshot override fun dispose() { // Explicitly don't call super.dispose() disposed = true if (ownsParentSnapshot) { parentSnapshot?.dispose() } } override var snapshotId: SnapshotId get() = currentSnapshot.snapshotId @Suppress("UNUSED_PARAMETER") set(value) { unsupported() } override var invalid get() = currentSnapshot.invalid @Suppress("UNUSED_PARAMETER") set(value) = unsupported() override fun hasPendingChanges(): Boolean = currentSnapshot.hasPendingChanges() override var modified: MutableScatterSet? get() = currentSnapshot.modified @Suppress("UNUSED_PARAMETER") set(value) = unsupported() override var writeCount: Int get() = currentSnapshot.writeCount set(value) { currentSnapshot.writeCount = value } override val readOnly: Boolean get() = currentSnapshot.readOnly override fun apply(): SnapshotApplyResult = currentSnapshot.apply() override fun recordModified(state: StateObject) = currentSnapshot.recordModified(state) override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot { val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver) return if (!mergeParentObservers) { createTransparentSnapshotWithNoParentReadObserver( previousSnapshot = currentSnapshot.takeNestedSnapshot(null), readObserver = mergedReadObserver, ownsPreviousSnapshot = true, ) } else { currentSnapshot.takeNestedSnapshot(mergedReadObserver) } } override fun takeNestedMutableSnapshot( readObserver: ((Any) -> Unit)?, writeObserver: ((Any) -> Unit)?, ): MutableSnapshot { val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver) val mergedWriteObserver = mergedWriteObserver(writeObserver, this.writeObserver) return if (!mergeParentObservers) { val nestedSnapshot = currentSnapshot.takeNestedMutableSnapshot( readObserver = null, writeObserver = mergedWriteObserver, ) TransparentObserverMutableSnapshot( parentSnapshot = nestedSnapshot, specifiedReadObserver = mergedReadObserver, specifiedWriteObserver = mergedWriteObserver, mergeParentObservers = false, ownsParentSnapshot = true, ) } else { currentSnapshot.takeNestedMutableSnapshot(mergedReadObserver, mergedWriteObserver) } } override fun notifyObjectsInitialized() = currentSnapshot.notifyObjectsInitialized() /** Should never be called. */ override fun nestedActivated(snapshot: Snapshot) = unsupported() override fun nestedDeactivated(snapshot: Snapshot) = unsupported() } /** A pseudo snapshot that doesn't introduce isolation but does introduce observers. */ internal class TransparentObserverSnapshot( private val parentSnapshot: Snapshot?, specifiedReadObserver: ((Any) -> Unit)?, private val mergeParentObservers: Boolean, private val ownsParentSnapshot: Boolean, ) : Snapshot(INVALID_SNAPSHOT, SnapshotIdSet.EMPTY) { override var readObserver: ((Any) -> Unit)? = mergedReadObserver( specifiedReadObserver, parentSnapshot?.readObserver ?: globalSnapshot.readObserver, mergeParentObservers, ) override val writeObserver: ((Any) -> Unit)? = null internal val threadId: Long = currentThreadId() override val root: Snapshot = this private val currentSnapshot: Snapshot get() = parentSnapshot ?: globalSnapshot override fun dispose() { // Explicitly don't call super.dispose() disposed = true if (ownsParentSnapshot) { parentSnapshot?.dispose() } } override var snapshotId: SnapshotId get() = currentSnapshot.snapshotId @Suppress("UNUSED_PARAMETER") set(value) { unsupported() } override var invalid get() = currentSnapshot.invalid @Suppress("UNUSED_PARAMETER") set(value) = unsupported() override fun hasPendingChanges(): Boolean = currentSnapshot.hasPendingChanges() override var modified: MutableScatterSet? get() = currentSnapshot.modified @Suppress("UNUSED_PARAMETER") set(value) = unsupported() override val readOnly: Boolean get() = currentSnapshot.readOnly override fun recordModified(state: StateObject) = currentSnapshot.recordModified(state) override fun takeNestedSnapshot(readObserver: ((Any) -> Unit)?): Snapshot { val mergedReadObserver = mergedReadObserver(readObserver, this.readObserver) return if (!mergeParentObservers) { createTransparentSnapshotWithNoParentReadObserver( currentSnapshot.takeNestedSnapshot(null), mergedReadObserver, ownsPreviousSnapshot = true, ) } else { currentSnapshot.takeNestedSnapshot(mergedReadObserver) } } override fun notifyObjectsInitialized() = currentSnapshot.notifyObjectsInitialized() /** Should never be called. */ override fun nestedActivated(snapshot: Snapshot) = unsupported() override fun nestedDeactivated(snapshot: Snapshot) = unsupported() } private fun createTransparentSnapshotWithNoParentReadObserver( previousSnapshot: Snapshot?, readObserver: ((Any) -> Unit)? = null, ownsPreviousSnapshot: Boolean = false, ): Snapshot = if (previousSnapshot is MutableSnapshot || previousSnapshot == null) { TransparentObserverMutableSnapshot( parentSnapshot = previousSnapshot as? MutableSnapshot, specifiedReadObserver = readObserver, specifiedWriteObserver = null, mergeParentObservers = false, ownsParentSnapshot = ownsPreviousSnapshot, ) } else { TransparentObserverSnapshot( parentSnapshot = previousSnapshot, specifiedReadObserver = readObserver, mergeParentObservers = false, ownsParentSnapshot = ownsPreviousSnapshot, ) } internal fun mergedReadObserver( readObserver: ((Any) -> Unit)?, parentObserver: ((Any) -> Unit)?, mergeReadObserver: Boolean = true, ): ((Any) -> Unit)? { @Suppress("NAME_SHADOWING") val parentObserver = if (mergeReadObserver) parentObserver else null return if (readObserver != null && parentObserver != null && readObserver !== parentObserver) { { state: Any -> readObserver(state) parentObserver(state) } } else readObserver ?: parentObserver } internal fun mergedWriteObserver( writeObserver: ((Any) -> Unit)?, parentObserver: ((Any) -> Unit)?, ): ((Any) -> Unit)? = if (writeObserver != null && parentObserver != null && writeObserver !== parentObserver) { { state: Any -> writeObserver(state) parentObserver(state) } } else writeObserver ?: parentObserver /** * Snapshot id of `0` is reserved as invalid and no state record with snapshot `0` is considered * valid. * * The value `0` was chosen as it is the default value of the Int snapshot id type and records * initially created will naturally have a snapshot id of 0. If this wasn't considered invalid * adding such a record to a state object will make the state record immediately visible to the * snapshots instead of being born invalid. Using `0` ensures all state records are created invalid * and must be explicitly marked as valid in to be visible in a snapshot. */ private val INVALID_SNAPSHOT = SnapshotIdZero /** Current thread snapshot */ private val threadSnapshot = SnapshotThreadLocal() /** * A global synchronization object. This synchronization object should be taken before modifying any * of the fields below. */ @PublishedApi internal val lock: SynchronizedObject = makeSynchronizedObject() @Suppress("BanInlineOptIn", "LEAKED_IN_PLACE_LAMBDA", "WRONG_INVOCATION_KIND") @OptIn(ExperimentalContracts::class) @PublishedApi internal inline fun sync(block: () -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return synchronized(lock, block) } // The following variables should only be written when sync is taken /** * A set of snapshots that are currently open and should be considered invalid for new snapshots. */ private var openSnapshots = SnapshotIdSet.EMPTY /** The first snapshot created must be at least on more than the [Snapshot.PreexistingSnapshotId] */ private var nextSnapshotId = Snapshot.PreexistingSnapshotId.toSnapshotId() + 1 /** * A tracking table for pinned snapshots. A pinned snapshot is the lowest snapshot id that the * snapshot is ignoring by considering them invalid. This is used to calculate when a snapshot * record can be reused. */ private val pinningTable = SnapshotDoubleIndexHeap() /** * The set of objects who have more than one active state record. These are traversed during apply * of mutable snapshots and when the global snapshot is advanced to determine if any of the records * can be cleared. */ private val extraStateObjects = SnapshotWeakSet() /** A list of apply observers */ private var applyObservers = emptyList<(Set, Snapshot) -> Unit>() /** A list of observers of writes to the global state. */ private var globalWriteObservers = emptyList<(Any) -> Unit>() private val globalSnapshot = GlobalSnapshot( snapshotId = nextSnapshotId.also { nextSnapshotId += 1 }, invalid = SnapshotIdSet.EMPTY, ) .also { openSnapshots = openSnapshots.set(it.snapshotId) } // Unused, kept for API compat @Suppress("unused") @PublishedApi internal val snapshotInitializer: Snapshot = globalSnapshot private fun resetGlobalSnapshotLocked( globalSnapshot: GlobalSnapshot, block: (invalid: SnapshotIdSet) -> T, ): T { val snapshotId = globalSnapshot.snapshotId val result = block(openSnapshots.clear(snapshotId)) val nextGlobalSnapshotId = nextSnapshotId nextSnapshotId += 1 openSnapshots = openSnapshots.clear(snapshotId) globalSnapshot.snapshotId = nextGlobalSnapshotId globalSnapshot.invalid = openSnapshots globalSnapshot.writeCount = 0 globalSnapshot.modified = null globalSnapshot.releasePinnedSnapshotLocked() openSnapshots = openSnapshots.set(nextGlobalSnapshotId) return result } /** * Counts the number of threads currently inside `advanceGlobalSnapshot`, notifying observers of * changes to the global snapshot. */ private var pendingApplyObserverCount = AtomicInt(0) private fun advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T { val globalSnapshot = globalSnapshot val modified: MutableScatterSet? val result = sync { modified = globalSnapshot.modified if (modified != null) { pendingApplyObserverCount.add(1) } resetGlobalSnapshotLocked(globalSnapshot, block) } // If the previous global snapshot had any modified states then notify the registered apply // observers. modified?.let { try { val observers = applyObservers val modifiedSet = it.wrapIntoSet() verboseTrace("Compose:applyObservers") { observers.fastForEach { observer -> observer(modifiedSet, globalSnapshot) } } } finally { pendingApplyObserverCount.add(-1) } } sync { checkAndOverwriteUnusedRecordsLocked() modified?.forEach { processForUnusedRecordsLocked(it) } } return result } private fun advanceGlobalSnapshot() = advanceGlobalSnapshot(emptyLambda) private fun takeNewSnapshot(block: (invalid: SnapshotIdSet) -> T): T = advanceGlobalSnapshot { invalid -> val result = block(invalid) sync { openSnapshots = openSnapshots.set(result.snapshotId) } result } private fun validateOpen(snapshot: Snapshot) { val openSnapshots = openSnapshots if (!openSnapshots.get(snapshot.snapshotId)) { error( "Snapshot is not open: snapshotId=${ snapshot.snapshotId }, disposed=${ snapshot.disposed }, applied=${ (snapshot as? MutableSnapshot)?.applied ?: "read-only" }, lowestPin=${ sync { pinningTable.lowestOrDefault(SnapshotIdInvalidValue) } }" ) } } /** * A candidate snapshot is valid if the it is less than or equal to the current snapshot and it * wasn't specifically marked as invalid when the snapshot started. * * All snapshot active at when the snapshot was taken considered invalid for the snapshot (they have * not been applied and therefore are considered invalid). * * All snapshots taken after the current snapshot are considered invalid since they where taken * after the current snapshot was taken. * * INVALID_SNAPSHOT is reserved as an invalid snapshot id. */ private fun valid( currentSnapshot: SnapshotId, candidateSnapshot: SnapshotId, invalid: SnapshotIdSet, ): Boolean { return candidateSnapshot != INVALID_SNAPSHOT && candidateSnapshot <= currentSnapshot && !invalid.get(candidateSnapshot) } // Determine if the given data is valid for the snapshot. private fun valid(data: StateRecord, snapshot: SnapshotId, invalid: SnapshotIdSet): Boolean { return valid(snapshot, data.snapshotId, invalid) } private fun readable(r: T, id: SnapshotId, invalid: SnapshotIdSet): T? { // The readable record is the valid record with the highest snapshotId var current: StateRecord? = r var candidate: StateRecord? = null while (current != null) { if (valid(current, id, invalid)) { candidate = if (candidate == null) current else if (candidate.snapshotId < current.snapshotId) current else candidate } current = current.next } if (candidate != null) { @Suppress("UNCHECKED_CAST") return candidate as T } return null } /** * Return the current readable state record for the current snapshot. It is assumed that [this] is * the first record of [state] */ public fun T.readable(state: StateObject): T { val snapshot = Snapshot.current snapshot.readObserver?.invoke(state) return readable(this, snapshot.snapshotId, snapshot.invalid) ?: sync { // Readable can return null when the global snapshot has been advanced by another thread // and state written to the object was overwritten while this thread was paused. // Repeating the read is valid here as either this will return the same result as // the previous call or will find a valid record. Being in a sync block prevents other // threads from writing to this state object until the read completes. val syncSnapshot = Snapshot.current @Suppress("UNCHECKED_CAST") readable(state.firstStateRecord as T, syncSnapshot.snapshotId, syncSnapshot.invalid) ?: readError() } } // unused, still here for API compat. /** * Return the current readable state record for the [snapshot]. It is assumed that [this] is the * first record of [state] */ public fun T.readable(state: StateObject, snapshot: Snapshot): T { // invoke the observer associated with the current snapshot. snapshot.readObserver?.invoke(state) return readable(this, snapshot.snapshotId, snapshot.invalid) ?: sync { // Readable can return null when the global snapshot has been advanced by another thread // See T.readable(state: StateObject) for more info. val syncSnapshot = Snapshot.current @Suppress("UNCHECKED_CAST") readable(state.firstStateRecord as T, syncSnapshot.snapshotId, syncSnapshot.invalid) ?: readError() } } private fun readError(): Nothing { error( "Reading a state that was created after the snapshot was taken or in a snapshot that " + "has not yet been applied" ) } /** * A record can be reused if no other snapshot will see it as valid. This is always true for a * record created in an abandoned snapshot. It is also true if the record is valid in the previous * snapshot and is obscured by another record also valid in the previous state record. */ private fun usedLocked(state: StateObject): StateRecord? { var current: StateRecord? = state.firstStateRecord var validRecord: StateRecord? = null val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) - 1 val invalid = SnapshotIdSet.EMPTY while (current != null) { val currentId = current.snapshotId if (currentId == INVALID_SNAPSHOT) { // Any records that were marked invalid by an abandoned snapshot or is marked reachable // can be used immediately. return current } if (valid(current, reuseLimit, invalid)) { if (validRecord == null) { validRecord = current } else { // If we have two valid records one must obscure the other. Return the // record with the lowest id return if (current.snapshotId < validRecord.snapshotId) current else validRecord } } current = current.next } return null } /** * Clear records that cannot be selected in any currently open snapshot. * * This method uses the same technique as [usedLocked] which uses the [pinningTable] to determine * lowest id in the invalid set for all snapshots. Only the record with the greatest id of all * records less or equal to this lowest id can possibly be selected in any snapshot and all other * records below that number can be overwritten. * * However, this technique doesn't find all records that will not be selected by any open snapshot * as a record that has an id above that number could be reusable but will not be found. * * For example if snapshot 1 is open and 2 is created and modifies [state] then is applied, 3 is * open and then 4 is open, and then 1 is applied. When 3 modifies [state] and then applies, as 1 is * pinned by 4, it is uncertain whether the record for 2 is needed by 4 so it must be kept even if 4 * also modified [state] and would not select 2. Accurately determine if a record is selectable * would require keeping a list of all open [Snapshot] instances which currently is not kept and * traversing that list for each record. * * If any such records are possible this method returns true. In other words, this method returns * true if any records might be reusable but this function could not prove there were or not. */ private fun overwriteUnusedRecordsLocked(state: StateObject): Boolean { var current: StateRecord? = state.firstStateRecord var overwriteRecord: StateRecord? = null var validRecord: StateRecord? = null val reuseLimit = pinningTable.lowestOrDefault(nextSnapshotId) var retainedRecords = 0 while (current != null) { val currentId = current.snapshotId if (currentId != INVALID_SNAPSHOT) { if (currentId < reuseLimit) { if (validRecord == null) { // If any records are below [reuseLimit] then we must keep the highest one // so the lowest snapshot can select it. validRecord = current retainedRecords++ } else { // If [validRecord] is from an earlier snapshot, overwrite it instead val recordToOverwrite = if (current.snapshotId < validRecord.snapshotId) { current } else { // We cannot use `.also { }` here as it prevents smart casting of other // uses of [validRecord]. val result = validRecord validRecord = current result } if (overwriteRecord == null) { // Find a record we will definitely keep overwriteRecord = state.firstStateRecord.findYoungestOr { it.snapshotId >= reuseLimit } } recordToOverwrite.snapshotId = INVALID_SNAPSHOT recordToOverwrite.assign(overwriteRecord) } } else { retainedRecords++ } } current = current.next } return retainedRecords > 1 } private inline fun StateRecord.findYoungestOr(predicate: (StateRecord) -> Boolean): StateRecord { var current: StateRecord? = this var youngest = this while (current != null) { if (predicate(current)) return current if (youngest.snapshotId < current.snapshotId) youngest = current current = current.next } return youngest } private fun checkAndOverwriteUnusedRecordsLocked() { extraStateObjects.removeIf { !overwriteUnusedRecordsLocked(it) } } private fun processForUnusedRecordsLocked(state: StateObject) { if (overwriteUnusedRecordsLocked(state)) { extraStateObjects.add(state) } } @PublishedApi internal fun T.writableRecord(state: StateObject, snapshot: Snapshot): T { if (snapshot.readOnly) { // If the snapshot is read-only, use the snapshot recordModified to report it. snapshot.recordModified(state) } val id = snapshot.snapshotId val readData = readable(this, id, snapshot.invalid) ?: readError() // If the readable data was born in this snapshot, it is writable. if (readData.snapshotId == snapshot.snapshotId) return readData // Otherwise, make a copy of the readable data and mark it as born in this snapshot, making it // writable. @Suppress("UNCHECKED_CAST") val newData = sync { // Verify that some other thread didn't already create this. val newReadData = readable(state.firstStateRecord, id, snapshot.invalid) ?: readError() if (newReadData.snapshotId == id) newReadData else newReadData.newWritableRecordLocked(state, snapshot) } as T if (readData.snapshotId != Snapshot.PreexistingSnapshotId.toSnapshotId()) { snapshot.recordModified(state) } return newData } internal fun T.overwritableRecord( state: StateObject, snapshot: Snapshot, candidate: T, ): T { if (snapshot.readOnly) { // If the snapshot is read-only, use the snapshot recordModified to report it. snapshot.recordModified(state) } val id = snapshot.snapshotId if (candidate.snapshotId == id) return candidate val newData = sync { newOverwritableRecordLocked(state) } newData.snapshotId = id if (candidate.snapshotId != Snapshot.PreexistingSnapshotId.toSnapshotId()) { snapshot.recordModified(state) } return newData } internal fun T.newWritableRecord(state: StateObject, snapshot: Snapshot) = sync { newWritableRecordLocked(state, snapshot) } private fun T.newWritableRecordLocked(state: StateObject, snapshot: Snapshot): T { // Calling used() on a state object might return the same record for each thread calling // used() therefore selecting the record to reuse should be guarded. // Note: setting the snapshotId to Int.MAX_VALUE will make it invalid for all snapshots. // This means the lock can be released as used() will no longer select it. Using id could // also be used but it puts the object into a state where the reused value appears to be // the current valid value for the snapshot. This is not an issue if the snapshot is only // being read from a single thread but using Int.MAX_VALUE allows multiple readers, // single writer, of a snapshot. Note that threads reading a mutating snapshot should not // cache the result of readable() as the mutating thread calls to writable() can change the // result of readable(). val newData = newOverwritableRecordLocked(state) newData.assign(this) newData.snapshotId = snapshot.snapshotId return newData } internal fun T.newOverwritableRecordLocked(state: StateObject): T { // Calling used() on a state object might return the same record for each thread calling // used() therefore selecting the record to reuse should be guarded. // Note: setting the snapshotId to Int.MAX_VALUE will make it invalid for all snapshots. // This means the lock can be released as used() will no longer select it. Using id could // also be used but it puts the object into a state where the reused value appears to be // the current valid value for the snapshot. This is not an issue if the snapshot is only // being read from a single thread but using Int.MAX_VALUE allows multiple readers, // single writer, of a snapshot. Note that threads reading a mutating snapshot should not // cache the result of readable() as the mutating thread calls to writable() can change the // result of readable(). @Suppress("UNCHECKED_CAST") return (usedLocked(state) as T?)?.apply { snapshotId = SnapshotIdMax } ?: create(SnapshotIdMax).apply { this.next = state.firstStateRecord state.prependStateRecord(this as T) } as T } @PublishedApi internal fun notifyWrite(snapshot: Snapshot, state: StateObject) { snapshot.writeCount += 1 snapshot.writeObserver?.invoke(state) } /** * Call [block] with a writable state record for [snapshot] of the given record. It is assumed that * this is called for the first state record in a state object. If the snapshot is read-only calling * this will throw. */ public inline fun T.writable( state: StateObject, snapshot: Snapshot, block: T.() -> R, ): R { // A writable record will always be the readable record (as all newer records are invalid it // must be the newest valid record). This means that if the readable record is not from the // current snapshot, a new record must be created. To create a new writable record, a record // can be reused, if possible, and the readable record is applied to it. If a record cannot // be reused, a new record is created and the readable record is applied to it. Once the // values are correct the record is made live by giving it the current snapshot id. // Writes need to be in a `sync` block as all writes in flight must be completed before a new // snapshot is take. Writing in a sync block ensures this is the case because new snapshots // are also in a sync block. return sync { this.writableRecord(state, snapshot).block() } .also { notifyWrite(snapshot, state) } } /** * Call [block] with a writable state record for the given record. It is assumed that this is called * for the first state record in a state object. A record is writable if it was created in the * current mutable snapshot. */ public inline fun T.writable(state: StateObject, block: T.() -> R): R { val snapshot: Snapshot return sync { snapshot = Snapshot.current this.writableRecord(state, snapshot).block() } .also { notifyWrite(snapshot, state) } } /** * Call [block] with a writable state record for the given record. It is assumed that this is called * for the first state record in a state object. A record is writable if it was created in the * current mutable snapshot. This should only be used when the record will be overwritten in its * entirety (such as having only one field and that field is written to). * * WARNING: If the caller doesn't overwrite all the fields in the state record the object will be * inconsistent and the fields not written are almost guaranteed to be incorrect. If it is possible * that [block] will not write to all the fields use [writable] instead. * * @param state The object that has this record in its record list. * @param candidate The current for the snapshot record returned by [withCurrent] * @param block The block that will mutate all the field of the record. */ internal inline fun T.overwritable( state: StateObject, candidate: T, block: T.() -> R, ): R { val snapshot: Snapshot return sync { snapshot = Snapshot.current this.overwritableRecord(state, snapshot, candidate).block() } .also { notifyWrite(snapshot, state) } } /** * Produce a set of optimistic merges of the state records, this is performed outside the a * synchronization block to reduce the amount of time taken in the synchronization block reducing * the thread contention of merging state values. * * How sets and ids are used to determine a merged record is explained in * [MutableSnapshot.innerApplyLocked]. * * @see MutableSnapshot.innerApplyLocked */ private fun optimisticMerges( currentSnapshotId: SnapshotId, applyingSnapshot: MutableSnapshot, invalidSnapshots: SnapshotIdSet, ): Map? { val modified = applyingSnapshot.modified if (modified == null) return null val applyingSnapshotId = applyingSnapshot.snapshotId val start = applyingSnapshot.invalid.set(applyingSnapshotId).or(applyingSnapshot.previousIds) var result: MutableMap? = null modified.forEach { state -> val first = state.firstStateRecord val current = readable(first, currentSnapshotId, invalidSnapshots) ?: return@forEach val previous = readable(first, applyingSnapshotId, start) ?: return@forEach if (current != previous) { // Try to produce a merged state record val applied = readable(first, applyingSnapshotId, applyingSnapshot.invalid) ?: readError() val merged = state.mergeRecords(previous, current, applied) if (merged != null) { (result ?: hashMapOf().also { result = it })[current] = merged } else { // If one fails don't bother calculating the others as they are likely not going // to be used. There is an unlikely case that a optimistic merge cannot be // produced but the snapshot will apply because, once the synchronization is taken, // the current state can be merge. This routine errors on the side of reduced // overall work by not performing work that is likely to be ignored. return null } } } return result } private fun reportReadonlySnapshotWrite(): Nothing { error("Cannot modify a state object in a read-only snapshot") } /** Returns the current record without notifying any read observers. */ @PublishedApi internal fun current(r: T, snapshot: Snapshot): T = readable(r, snapshot.snapshotId, snapshot.invalid) ?: sync { // Global snapshot could have been advanced // see StateRecord.readable for more details readable(r, snapshot.snapshotId, snapshot.invalid) } ?: readError() @PublishedApi internal fun current(r: T): T = Snapshot.current.let { snapshot -> readable(r, snapshot.snapshotId, snapshot.invalid) ?: sync { // Global snapshot could have been advanced // see StateRecord.readable for more details Snapshot.current.let { syncSnapshot -> readable(r, syncSnapshot.snapshotId, syncSnapshot.invalid) } } ?: readError() } /** * Provides a [block] with the current record, without notifying any read observers. * * @see readable */ public inline fun T.withCurrent(block: (r: T) -> R): R = block(current(this)) /** Helper routine to add a range of values ot a snapshot set */ internal fun SnapshotIdSet.addRange(from: SnapshotId, until: SnapshotId): SnapshotIdSet { var result = this var invalidId = from while (invalidId < until) { result = result.set(invalidId) invalidId += 1 } return result } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateList.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.runtime.snapshots import androidx.compose.runtime.Stable import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentList import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentListOf import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import androidx.compose.runtime.requirePrecondition /** * An implementation of [MutableList] that can be observed and snapshot. This is the result type * created by [androidx.compose.runtime.mutableStateListOf]. * * This class closely implements the same semantics as [ArrayList]. * * @see androidx.compose.runtime.mutableStateListOf */ @Stable public expect class SnapshotStateList internal constructor(persistentList: PersistentList) : StateObject, MutableList, RandomAccess { public constructor() override var firstStateRecord: StateRecord private set override fun prependStateRecord(value: StateRecord) /** * Return a list containing all the elements of this list. * * The list returned is immutable and returned will not change even if the content of the list * is changed in the same snapshot. It also will be the same instance until the content is * changed. It is not, however, guaranteed to be the same instance for the same list as adding * and removing the same item from the this list might produce a different instance with the * same content. * * This operation is O(1) and does not involve a physically copying the list. It instead returns * the underlying immutable list used internally to store the content of the list. * * It is recommended to use [toList] when using returning the value of this list from * [androidx.compose.runtime.snapshotFlow]. */ public fun toList(): List override val size: Int override fun contains(element: T): Boolean override fun containsAll(elements: Collection): Boolean override fun get(index: Int): T override fun indexOf(element: T): Int override fun isEmpty(): Boolean override fun iterator(): MutableIterator override fun lastIndexOf(element: T): Int override fun listIterator(): MutableListIterator override fun listIterator(index: Int): MutableListIterator override fun subList(fromIndex: Int, toIndex: Int): MutableList override fun add(element: T): Boolean override fun add(index: Int, element: T) override fun addAll(elements: Collection): Boolean override fun addAll(index: Int, elements: Collection): Boolean override fun clear() override fun remove(element: T): Boolean override fun removeAll(elements: Collection): Boolean override fun removeAt(index: Int): T override fun retainAll(elements: Collection): Boolean override fun set(index: Int, element: T): T public fun removeRange(fromIndex: Int, toIndex: Int) internal fun retainAllInRange(elements: Collection, start: Int, end: Int): Int } internal inline fun SnapshotStateList.writable( block: StateListStateRecord.() -> R ): R = @Suppress("UNCHECKED_CAST") (firstStateRecord as StateListStateRecord).writable(this, block) internal inline fun SnapshotStateList.withCurrent( block: StateListStateRecord.() -> R ): R = @Suppress("UNCHECKED_CAST") (firstStateRecord as StateListStateRecord).withCurrent(block) internal fun SnapshotStateList.mutateBoolean(block: (MutableList) -> Boolean): Boolean = mutate(block) internal inline fun SnapshotStateList.mutate(block: (MutableList) -> R): R { var result: R while (true) { var oldList: PersistentList? = null var currentModification = 0 synchronized(sync) { val current = withCurrent { this } currentModification = current.modification oldList = current.list } val builder = oldList!!.builder() result = block(builder) val newList = builder.build() if ( newList == oldList || writable { attemptUpdate(currentModification, newList, structural = true) } ) break } return result } internal inline fun SnapshotStateList.update( structural: Boolean = true, block: (PersistentList) -> PersistentList, ) { conditionalUpdate(structural, block) } @Suppress("NOTHING_TO_INLINE") internal inline fun SnapshotStateList.clearImpl() { writable { synchronized(sync) { list = persistentListOf() modification++ structuralChange++ } } } internal inline fun SnapshotStateList.conditionalUpdate( structural: Boolean = true, block: (PersistentList) -> PersistentList, ) = run { val result: Boolean while (true) { var oldList: PersistentList? = null var currentModification = 0 synchronized(sync) { val current = withCurrent { this } currentModification = current.modification oldList = current.list } val newList = block(oldList!!) if (newList == oldList) { result = false break } if (writable { attemptUpdate(currentModification, newList, structural) }) { result = true break } } result } // NOTE: do not inline this method to avoid class verification failures, see b/369909868 internal fun StateListStateRecord.attemptUpdate( currentModification: Int, newList: PersistentList, structural: Boolean, ): Boolean = synchronized(sync) { if (modification == currentModification) { list = newList if (structural) structuralChange++ modification++ true } else false } internal fun SnapshotStateList.stateRecordWith(list: PersistentList): StateRecord { val snapshot = currentSnapshot() return StateListStateRecord(snapshot.snapshotId, list).also { if (snapshot !is GlobalSnapshot) { it.next = StateListStateRecord(Snapshot.PreexistingSnapshotId.toSnapshotId(), list) } } } internal val SnapshotStateList.structure: Int get() = withCurrent { structuralChange } @Suppress("UNCHECKED_CAST") internal val SnapshotStateList.readable: StateListStateRecord get() = (firstStateRecord as StateListStateRecord).readable(this) /** * Creates a new snapshot state list with the specified [size], where each element is calculated by * calling the specified [init] function. * * The function [init] is called for each list element sequentially starting from the first one. It * should return the value for a list element given its index. */ public fun SnapshotStateList(size: Int, init: (index: Int) -> T): SnapshotStateList { if (size == 0) { return SnapshotStateList() } val builder = persistentListOf().builder() for (i in 0 until size) { builder.add(init(i)) } return SnapshotStateList(builder.build()) } /** This is an internal implementation class of [SnapshotStateList]. Do not use. */ internal class StateListStateRecord internal constructor(snapshotId: SnapshotId, internal var list: PersistentList) : StateRecord(snapshotId) { internal var modification = 0 internal var structuralChange = 0 override fun assign(value: StateRecord) { synchronized(sync) { @Suppress("UNCHECKED_CAST") list = (value as StateListStateRecord).list modification = value.modification structuralChange = value.structuralChange } } override fun create(): StateRecord = create(currentSnapshot().snapshotId) override fun create(snapshotId: SnapshotId): StateRecord = StateListStateRecord(snapshotId, list) } /** * This lock is used to ensure that the value of modification and the list in the state record, when * used together, are atomically read and written. * * A global sync object is used to avoid having to allocate a sync object and initialize a monitor * for each instance the list. This avoid additional allocations but introduces some contention * between lists. As there is already contention on the global snapshot lock to write so the * additional contention introduced by this lock is nominal. * * In code the requires this lock and calls `writable` (or other operation that acquires the * snapshot global lock), this lock *MUST* be acquired last to avoid deadlocks. In other words, the * lock must be taken in the `writable` lambda, if `writable` is used. */ private val sync = makeSynchronizedObject() private fun modificationError(): Nothing = error("Cannot modify a state list through an iterator") private fun validateRange(index: Int, size: Int) { if (index !in 0 until size) { throw IndexOutOfBoundsException("index ($index) is out of bound of [0, $size)") } } private fun invalidIteratorSet(): Nothing = error( "Cannot call set before the first call to next() or previous() " + "or immediately after a call to add() or remove()" ) internal class StateListIterator(val list: SnapshotStateList, offset: Int) : MutableListIterator { private var index = offset - 1 private var lastRequested = -1 private var structure = list.structure override fun hasPrevious() = index >= 0 override fun nextIndex() = index + 1 override fun previous(): T { validateModification() validateRange(index, list.size) lastRequested = index return list[index].also { index-- } } override fun previousIndex(): Int = index override fun add(element: T) { validateModification() list.add(index + 1, element) lastRequested = -1 index++ structure = list.structure } override fun hasNext() = index < list.size - 1 override fun next(): T { validateModification() val newIndex = index + 1 lastRequested = newIndex validateRange(newIndex, list.size) return list[newIndex].also { index = newIndex } } override fun remove() { validateModification() list.removeAt(lastRequested) index-- lastRequested = -1 structure = list.structure } override fun set(element: T) { validateModification() if (lastRequested < 0) invalidIteratorSet() list.set(lastRequested, element) structure = list.structure } private fun validateModification() { if (list.structure != structure) { throw ConcurrentModificationException() } } } internal class SubList(val parentList: SnapshotStateList, fromIndex: Int, toIndex: Int) : MutableList { private val offset = fromIndex private var structure = parentList.structure override var size = toIndex - fromIndex private set override fun contains(element: T): Boolean = indexOf(element) >= 0 override fun containsAll(elements: Collection): Boolean = elements.all { contains(it) } override fun get(index: Int): T { validateModification() validateRange(index, size) return parentList[offset + index] } override fun indexOf(element: T): Int { validateModification() (offset until offset + size).forEach { if (element == parentList[it]) return it - offset } return -1 } override fun isEmpty(): Boolean = size == 0 override fun iterator(): MutableIterator = listIterator() override fun lastIndexOf(element: T): Int { validateModification() var index = offset + size - 1 while (index >= offset) { if (element == parentList[index]) return index - offset index-- } return -1 } override fun add(element: T): Boolean { validateModification() parentList.add(offset + size, element) size++ structure = parentList.structure return true } override fun add(index: Int, element: T) { validateModification() parentList.add(offset + index, element) size++ structure = parentList.structure } override fun addAll(index: Int, elements: Collection): Boolean { validateModification() val result = parentList.addAll(index + offset, elements) if (result) { size += elements.size structure = parentList.structure } return result } override fun addAll(elements: Collection): Boolean = addAll(size, elements) override fun clear() { if (size > 0) { validateModification() parentList.removeRange(offset, offset + size) size = 0 structure = parentList.structure } } override fun listIterator(): MutableListIterator = listIterator(0) override fun listIterator(index: Int): MutableListIterator { validateModification() var current = index - 1 return object : MutableListIterator { override fun hasPrevious() = current >= 0 override fun nextIndex(): Int = current + 1 override fun previous(): T { val oldCurrent = current validateRange(oldCurrent, size) current = oldCurrent - 1 return this@SubList[oldCurrent] } override fun previousIndex(): Int = current override fun add(element: T) = modificationError() override fun hasNext(): Boolean = current < size - 1 override fun next(): T { val newCurrent = current + 1 validateRange(newCurrent, size) current = newCurrent return this@SubList[newCurrent] } override fun remove() = modificationError() override fun set(element: T) = modificationError() } } override fun remove(element: T): Boolean { val index = indexOf(element) return if (index >= 0) { removeAt(index) true } else false } override fun removeAll(elements: Collection): Boolean { var removed = false for (element in elements) { removed = remove(element) || removed } return removed } override fun removeAt(index: Int): T { validateModification() return parentList.removeAt(offset + index).also { size-- structure = parentList.structure } } override fun retainAll(elements: Collection): Boolean { validateModification() val removed = parentList.retainAllInRange(elements, offset, offset + size) if (removed > 0) { structure = parentList.structure size -= removed } return removed > 0 } override fun set(index: Int, element: T): T { validateRange(index, size) validateModification() val result = parentList.set(index + offset, element) structure = parentList.structure return result } override fun subList(fromIndex: Int, toIndex: Int): MutableList { requirePrecondition(fromIndex in 0..toIndex && toIndex <= size) { "fromIndex or toIndex are out of bounds" } validateModification() return SubList(parentList, fromIndex + offset, toIndex + offset) } private fun validateModification() { if (parentList.structure != structure) { throw ConcurrentModificationException() } } } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/snapshots/SnapshotStateMap.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.runtime.snapshots import androidx.compose.runtime.Stable import androidx.compose.runtime.external.kotlinx.collections.immutable.PersistentMap import androidx.compose.runtime.external.kotlinx.collections.immutable.persistentHashMapOf import androidx.compose.runtime.platform.makeSynchronizedObject import androidx.compose.runtime.platform.synchronized import kotlin.jvm.JvmName /** * An implementation of [MutableMap] that can be observed and snapshot. This is the result type * created by [androidx.compose.runtime.mutableStateMapOf]. * * This class closely implements the same semantics as [HashMap]. * * @see androidx.compose.runtime.mutableStateMapOf */ @Stable public class SnapshotStateMap : StateObject, MutableMap { override var firstStateRecord: StateRecord = persistentHashMapOf().let { map -> val snapshot = currentSnapshot() StateMapStateRecord(snapshot.snapshotId, map).also { if (snapshot !is GlobalSnapshot) { it.next = StateMapStateRecord(Snapshot.PreexistingSnapshotId.toSnapshotId(), map) } } } private set override fun prependStateRecord(value: StateRecord) { @Suppress("UNCHECKED_CAST") firstStateRecord = value as StateMapStateRecord } /** * Returns an immutable map containing all key-value pairs from the original map. * * The content of the map returned will not change even if the content of the map is changed in * the same snapshot. It also will be the same instance until the content is changed. It is not, * however, guaranteed to be the same instance for the same content as adding and removing the * same item from the this map might produce a different instance with the same content. * * This operation is O(1) and does not involve a physically copying the map. It instead returns * the underlying immutable map used internally to store the content of the map. * * It is recommended to use [toMap] when using returning the value of this map from * [androidx.compose.runtime.snapshotFlow]. */ public fun toMap(): Map = readable.map override val size: Int get() = readable.map.size override fun containsKey(key: K): Boolean = readable.map.containsKey(key) override fun containsValue(value: V): Boolean = readable.map.containsValue(value) override fun get(key: K): V? = readable.map[key] override fun isEmpty(): Boolean = readable.map.isEmpty() override val entries: MutableSet> = SnapshotMapEntrySet(this) override val keys: MutableSet = SnapshotMapKeySet(this) override val values: MutableCollection = SnapshotMapValueSet(this) @Suppress("UNCHECKED_CAST") override fun toString(): String = (firstStateRecord as StateMapStateRecord).withCurrent { "SnapshotStateMap(value=${it.map})@${hashCode()}" } override fun clear(): Unit = update { persistentHashMapOf() } override fun put(key: K, value: V): V? = mutate { it.put(key, value) } override fun putAll(from: Map): Unit = mutate { it.putAll(from) } override fun remove(key: K): V? = mutate { it.remove(key) } internal val modification get() = readable.modification internal fun removeValue(value: V) = entries .firstOrNull { it.value == value } ?.let { remove(it.key) true } == true @Suppress("UNCHECKED_CAST") internal val readable: StateMapStateRecord get() = (firstStateRecord as StateMapStateRecord).readable(this) internal inline fun removeIf(predicate: (MutableMap.MutableEntry) -> Boolean): Boolean { var removed = false mutate { for (entry in this.entries) { if (predicate(entry)) { it.remove(entry.key) removed = true } } } return removed } internal inline fun any(predicate: (Map.Entry) -> Boolean): Boolean { for (entry in readable.map.entries) { if (predicate(entry)) return true } return false } internal inline fun all(predicate: (Map.Entry) -> Boolean): Boolean { for (entry in readable.map.entries) { if (!predicate(entry)) return false } return true } /** * An internal function used by the debugger to display the value of the current value of the * mutable state object without triggering read observers. */ @Suppress("unused") internal val debuggerDisplayValue: Map @JvmName("getDebuggerDisplayValue") get() = withCurrent { map } private inline fun withCurrent(block: StateMapStateRecord.() -> R): R = @Suppress("UNCHECKED_CAST") (firstStateRecord as StateMapStateRecord).withCurrent(block) private inline fun writable(block: StateMapStateRecord.() -> R): R = @Suppress("UNCHECKED_CAST") (firstStateRecord as StateMapStateRecord).writable(this, block) private inline fun mutate(block: (MutableMap) -> R): R { var result: R while (true) { var oldMap: PersistentMap? = null var currentModification = 0 synchronized(sync) { val current = withCurrent { this } oldMap = current.map currentModification = current.modification } val builder = oldMap!!.builder() result = block(builder) val newMap = builder.build() if (newMap == oldMap || writable { attemptUpdate(currentModification, newMap) }) break } return result } private fun StateMapStateRecord.attemptUpdate( currentModification: Int, newMap: PersistentMap, ) = synchronized(sync) { if (modification == currentModification) { map = newMap modification++ true } else false } private inline fun update(block: (PersistentMap) -> PersistentMap) = withCurrent { val newMap = block(map) if (newMap !== map) writable { commitUpdate(newMap) } } // NOTE: do not inline this method to avoid class verification failures, see b/369909868 private fun StateMapStateRecord.commitUpdate(newMap: PersistentMap) = synchronized(sync) { map = newMap modification++ } /** Implementation class of [SnapshotStateMap]. Do not use. */ internal class StateMapStateRecord internal constructor(snapshotId: SnapshotId, internal var map: PersistentMap) : StateRecord(snapshotId) { internal var modification = 0 override fun assign(value: StateRecord) { @Suppress("UNCHECKED_CAST") val other = (value as StateMapStateRecord) synchronized(sync) { map = other.map modification = other.modification } } override fun create(): StateRecord = StateMapStateRecord(currentSnapshot().snapshotId, map) override fun create(snapshotId: SnapshotId): StateRecord = StateMapStateRecord(snapshotId, map) } } private abstract class SnapshotMapSet(val map: SnapshotStateMap) : MutableSet { override val size: Int get() = map.size override fun clear() = map.clear() override fun isEmpty() = map.isEmpty() } private class SnapshotMapEntrySet(map: SnapshotStateMap) : SnapshotMapSet>(map) { override fun add(element: MutableMap.MutableEntry) = unsupported() override fun addAll(elements: Collection>) = unsupported() override fun iterator(): MutableIterator> = StateMapMutableEntriesIterator(map, map.readable.map.entries.iterator()) override fun remove(element: MutableMap.MutableEntry) = map.remove(element.key) != null override fun removeAll(elements: Collection>): Boolean { var removed = false for (element in elements) { removed = map.remove(element.key) != null || removed } return removed } override fun retainAll(elements: Collection>): Boolean { val entries = elements.associate { it.key to it.value } return map.removeIf { !entries.containsKey(it.key) || entries[it.key] != it.value } } override fun contains(element: MutableMap.MutableEntry): Boolean { return map[element.key] == element.value } override fun containsAll(elements: Collection>): Boolean { return elements.all { contains(it) } } } private class SnapshotMapKeySet(map: SnapshotStateMap) : SnapshotMapSet(map) { override fun add(element: K) = unsupported() override fun addAll(elements: Collection) = unsupported() override fun iterator() = StateMapMutableKeysIterator(map, map.readable.map.entries.iterator()) override fun remove(element: K): Boolean = map.remove(element) != null override fun removeAll(elements: Collection): Boolean { var removed = false elements.forEach { removed = map.remove(it) != null || removed } return removed } override fun retainAll(elements: Collection): Boolean { val set = elements.toSet() return map.removeIf { it.key !in set } } override fun contains(element: K) = map.contains(element) override fun containsAll(elements: Collection): Boolean = elements.all { map.contains(it) } } private class SnapshotMapValueSet(map: SnapshotStateMap) : SnapshotMapSet(map) { override fun add(element: V) = unsupported() override fun addAll(elements: Collection) = unsupported() override fun iterator() = StateMapMutableValuesIterator(map, map.readable.map.entries.iterator()) override fun remove(element: V): Boolean = map.removeValue(element) override fun removeAll(elements: Collection): Boolean { val set = elements.toSet() return map.removeIf { it.value in set } } override fun retainAll(elements: Collection): Boolean { val set = elements.toSet() return map.removeIf { it.value !in set } } override fun contains(element: V) = map.containsValue(element) override fun containsAll(elements: Collection): Boolean { return elements.all { map.containsValue(it) } } } /** * This lock is used to ensure that the value of modification and the map in the state record, when * used together, are atomically read and written. * * A global sync object is used to avoid having to allocate a sync object and initialize a monitor * for each instance the map. This avoids additional allocations but introduces some contention * between maps. As there is already contention on the global snapshot lock to write so the * additional contention introduced by this lock is nominal. * * In code the requires this lock and calls `writable` (or other operation that acquires the * snapshot global lock), this lock *MUST* be acquired last to avoid deadlocks. In other words, the * lock must be taken in the `writable` lambda, if `writable` is used. */ private val sync = makeSynchronizedObject() private abstract class StateMapMutableIterator( val map: SnapshotStateMap, val iterator: Iterator>, ) { protected var modification = map.modification protected var current: Map.Entry? = null protected var next: Map.Entry? = null init { advance() } fun remove() = modify { val value = current if (value != null) { map.remove(value.key) current = null } else { throw IllegalStateException() } } fun hasNext() = next != null protected fun advance() { current = next next = if (iterator.hasNext()) iterator.next() else null } protected inline fun modify(block: () -> T): T { if (map.modification != modification) { throw ConcurrentModificationException() } return block().also { modification = map.modification } } } private class StateMapMutableEntriesIterator( map: SnapshotStateMap, iterator: Iterator>, ) : StateMapMutableIterator(map, iterator), MutableIterator> { override fun next(): MutableMap.MutableEntry { advance() if (current != null) { return object : MutableMap.MutableEntry { override val key = current!!.key override var value = current!!.value override fun setValue(newValue: V): V = modify { val result = value map[key] = newValue value = newValue return result } } } else { throw IllegalStateException() } } } private class StateMapMutableKeysIterator( map: SnapshotStateMap, iterator: Iterator>, ) : StateMapMutableIterator(map, iterator), MutableIterator { override fun next(): K { val result = next ?: throw IllegalStateException() advance() return result.key } } private class StateMapMutableValuesIterator( map: SnapshotStateMap, iterator: Iterator>, ) : StateMapMutableIterator(map, iterator), MutableIterator { override fun next(): V { val result = next ?: throw IllegalStateException() advance() return result.value } } internal fun unsupported(): Nothing { throw UnsupportedOperationException() } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/ComposeVersion.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.runtime @Suppress("unused") internal object ComposeVersion { /** * This version number is used by the compose compiler in order to verify that the compiler and * the runtime are compatible with one another. * * Every release should increase this number to a multiple of 100, which provides for the * opportunity to use the last two digits for releases made out-of-band. * * IMPORTANT: Whenever updating this value, please make sure to also update `versionTable` and * `minimumRuntimeVersionInt` in `VersionChecker.kt` of the compiler. */ const val version: Int = 13000 } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/Applier.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.runtime import androidx.compose.runtime.internal.JvmDefaultWithCompatibility /** * An Applier is responsible for applying the tree-based operations that get emitted during a * composition. Every [Composer] has an [Applier] which it uses to emit a [ComposeNode]. * * A custom [Applier] implementation will be needed in order to utilize Compose to build and * maintain a tree of a novel type. * * @sample androidx.compose.runtime.samples.CustomTreeComposition * @see AbstractApplier * @see Composition * @see Composer * @see ComposeNode */ @JvmDefaultWithCompatibility public interface Applier { /** * The node that operations will be applied on at any given time. It is expected that the value * of this property will change as [down] and [up] are called. */ public val current: N /** * Called when the [Composer] is about to begin applying changes using this applier. * [onEndChanges] will be called when changes are complete. */ public fun onBeginChanges() {} /** * Called when the [Composer] is finished applying changes using this applier. A call to * [onBeginChanges] will always precede a call to [onEndChanges]. */ public fun onEndChanges() {} /** * Indicates that the applier is getting traversed "down" the tree. When this gets called, * [node] is expected to be a child of [current], and after this operation, [node] is expected * to be the new [current]. */ public fun down(node: N) /** * Indicates that the applier is getting traversed "up" the tree. After this operation * completes, the [current] should return the "parent" of the [current] node at the beginning of * this operation. */ public fun up() /** * Indicates that [instance] should be inserted as a child to [current] at [index]. An applier * should insert the node into the tree either in [insertTopDown] or [insertBottomUp], not both. * * The [insertTopDown] method is called before the children of [instance] have been created and * inserted into it. [insertBottomUp] is called after all children have been created and * inserted. * * Some trees are faster to build top-down, in which case the [insertTopDown] method should be * used to insert the [instance]. Other trees are faster to build bottom-up in which case * [insertBottomUp] should be used. * * To give example of building a tree top-down vs. bottom-up consider the following tree, * ``` * R * | * B * / \ * A C * ``` * * where the node `B` is being inserted into the tree at `R`. Top-down building of the tree * first inserts `B` into `R`, then inserts `A` into `B` followed by inserting `C` into B`. For * example, * * ``` * 1 2 3 * R R R * | | | * B B B * / / \ * A A C * ``` * * A bottom-up building of the tree starts with inserting `A` and `C` into `B` then inserts `B` * tree into `R`. * * ``` * 1 2 3 * B B R * | / \ | * A A C B * / \ * A C * ``` * * To see how building top-down vs. bottom-up can differ significantly in performance consider a * tree where whenever a child is added to the tree all parent nodes, up to the root, are * notified of the new child entering the tree. If the tree is built top-down, * 1. `R` is notified of `B` entering. * 2. `B` is notified of `A` entering, `R` is notified of `A` entering. * 3. `B` is notified of `C` entering, `R` is notified of `C` entering. * * for a total of 5 notifications. The number of notifications grows exponentially with the * number of inserts. * * For bottom-up, the notifications are, * 1. `B` is notified `A` entering. * 2. `B` is notified `C` entering. * 3. `R` is notified `B` entering. * * The notifications are linear to the number of nodes inserted. * * If, on the other hand, all children are notified when the parent enters a tree, then the * notifications are, for top-down, * 1. `B` is notified it is entering `R`. * 2. `A` is notified it is entering `B`. * 3. `C` is notified it is entering `B`. * * which is linear to the number of nodes inserted. * * For bottom-up, the notifications look like, * 1. `A` is notified it is entering `B`. * 2. `C` is notified it is entering `B`. * 3. `B` is notified it is entering `R`, `A` is notified it is entering `R`, `C` is notified it * is entering `R`. * * which exponential to the number of nodes inserted. */ public fun insertTopDown(index: Int, instance: N) /** * Indicates that [instance] should be inserted as a child of [current] at [index]. An applier * should insert the node into the tree either in [insertTopDown] or [insertBottomUp], not both. * See the description of [insertTopDown] to which describes when to implement [insertTopDown] * and when to use [insertBottomUp]. */ public fun insertBottomUp(index: Int, instance: N) /** * Indicates that the children of [current] from [index] to [index] + [count] should be removed. */ public fun remove(index: Int, count: Int) /** * Indicates that [count] children of [current] should be moved from index [from] to index [to]. * * The [to] index is relative to the position before the change, so, for example, to move an * element at position 1 to after the element at position 2, [from] should be `1` and [to] * should be `3`. If the elements were A B C D E, calling `move(1, 3, 1)` would result in the * elements being reordered to A C B D E. */ public fun move(from: Int, to: Int, count: Int) /** * Move to the root and remove all nodes from the root, preparing both this [Applier] and its * root to be used as the target of a new composition in the future. */ public fun clear() /** Apply a change to the current node. */ public fun apply(block: N.(Any?) -> Unit, value: Any?) { current.block(value) } /** Notify [current] is is being reused in reusable content. */ public fun reuse() { (current as? ComposeNodeLifecycleCallback)?.onReuse() } } /** * An abstract [Applier] implementation. * * @sample androidx.compose.runtime.samples.CustomTreeComposition * @see Applier * @see Composition * @see Composer * @see ComposeNode */ public abstract class AbstractApplier(public val root: T) : Applier { private val stack = Stack() override var current: T = root protected set override fun down(node: T) { stack.push(current) current = node } override fun up() { current = stack.pop() } final override fun clear() { stack.clear() current = root onClear() } /** Called to perform clearing of the [root] when [clear] is called. */ protected abstract fun onClear() protected fun MutableList.remove(index: Int, count: Int) { if (count == 1) { removeAt(index) } else { subList(index, index + count).clear() } } protected fun MutableList.move(from: Int, to: Int, count: Int) { val dest = if (from > to) to else to - count if (count == 1) { if (from == to + 1 || from == to - 1) { // Adjacent elements, perform swap to avoid backing array manipulations. val fromEl = get(from) val toEl = set(to, fromEl) set(from, toEl) } else { val fromEl = removeAt(from) add(dest, fromEl) } } else { val subView = subList(from, from + count) val subCopy = subView.toMutableList() subView.clear() addAll(dest, subCopy) } } } internal class OffsetApplier(private val applier: Applier, private val offset: Int) : Applier { private var nesting = 0 override val current: N get() = applier.current override fun down(node: N) { nesting++ applier.down(node) } override fun up() { runtimeCheck(nesting > 0) { "OffsetApplier up called with no corresponding down" } nesting-- applier.up() } override fun insertTopDown(index: Int, instance: N) { applier.insertTopDown(index + if (nesting == 0) offset else 0, instance) } override fun insertBottomUp(index: Int, instance: N) { applier.insertBottomUp(index + if (nesting == 0) offset else 0, instance) } override fun remove(index: Int, count: Int) { applier.remove(index + if (nesting == 0) offset else 0, count) } override fun move(from: Int, to: Int, count: Int) { val effectiveOffset = if (nesting == 0) offset else 0 applier.move(from + effectiveOffset, to + effectiveOffset, count) } override fun clear() { composeImmediateRuntimeError("Clear is not valid on OffsetApplier") } override fun apply(block: N.(Any?) -> Unit, value: Any?) { applier.apply(block, value) } override fun reuse() { applier.reuse() } } /** * A stub of [Applier] that does not implement any operations and throws when called into. Used to * apply pending changes that do not result in a change to the composition hierarchy and therefore * do not need a real application phase after completing the composition. */ internal object ThrowingApplierStub : Applier { override val current: Any get() = throwIllegalOperationException() override fun up() = throwIllegalOperationException() override fun remove(index: Int, count: Int) = throwIllegalOperationException() override fun move(from: Int, to: Int, count: Int) = throwIllegalOperationException() override fun clear() = throwIllegalOperationException() override fun insertBottomUp(index: Int, instance: Any?) = throwIllegalOperationException() override fun insertTopDown(index: Int, instance: Any?) = throwIllegalOperationException() override fun down(node: Any?) = throwIllegalOperationException() private fun throwIllegalOperationException() { composeImmediateRuntimeError( "ChangeList cannot call the Applier when " + "executing pending changes outside of the applier phase." ) } } ``` ## File: compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/RecomposeScopeImpl.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.runtime import androidx.collection.MutableObjectIntMap import androidx.collection.MutableScatterMap import androidx.collection.ScatterSet import androidx.compose.runtime.composer.gapbuffer.GapAnchor import androidx.compose.runtime.composer.gapbuffer.SlotTable import androidx.compose.runtime.composer.gapbuffer.SlotWriter import androidx.compose.runtime.snapshots.fastAny import androidx.compose.runtime.snapshots.fastForEach import androidx.compose.runtime.tooling.ComposeToolingApi import androidx.compose.runtime.tooling.IdentifiableRecomposeScope /** * Represents a recomposable scope or section of the composition hierarchy. Can be used to manually * invalidate the scope to schedule it for recomposition. */ public interface RecomposeScope { /** * Invalidate the corresponding scope, requesting the composer recompose this scope. * * This method is thread safe. */ public fun invalidate() } private const val changedLowBitMask = 0b001_001_001_001_001_001_001_001_001_001_0 private const val changedHighBitMask = changedLowBitMask shl 1 private const val changedMask = (changedLowBitMask or changedHighBitMask).inv() /** * A compiler plugin utility function to change $changed flags from Different(10) to Same(01) for * when captured by restart lambdas. All parameters are passed with the same value as it was * previously invoked with and the changed flags should reflect that. */ @PublishedApi internal fun updateChangedFlags(flags: Int): Int { val lowBits = flags and changedLowBitMask val highBits = flags and changedHighBitMask return ((flags and changedMask) or (lowBits or (highBits shr 1)) or ((lowBits shl 1) and highBits)) } private const val UsedFlag = 0x001 private const val DefaultsInScopeFlag = 0x002 private const val DefaultsInvalidFlag = 0x004 private const val RequiresRecomposeFlag = 0x008 private const val SkippedFlag = 0x010 private const val RereadingFlag = 0x020 private const val ForcedRecomposeFlag = 0x040 private const val ForceReusing = 0x080 private const val Paused = 0x100 private const val Resuming = 0x200 private const val ResetReusing = 0x400 internal interface RecomposeScopeOwner { fun invalidate(scope: RecomposeScopeImpl, instance: Any?): InvalidationResult fun recomposeScopeReleased(scope: RecomposeScopeImpl) fun recordReadOf(value: Any) } /** * A RecomposeScope is created for a region of the composition that can be recomposed independently * of the rest of the composition. The composer will position the slot table to the location stored * in [anchor] and call [block] when recomposition is requested. It is created by * [Composer.startRestartGroup] and is used to track how to restart the group. */ @OptIn(ComposeToolingApi::class) internal class RecomposeScopeImpl(internal var owner: RecomposeScopeOwner?) : ScopeUpdateScope, RecomposeScope, IdentifiableRecomposeScope { /** The backing store for the boolean flags tracked by the recompose scope. */ private var flags: Int = 0 /** * An anchor to the location in the slot table that start the group associated with this * recompose scope. */ var anchor: Anchor? = null /** Access to anchor from tooling */ @ComposeToolingApi override val identity: Any? get() = anchor /** * Return whether the scope is valid. A scope becomes invalid when the slots it updates are * removed from the slot table. For example, if the scope is in the then clause of an if * statement that later becomes false. */ val valid: Boolean get() = owner != null && anchor?.valid ?: false val canRecompose: Boolean get() = block != null /** * Used is set when the [RecomposeScopeImpl] is used by, for example, [currentRecomposeScope]. * This is used as the result of [Composer.endRestartGroup] and indicates whether the lambda * that is stored in [block] will be used. */ var used: Boolean get() = getFlag(UsedFlag) set(value) { setFlag(UsedFlag, value) } /** * Used to force a scope to the reusing state when a composition is paused while reusing * content. */ var reusing: Boolean get() = getFlag(ForceReusing) set(value) { setFlag(ForceReusing, value) } /** * Used to restore the reusing state after unpausing a composition that was paused in a reusing * state. */ var resetReusing: Boolean get() = getFlag(ResetReusing) set(value) { setFlag(ResetReusing, value) } /** Used to flag a scope as paused for pausable compositions */ var paused: Boolean get() = getFlag(Paused) set(value) { setFlag(Paused, value) } /** Used to flag a scope as paused for pausable compositions */ var resuming: Boolean get() = getFlag(Resuming) set(value) { setFlag(Resuming, value) } /** * Set to true when the there are function default calculations in the scope. These are treated * as a special case to avoid having to create a special scope for them. If these change the * this scope needs to be recomposed but the default values can be skipped if they where not * invalidated. */ var defaultsInScope: Boolean get() = getFlag(DefaultsInScopeFlag) set(value) { setFlag(DefaultsInScopeFlag, value) } /** * Tracks whether any of the calculations in the default values were changed. See * [defaultsInScope] for details. */ var defaultsInvalid: Boolean get() = getFlag(DefaultsInvalidFlag) set(value) { setFlag(DefaultsInvalidFlag, value) } /** * Tracks whether the scope was invalidated directly but was recomposed because the caller was * recomposed. This ensures that a scope invalidated directly will recompose even if its * parameters are the same as the previous recomposition. */ var requiresRecompose: Boolean get() = getFlag(RequiresRecomposeFlag) set(value) { setFlag(RequiresRecomposeFlag, value) } /** The lambda to call to restart the scopes composition. */ private var block: ((Composer, Int) -> Unit)? = null /** * Restart the scope's composition. It is an error if [block] was not updated. The code * generated by the compiler ensures that when the recompose scope is used then [block] will be * set but it might occur if the compiler is out-of-date (or ahead of the runtime) or incorrect * direct calls to [Composer.startRestartGroup] and [Composer.endRestartGroup]. */ fun compose(composer: Composer) { block?.invoke(composer, 1) ?: error("Invalid restart scope") } /** * Invalidate the group which will cause [owner] to request this scope be recomposed, and an * [InvalidationResult] will be returned. */ fun invalidateForResult(value: Any?): InvalidationResult = owner?.invalidate(this, value) ?: InvalidationResult.IGNORED /** * Release the recompose scope. This is called when the recompose scope has been removed by the * compostion because the part of the composition it was tracking was removed. */ fun release() { owner?.recomposeScopeReleased(this) owner = null trackedInstances = null trackedDependencies = null block = null } /** * Called when the data tracked by this recompose scope moves to a different composition when * for example, the movable content it is part of has moved. */ fun adoptedBy(owner: RecomposeScopeOwner) { this.owner = owner } /** * Invalidate the group which will cause [owner] to request this scope be recomposed. * * Unlike [invalidateForResult], this method is thread safe and calls the thread safe invalidate * on the composer. */ override fun invalidate() { owner?.invalidate(this, null) } /** * Update [block]. The scope is returned by [Composer.endRestartGroup] when [used] is true and * implements [ScopeUpdateScope]. */ override fun updateScope(block: (Composer, Int) -> Unit) { this.block = block } private var currentToken = 0 private var trackedInstances: MutableObjectIntMap? = null private var trackedDependencies: MutableScatterMap, Any?>? = null private var rereading: Boolean get() = getFlag(RereadingFlag) set(value) { setFlag(RereadingFlag, value) } /** * Used to explicitly force recomposition. This is used during live edit to force a recompose * scope that doesn't have a restart callback to recompose as its parent (or some parent above * it) was invalidated and the path to this scope has also been forced. */ var forcedRecompose: Boolean get() = getFlag(ForcedRecomposeFlag) set(value) { setFlag(ForcedRecomposeFlag, value) } /** Indicates whether the scope was skipped (e.g. [scopeSkipped] was called. */ internal var skipped: Boolean get() = getFlag(SkippedFlag) private set(value) { setFlag(SkippedFlag, value) } /** * Called when composition start composing into this scope. The [token] is a value that is * unique everytime this is called. This is currently the snapshot id but that shouldn't be * relied on. */ fun start(token: Int) { currentToken = token skipped = false } fun scopeSkipped() { if (!reusing) { skipped = true } } /** * Track instances that were read in scope. * * @return whether the value was already read in scope during current pass */ fun recordRead(instance: Any): Boolean { if (rereading) return false // Re-reading should force composition to update its tracking val trackedInstances = trackedInstances ?: MutableObjectIntMap().also { trackedInstances = it } val token = trackedInstances.put(instance, currentToken, default = -1) if (token == currentToken) { return true } return false } fun recordDerivedStateValue(instance: DerivedState<*>, value: Any?) { val trackedDependencies = trackedDependencies ?: MutableScatterMap, Any?>().also { trackedDependencies = it } trackedDependencies[instance] = value } /** * Returns true if the scope is observing derived state which might make this scope * conditionally invalidated. */ val isConditional: Boolean get() = trackedDependencies != null /** * Determine if the scope should be considered invalid. * * @param instances The set of objects reported as invalidating this scope. */ fun isInvalidFor(instances: Any? /* State | ScatterSet | null */): Boolean { // If a non-empty instances exists and contains only derived state objects with their // default values, then the scope should not be considered invalid. Otherwise the scope // should if it was invalidated by any other kind of instance. if (instances == null) return true val trackedDependencies = trackedDependencies ?: return true return when (instances) { is DerivedState<*> -> { instances.checkDerivedStateChanged(trackedDependencies) } is ScatterSet<*> -> { instances.isNotEmpty() && instances.any { it !is DerivedState<*> || it.checkDerivedStateChanged(trackedDependencies) } } else -> true } } private fun DerivedState<*>.checkDerivedStateChanged( dependencies: MutableScatterMap, Any?> ): Boolean { @Suppress("UNCHECKED_CAST") this as DerivedState val policy = policy ?: structuralEqualityPolicy() return !policy.equivalent(currentRecord.currentValue, dependencies[this]) } fun rereadTrackedInstances() { owner?.let { owner -> trackedInstances?.let { trackedInstances -> rereading = true try { trackedInstances.forEach { value, _ -> owner.recordReadOf(value) } } finally { rereading = false } } } } /** * Called when composition is completed for this scope. The [token] is the same token passed in * the previous call to [start]. If [end] returns a non-null value the lambda returned will be * called during [ControlledComposition.applyChanges]. */ fun end(token: Int): ((Composition) -> Unit)? { return trackedInstances?.let { instances -> // If any value previous observed was not read in this current composition // schedule the value to be removed from the observe scope and removed from the // observations tracked by the composition. // [skipped] is true if the scope was skipped. If the scope was skipped we should // leave the observations unmodified. if (!skipped && instances.any { _, instanceToken -> instanceToken != token }) { composition -> if ( currentToken == token && instances == trackedInstances && composition is CompositionImpl ) { instances.removeIf { instance, instanceToken -> val shouldRemove = instanceToken != token if (shouldRemove) { composition.removeObservation(instance, this) if (instance is DerivedState<*>) { composition.removeDerivedStateObservation(instance) trackedDependencies?.remove(instance) } } shouldRemove } } } else null } } @Suppress("NOTHING_TO_INLINE") private inline fun getFlag(flag: Int) = flags and flag != 0 @Suppress("NOTHING_TO_INLINE") private inline fun setFlag(flag: Int, value: Boolean) { val existingFlags = flags flags = if (value) { existingFlags or flag } else { existingFlags and flag.inv() } } companion object { internal fun adoptAnchoredScopes( slots: SlotWriter, anchors: List, newOwner: RecomposeScopeOwner, ) { if (anchors.isNotEmpty()) { anchors.fastForEach { anchor -> // The recompose scope is always at slot 0 of a restart group. val recomposeScope = slots.slot(anchor, 0) as? RecomposeScopeImpl // Check for null as the anchor might not be for a recompose scope recomposeScope?.adoptedBy(newOwner) } } } internal fun hasAnchoredRecomposeScopes(slots: SlotTable, anchors: List) = anchors.isNotEmpty() && anchors.fastAny { slots.ownsAnchor(it) && slots.slot(slots.anchorIndex(it), 0) is RecomposeScopeImpl } } } ``` ================================================ FILE: .claude/skills/compose-expert/references/source-code/ui-source.md ================================================ # Compose UI Source Reference ## File: compose/ui/ui/src/androidMain/kotlin/androidx/compose/ui/platform/AndroidCompositionLocals.android.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.ui.platform import android.content.Context import android.content.res.Configuration import android.content.res.Resources import android.view.View import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalWithComputedDefaultOf import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.res.ImageVectorCache import androidx.compose.ui.res.ResourceIdCache import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.savedstate.SavedStateRegistryOwner import androidx.savedstate.compose.LocalSavedStateRegistryOwner /** * The Android [Configuration]. The [Configuration] is useful for determining how to organize the * UI. */ val LocalConfiguration = compositionLocalOf { noLocalProvidedFor("LocalConfiguration") } /** Provides a [Context] that can be used by Android applications. */ val LocalContext = staticCompositionLocalOf { noLocalProvidedFor("LocalContext") } /** * The Android [Resources]. This will be updated when [LocalConfiguration] changes, to ensure that * calls to APIs such as [Resources.getString] return updated values. */ val LocalResources = compositionLocalWithComputedDefaultOf { // Read LocalConfiguration here to invalidate callers of LocalResources when the // configuration changes. This is preferable to explicitly providing the resources object // because the resources object can still have the same instance, even though the // configuration changed, which would mean that callers would not get invalidated. To // resolve that we would need to use neverEqualPolicy to force an invalidation even though // the Resources didn't change, but then that would cause invalidations every time the // providing Composable is recomposed, regardless of whether a configuration change happened // or not. LocalConfiguration.currentValue LocalContext.currentValue.resources } internal val LocalImageVectorCache = staticCompositionLocalOf { noLocalProvidedFor("LocalImageVectorCache") } internal val LocalResourceIdCache = staticCompositionLocalOf { noLocalProvidedFor("LocalResourceIdCache") } @Deprecated( "Moved to lifecycle-runtime-compose library in androidx.lifecycle.compose package.", ReplaceWith("androidx.lifecycle.compose.LocalLifecycleOwner"), ) actual val LocalLifecycleOwner get() = LocalLifecycleOwner /** The CompositionLocal containing the current [SavedStateRegistryOwner]. */ @Deprecated( "Moved to savedstate-compose library in androidx.savedstate.compose package.", ReplaceWith("androidx.savedstate.compose.LocalSavedStateRegistryOwner"), ) val LocalSavedStateRegistryOwner get() = LocalSavedStateRegistryOwner /** The CompositionLocal containing the current Compose [View]. */ val LocalView = staticCompositionLocalOf { noLocalProvidedFor("LocalView") } private fun noLocalProvidedFor(name: String): Nothing { error("CompositionLocal $name not present") } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/Modifier.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.ui import androidx.compose.runtime.Stable import androidx.compose.ui.internal.JvmDefaultWithCompatibility import androidx.compose.ui.internal.PlatformOptimizedCancellationException import androidx.compose.ui.internal.checkPrecondition import androidx.compose.ui.node.DelegatableNode import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.NodeCoordinator import androidx.compose.ui.node.NodeKind import androidx.compose.ui.node.ObserverNodeOwnerScope import androidx.compose.ui.node.requireOwner import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancel internal class ModifierNodeDetachedCancellationException : PlatformOptimizedCancellationException("The Modifier.Node was detached") /** * An ordered, immutable collection of [modifier elements][Modifier.Element] that decorate or add * behavior to Compose UI elements. For example, backgrounds, padding and click event listeners * decorate or add behavior to rows, text or buttons. * * @sample androidx.compose.ui.samples.ModifierUsageSample * * Modifier implementations should offer a fluent factory extension function on [Modifier] for * creating combined modifiers by starting from existing modifiers: * * @sample androidx.compose.ui.samples.ModifierFactorySample * * Modifier elements may be combined using [then]. Order is significant; modifier elements that * appear first will be applied first. * * Composables that accept a [Modifier] as a parameter to be applied to the whole component * represented by the composable function should name the parameter `modifier` and assign the * parameter a default value of [Modifier]. It should appear as the first optional parameter in the * parameter list; after all required parameters (except for trailing lambda parameters) but before * any other parameters with default values. Any default modifiers desired by a composable function * should come after the `modifier` parameter's value in the composable function's implementation, * keeping [Modifier] as the default parameter value. For example: * * @sample androidx.compose.ui.samples.ModifierParameterSample * * The pattern above allows default modifiers to still be applied as part of the chain if a caller * also supplies unrelated modifiers. * * Composables that accept modifiers to be applied to a specific subcomponent `foo` should name the * parameter `fooModifier` and follow the same guidelines above for default values and behavior. * Subcomponent modifiers should be grouped together and follow the parent composable's modifier. * For example: * * @sample androidx.compose.ui.samples.SubcomponentModifierSample */ @Suppress("ModifierFactoryExtensionFunction") @Stable @JvmDefaultWithCompatibility interface Modifier { /** * Accumulates a value starting with [initial] and applying [operation] to the current value and * each element from outside in. * * Elements wrap one another in a chain from left to right; an [Element] that appears to the * left of another in a `+` expression or in [operation]'s parameter order affects all of the * elements that appear after it. [foldIn] may be used to accumulate a value starting from the * parent or head of the modifier chain to the final wrapped child. */ fun foldIn(initial: R, operation: (R, Element) -> R): R /** * Accumulates a value starting with [initial] and applying [operation] to the current value and * each element from inside out. * * Elements wrap one another in a chain from left to right; an [Element] that appears to the * left of another in a `+` expression or in [operation]'s parameter order affects all of the * elements that appear after it. [foldOut] may be used to accumulate a value starting from the * child or tail of the modifier chain up to the parent or head of the chain. */ fun foldOut(initial: R, operation: (Element, R) -> R): R /** Returns `true` if [predicate] returns true for any [Element] in this [Modifier]. */ fun any(predicate: (Element) -> Boolean): Boolean /** * Returns `true` if [predicate] returns true for all [Element]s in this [Modifier] or if this * [Modifier] contains no [Element]s. */ fun all(predicate: (Element) -> Boolean): Boolean /** * Concatenates this modifier with another. * * Returns a [Modifier] representing this modifier followed by [other] in sequence. */ infix fun then(other: Modifier): Modifier = if (other === Modifier) this else CombinedModifier(this, other) /** A single element contained within a [Modifier] chain. */ @JvmDefaultWithCompatibility interface Element : Modifier { override fun foldIn(initial: R, operation: (R, Element) -> R): R = operation(initial, this) override fun foldOut(initial: R, operation: (Element, R) -> R): R = operation(this, initial) override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this) override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this) } /** * The longer-lived object that is created for each [Modifier.Element] applied to a * [androidx.compose.ui.layout.Layout]. Most [Modifier.Node] implementations will have a * corresponding "Modifier Factory" extension method on Modifier that will allow them to be used * indirectly, without ever implementing a [Modifier.Node] subclass directly. In some cases it * may be useful to define a custom [Modifier.Node] subclass in order to efficiently implement * some collection of behaviors that requires maintaining state over time and over many * recompositions where the various provided Modifier factories are not sufficient. * * When a [Modifier] is set on a [androidx.compose.ui.layout.Layout], each [Modifier.Element] * contained in that linked list will result in a corresponding [Modifier.Node] instance in a * matching linked list of [Modifier.Node]s that the [androidx.compose.ui.layout.Layout] will * hold on to. As subsequent [Modifier] chains get set on the * [androidx.compose.ui.layout.Layout], the linked list of [Modifier.Node]s will be diffed and * updated as appropriate, even though the [Modifier] instance might be completely new. As a * result, the lifetime of a [Modifier.Node] is the intersection of the lifetime of the * [androidx.compose.ui.layout.Layout] that it lives on and a corresponding [Modifier.Element] * being present in the [androidx.compose.ui.layout.Layout]'s [Modifier]. * * If one creates a subclass of [Modifier.Node], it is expected that it will implement one or * more interfaces that interact with the various Compose UI subsystems. To use the * [Modifier.Node] subclass, it is expected that it will be instantiated by adding a * [androidx.compose.ui.node.ModifierNodeElement] to a [Modifier] chain. * * @see androidx.compose.ui.node.ModifierNodeElement * @see androidx.compose.ui.node.CompositionLocalConsumerModifierNode * @see androidx.compose.ui.node.DelegatableNode * @see androidx.compose.ui.node.DelegatingNode * @see androidx.compose.ui.node.LayoutModifierNode * @see androidx.compose.ui.node.DrawModifierNode * @see androidx.compose.ui.node.SemanticsModifierNode * @see androidx.compose.ui.node.PointerInputModifierNode * @see androidx.compose.ui.modifier.ModifierLocalModifierNode * @see androidx.compose.ui.node.ParentDataModifierNode * @see androidx.compose.ui.node.LayoutAwareModifierNode * @see androidx.compose.ui.node.GlobalPositionAwareModifierNode * @see androidx.compose.ui.node.ApproachLayoutModifierNode */ abstract class Node : DelegatableNode { @Suppress("LeakingThis") final override var node: Node = this private set private var scope: CoroutineScope? = null /** * A [CoroutineScope] that can be used to launch tasks that should run while the node is * attached. * * The scope is accessible between [onAttach] and [onDetach] calls, and will be cancelled * after the node is detached (after [onDetach] returns). * * @sample androidx.compose.ui.samples.ModifierNodeCoroutineScopeSample * @throws IllegalStateException If called while the node is not attached. */ val coroutineScope: CoroutineScope get() = scope ?: CoroutineScope( requireOwner().coroutineContext + Job(parent = requireOwner().coroutineContext[Job]) ) .also { scope = it } internal var kindSet: Int = 0 // NOTE: We use an aggregate mask that or's all of the type masks of the children of the // chain so that we can quickly prune a subtree. This INCLUDES the kindSet of this node // as well. Initialize this to "every node" so that before it is set it doesn't // accidentally cause a truncated traversal. internal var aggregateChildKindSet: Int = 0.inv() internal var parent: Node? = null internal var child: Node? = null internal var ownerScope: ObserverNodeOwnerScope? = null internal var coordinator: NodeCoordinator? = null private set internal var insertedNodeAwaitingAttachForInvalidation = false internal var updatedNodeAwaitingAttachForInvalidation = false private var onAttachRunExpected = false private var onDetachRunExpected = false internal var detachedListener: (() -> Unit)? = null /** * Indicates that the node is attached to a [androidx.compose.ui.layout.Layout] which is * part of the UI tree. This will get set to true right before [onAttach] is called, and set * to false right after [onDetach] is called. * * @see onAttach * @see onDetach */ var isAttached: Boolean = false private set /** * If this property returns `true`, then nodes will be automatically invalidated after the * modifier update completes (For example, if the returned Node is a [DrawModifierNode], its * [DrawModifierNode.invalidateDraw] function will be invoked automatically as part of auto * invalidation). * * This is enabled by default, and provides a convenient mechanism to schedule invalidation * and apply changes made to the modifier. You may choose to set this to `false` if your * modifier has auto-invalidatable properties that do not frequently require invalidation to * improve performance by skipping unnecessary invalidation. If `autoInvalidate` is set to * `false`, you must call the appropriate invalidate functions manually when the modifier is * updated or else the updates may not be reflected in the UI appropriately. */ @Suppress("GetterSetterNames") @get:Suppress("GetterSetterNames") open val shouldAutoInvalidate: Boolean get() = true internal open fun updateCoordinator(coordinator: NodeCoordinator?) { this.coordinator = coordinator } @Suppress("NOTHING_TO_INLINE") internal inline fun isKind(kind: NodeKind<*>) = kindSet and kind.mask != 0 internal open fun markAsAttached() { checkPrecondition(!isAttached) { "node attached multiple times" } checkPrecondition(coordinator != null) { "attach invoked on a node without a coordinator" } isAttached = true onAttachRunExpected = true } internal open fun runAttachLifecycle() { checkPrecondition(isAttached) { "Must run markAsAttached() prior to runAttachLifecycle" } checkPrecondition(onAttachRunExpected) { "Must run runAttachLifecycle() only once " + "after markAsAttached()" } onAttachRunExpected = false onAttach() onDetachRunExpected = true } internal open fun runDetachLifecycle() { checkPrecondition(isAttached) { "node detached multiple times" } checkPrecondition(coordinator != null) { "detach invoked on a node without a coordinator" } checkPrecondition(onDetachRunExpected) { "Must run runDetachLifecycle() once after runAttachLifecycle() and before " + "markAsDetached()" } onDetachRunExpected = false detachedListener?.invoke() onDetach() } internal open fun markAsDetached() { checkPrecondition(isAttached) { "Cannot detach a node that is not attached" } checkPrecondition(!onAttachRunExpected) { "Must run runAttachLifecycle() before markAsDetached()" } checkPrecondition(!onDetachRunExpected) { "Must run runDetachLifecycle() before markAsDetached()" } isAttached = false scope?.let { it.cancel(ModifierNodeDetachedCancellationException()) scope = null } } internal open fun reset() { checkPrecondition(isAttached) { "reset() called on an unattached node" } onReset() } /** * Called when the node is attached to a [androidx.compose.ui.layout.Layout] which is part * of the UI tree. When called, `node` is guaranteed to be non-null. You can call * sideEffect, coroutineScope, etc. This is not guaranteed to get called at a time where the * rest of the Modifier.Nodes in the hierarchy are "up to date". For instance, at the time * of calling onAttach for this node, another node may be in the tree that will be detached * by the time Compose has finished applying changes. As a result, if you need to guarantee * that the state of the tree is "final" for this round of changes, you should use the * [sideEffect] API to schedule the calculation to be done at that time. */ open fun onAttach() {} /** * Called when the node is not attached to a [androidx.compose.ui.layout.Layout] which is * not a part of the UI tree anymore. Note that the node can be reattached again. * * This should be called right before the node gets removed from the list, so you should * still be able to traverse inside of this method. Ideally we would not allow you to * trigger side effects here. */ open fun onDetach() {} /** * Called when the node is about to be moved to a pool of layouts ready to be reused. For * example it happens when the node is part of the item of LazyColumn after this item is * scrolled out of the viewport. This means this node could be in future reused for a * [androidx.compose.ui.layout.Layout] displaying a semantically different content when the * list will be populating a new item. * * Use this callback to reset some local item specific state, like "is my component * focused". * * This callback is called while the node is attached. Right after this callback the node * will be detached and later reattached when reused. * * @sample androidx.compose.ui.samples.ModifierNodeResetSample */ open fun onReset() {} /** * This can be called to register [effect] as a function to be executed after all of the * changes to the tree are applied. * * This API can only be called if the node [isAttached]. */ fun sideEffect(effect: () -> Unit) { requireOwner().registerOnEndApplyChangesListener(effect) } internal open fun setAsDelegateTo(owner: Node) { node = owner } } /** * The companion object `Modifier` is the empty, default, or starter [Modifier] that contains no * [elements][Element]. Use it to create a new [Modifier] using modifier extension factory * functions: * * @sample androidx.compose.ui.samples.ModifierUsageSample * * or as the default value for [Modifier] parameters: * * @sample androidx.compose.ui.samples.ModifierParameterSample */ // The companion object implements `Modifier` so that it may be used as the start of a // modifier extension factory expression. companion object : Modifier { override fun foldIn(initial: R, operation: (R, Element) -> R): R = initial override fun foldOut(initial: R, operation: (Element, R) -> R): R = initial override fun any(predicate: (Element) -> Boolean): Boolean = false override fun all(predicate: (Element) -> Boolean): Boolean = true override infix fun then(other: Modifier): Modifier = other override fun toString() = "Modifier" } } /** * A node in a [Modifier] chain. A CombinedModifier always contains at least two elements; a * Modifier [outer] that wraps around the Modifier [inner]. */ class CombinedModifier(internal val outer: Modifier, internal val inner: Modifier) : Modifier { override fun foldIn(initial: R, operation: (R, Modifier.Element) -> R): R = inner.foldIn(outer.foldIn(initial, operation), operation) override fun foldOut(initial: R, operation: (Modifier.Element, R) -> R): R = outer.foldOut(inner.foldOut(initial, operation), operation) override fun any(predicate: (Modifier.Element) -> Boolean): Boolean = outer.any(predicate) || inner.any(predicate) override fun all(predicate: (Modifier.Element) -> Boolean): Boolean = outer.all(predicate) && inner.all(predicate) override fun equals(other: Any?): Boolean = other is CombinedModifier && outer == other.outer && inner == other.inner override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode() override fun toString() = "[" + foldIn("") { acc, element -> if (acc.isEmpty()) element.toString() else "$acc, $element" } + "]" } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/Layout.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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. */ @file:Suppress("DEPRECATION") package androidx.compose.ui.layout import androidx.compose.runtime.Applier import androidx.compose.runtime.Composable import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.SkippableUpdater import androidx.compose.runtime.currentComposer import androidx.compose.runtime.currentCompositeKeyHash import androidx.compose.runtime.currentCompositeKeyHashCode import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.UiComposable import androidx.compose.ui.graphics.GraphicsLayerScope import androidx.compose.ui.materialize import androidx.compose.ui.materializeWithCompositionLocalInjectionInternal import androidx.compose.ui.node.ComposeUiNode import androidx.compose.ui.node.ComposeUiNode.Companion.ApplyOnDeactivatedNodeAssertion import androidx.compose.ui.node.ComposeUiNode.Companion.SetCompositeKeyHash import androidx.compose.ui.node.ComposeUiNode.Companion.SetMeasurePolicy import androidx.compose.ui.node.ComposeUiNode.Companion.SetModifier import androidx.compose.ui.node.ComposeUiNode.Companion.SetResolvedCompositionLocals import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.node.checkMeasuredSize import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastForEach import kotlin.jvm.JvmName /** * [Layout] is the main core component for layout. It can be used to measure and position zero or * more layout children. * * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by * the [measurePolicy] instance. See [MeasurePolicy] for more details. * * For a composable able to define its content according to the incoming constraints, see * [androidx.compose.foundation.layout.BoxWithConstraints]. * * Example usage: * * @sample androidx.compose.ui.samples.LayoutUsage * * Example usage with custom intrinsic measurements: * * @sample androidx.compose.ui.samples.LayoutWithProvidedIntrinsicsUsage * @param content The children composable to be laid out. * @param modifier Modifiers to be applied to the layout. * @param measurePolicy The policy defining the measurement and positioning of the layout. * @see Layout * @see MeasurePolicy * @see androidx.compose.foundation.layout.BoxWithConstraints */ @Suppress("ComposableLambdaParameterPosition") @UiComposable @Composable inline fun Layout( content: @Composable @UiComposable () -> Unit, modifier: Modifier = Modifier, measurePolicy: MeasurePolicy, ) { val compositeKeyHash = currentCompositeKeyHashCode.hashCode() val localMap = currentComposer.currentCompositionLocalMap val materialized = currentComposer.materialize(modifier) ReusableComposeNode>( factory = ComposeUiNode.Constructor, update = { set(measurePolicy, SetMeasurePolicy) set(localMap, SetResolvedCompositionLocals) set(compositeKeyHash, SetCompositeKeyHash) reconcile(ApplyOnDeactivatedNodeAssertion) set(materialized, SetModifier) }, content = content, ) } /** * [Layout] is the main core component for layout for "leaf" nodes. It can be used to measure and * position zero children. * * The measurement, layout and intrinsic measurement behaviours of this layout will be defined by * the [measurePolicy] instance. See [MeasurePolicy] for more details. * * For a composable able to define its content according to the incoming constraints, see * [androidx.compose.foundation.layout.BoxWithConstraints]. * * Example usage: * * @sample androidx.compose.ui.samples.LayoutUsage * * Example usage with custom intrinsic measurements: * * @sample androidx.compose.ui.samples.LayoutWithProvidedIntrinsicsUsage * @param modifier Modifiers to be applied to the layout. * @param measurePolicy The policy defining the measurement and positioning of the layout. * @see Layout * @see MeasurePolicy * @see androidx.compose.foundation.layout.BoxWithConstraints */ @Suppress("NOTHING_TO_INLINE") @Composable @UiComposable inline fun Layout(modifier: Modifier = Modifier, measurePolicy: MeasurePolicy) { val compositeKeyHash = currentCompositeKeyHashCode.hashCode() val materialized = currentComposer.materialize(modifier) val localMap = currentComposer.currentCompositionLocalMap ReusableComposeNode>( factory = ComposeUiNode.Constructor, update = { set(measurePolicy, SetMeasurePolicy) set(localMap, SetResolvedCompositionLocals) reconcile(ApplyOnDeactivatedNodeAssertion) set(materialized, SetModifier) set(compositeKeyHash, SetCompositeKeyHash) }, ) } /** * [Layout] is the main core component for layout. It can be used to measure and position zero or * more layout children. * * This overload accepts a list of multiple composable content lambdas, which allows treating * measurables put into different content lambdas differently - measure policy will provide a list * of lists of Measurables, not just a single list. Such list has the same size as the list of * contents passed into [Layout] and contains the list of measurables of the corresponding content * lambda in the same order. * * Note that layouts emitted as part of all [contents] lambdas will be added as a direct children * for this [Layout]. This means that if you set a custom z index on some children, the drawing * order will be calculated as if they were all provided as part of one lambda. * * Example usage: * * @sample androidx.compose.ui.samples.LayoutWithMultipleContentsUsage * @param contents The list of children composable contents to be laid out. * @param modifier Modifiers to be applied to the layout. * @param measurePolicy The policy defining the measurement and positioning of the layout. * @see Layout for a simpler use case when you have only one content lambda. */ @Suppress("ComposableLambdaParameterPosition", "NOTHING_TO_INLINE") @UiComposable @Composable inline fun Layout( contents: List<@Composable @UiComposable () -> Unit>, modifier: Modifier = Modifier, measurePolicy: MultiContentMeasurePolicy, ) { Layout( content = combineAsVirtualLayouts(contents), modifier = modifier, measurePolicy = remember(measurePolicy) { createMeasurePolicy(measurePolicy) }, ) } @PublishedApi internal fun combineAsVirtualLayouts( contents: List<@Composable @UiComposable () -> Unit> ): @Composable @UiComposable () -> Unit = { contents.fastForEach { content -> val compositeKeyHash = currentCompositeKeyHashCode.hashCode() ReusableComposeNode>( factory = ComposeUiNode.VirtualConstructor, update = { set(compositeKeyHash, SetCompositeKeyHash) }, content = content, ) } } /** * This function uses a JVM-Name because the original name now has a different implementation for * backwards compatibility [materializerOfWithCompositionLocalInjection]. More details can be found * at https://issuetracker.google.com/275067189 */ @PublishedApi @JvmName("modifierMaterializerOf") internal fun materializerOf( modifier: Modifier ): @Composable SkippableUpdater.() -> Unit = { val compositeKeyHash = currentCompositeKeyHashCode.hashCode() val materialized = currentComposer.materialize(modifier) update { set(materialized, SetModifier) set(compositeKeyHash, SetCompositeKeyHash) } } /** * This function exists solely for solving a backwards-incompatibility with older compilations that * used an older version of the `Layout` composable. New code paths should not call this. More * details can be found at https://issuetracker.google.com/275067189 */ @JvmName("materializerOf") @Deprecated( "Needed only for backwards compatibility. Do not use.", level = DeprecationLevel.WARNING, ) @PublishedApi internal fun materializerOfWithCompositionLocalInjection( modifier: Modifier ): @Composable SkippableUpdater.() -> Unit = { val compositeKeyHash = currentCompositeKeyHash.hashCode() val materialized = currentComposer.materializeWithCompositionLocalInjectionInternal(modifier) update { set(materialized, SetModifier) set(compositeKeyHash, SetCompositeKeyHash) } } @Suppress("ComposableLambdaParameterPosition") @Composable @UiComposable @Deprecated( "This API is unsafe for UI performance at scale - using it incorrectly will lead " + "to exponential performance issues. This API should be avoided whenever possible." ) fun MultiMeasureLayout( modifier: Modifier = Modifier, content: @Composable @UiComposable () -> Unit, measurePolicy: MeasurePolicy, ) { val compositeKeyHash = currentCompositeKeyHash.hashCode() val materialized = currentComposer.materialize(modifier) val localMap = currentComposer.currentCompositionLocalMap ReusableComposeNode>( factory = LayoutNode.Constructor, update = { set(measurePolicy, SetMeasurePolicy) set(localMap, SetResolvedCompositionLocals) @Suppress("DEPRECATION") init { this.canMultiMeasure = true } reconcile(ApplyOnDeactivatedNodeAssertion) set(materialized, SetModifier) set(compositeKeyHash, SetCompositeKeyHash) }, content = content, ) } /** Used to return a fixed sized item for intrinsics measurements in [Layout] */ private class FixedSizeIntrinsicsPlaceable(width: Int, height: Int) : Placeable() { init { measuredSize = IntSize(width, height) } override fun get(alignmentLine: AlignmentLine): Int = AlignmentLine.Unspecified override fun placeAt( position: IntOffset, zIndex: Float, layerBlock: (GraphicsLayerScope.() -> Unit)?, ) {} } /** Identifies an [IntrinsicMeasurable] as a min or max intrinsic measurement. */ internal enum class IntrinsicMinMax { Min, Max, } /** Identifies an [IntrinsicMeasurable] as a width or height intrinsic measurement. */ internal enum class IntrinsicWidthHeight { Width, Height, } // A large value to use as a replacement for Infinity with DefaultIntrinisicMeasurable. // A layout likely won't use this dimension as it is opposite from the one being measured in // the max/min Intrinsic Width/Height, but it is possible. For example, if the direct child // uses normal measurement/layout, we don't want to return Infinity sizes when its parent // asks for intrinsic size. 15 bits can fit in a Constraints, so should be safe unless // the parent adds to it and the other dimension is also very large (> 2^15). internal const val LargeDimension = (1 shl 15) - 1 /** * A wrapper around a [Measurable] for intrinsic measurements in [Layout]. Consumers of [Layout] * don't identify intrinsic methods, but we can give a reasonable implementation by using their * [measure], substituting the intrinsics gathering method for the [Measurable.measure] call. */ internal class DefaultIntrinsicMeasurable( val measurable: IntrinsicMeasurable, private val minMax: IntrinsicMinMax, private val widthHeight: IntrinsicWidthHeight, ) : Measurable { override val parentData: Any? get() = measurable.parentData override fun measure(constraints: Constraints): Placeable { if (widthHeight == IntrinsicWidthHeight.Width) { val width = if (minMax == IntrinsicMinMax.Max) { measurable.maxIntrinsicWidth(constraints.maxHeight) } else { measurable.minIntrinsicWidth(constraints.maxHeight) } // Can't use infinity for height, so use a large number val height = if (constraints.hasBoundedHeight) constraints.maxHeight else LargeDimension return FixedSizeIntrinsicsPlaceable(width, height) } val height = if (minMax == IntrinsicMinMax.Max) { measurable.maxIntrinsicHeight(constraints.maxWidth) } else { measurable.minIntrinsicHeight(constraints.maxWidth) } // Can't use infinity for width, so use a large number val width = if (constraints.hasBoundedWidth) constraints.maxWidth else LargeDimension return FixedSizeIntrinsicsPlaceable(width, height) } override fun minIntrinsicWidth(height: Int): Int { return measurable.minIntrinsicWidth(height) } override fun maxIntrinsicWidth(height: Int): Int { return measurable.maxIntrinsicWidth(height) } override fun minIntrinsicHeight(width: Int): Int { return measurable.minIntrinsicHeight(width) } override fun maxIntrinsicHeight(width: Int): Int { return measurable.maxIntrinsicHeight(width) } } /** * Receiver scope for [Layout]'s and [LayoutModifier]'s layout lambda when used in an intrinsics * call. */ internal class IntrinsicsMeasureScope( intrinsicMeasureScope: IntrinsicMeasureScope, override val layoutDirection: LayoutDirection, ) : MeasureScope, IntrinsicMeasureScope by intrinsicMeasureScope { override fun layout( width: Int, height: Int, alignmentLines: Map, rulers: (RulerScope.() -> Unit)?, placementBlock: Placeable.PlacementScope.() -> Unit, ): MeasureResult { val w = width.fastCoerceAtLeast(0) val h = height.fastCoerceAtLeast(0) checkMeasuredSize(w, h) return object : MeasureResult { override val width: Int get() = w override val height: Int get() = h override val alignmentLines: Map get() = alignmentLines override val rulers: (RulerScope.() -> Unit)? get() = rulers override fun placeChildren() { // Intrinsics should never be placed } } } } internal class ApproachIntrinsicsMeasureScope( intrinsicMeasureScope: ApproachIntrinsicMeasureScope, override val layoutDirection: LayoutDirection, ) : ApproachMeasureScope, ApproachIntrinsicMeasureScope by intrinsicMeasureScope { override fun layout( width: Int, height: Int, alignmentLines: Map, rulers: (RulerScope.() -> Unit)?, placementBlock: Placeable.PlacementScope.() -> Unit, ): MeasureResult { val w = width.fastCoerceAtLeast(0) val h = height.fastCoerceAtLeast(0) checkMeasuredSize(w, h) return object : MeasureResult { override val width: Int get() = w override val height: Int get() = h override val alignmentLines: Map get() = alignmentLines override val rulers: (RulerScope.() -> Unit)? get() = rulers override fun placeChildren() { // Intrinsics should never be placed } } } } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/MeasurePolicy.kt ```kotlin /* * Copyright 2021 The Android Open Source Project * * 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 androidx.compose.ui.layout import androidx.compose.runtime.Stable import androidx.compose.ui.internal.JvmDefaultWithCompatibility import androidx.compose.ui.unit.Constraints import androidx.compose.ui.util.fastMap /** * Defines the measure and layout behavior of a [Layout]. [Layout] and [MeasurePolicy] are the way * Compose layouts (such as `Box`, `Column`, etc.) are built, and they can also be used to achieve * custom layouts. * * See [Layout] samples for examples of how to use [MeasurePolicy]. * * Intrinsic measurement methods define the intrinsic size of the layout. These can be queried by * the layout's parent in order to obtain, in specific cases, more information about the size of the * layout in the absence of specific constraints: * - [minIntrinsicWidth] defines the minimum width this layout can take, given a specific height, * such that the content of the layout will be painted correctly * - [minIntrinsicHeight] defines the minimum height this layout can take, given a specific width, * such that the content of the layout will be painted correctly * - [maxIntrinsicWidth] defines the minimum width such that increasing it further will not decrease * the minimum intrinsic height * - [maxIntrinsicHeight] defines the minimum height such that increasing it further will not * decrease the minimum intrinsic width * * Most layout scenarios do not require querying intrinsic measurements. Therefore, when writing a * custom layout, it is common to only define the actual measurement, as most of the times the * intrinsic measurements of the layout will not be queried. Moreover, intrinsic measurement methods * have default implementations that make a best effort attempt to calculate the intrinsic * measurements by reusing the [measure] method. Note this will not be correct for all layouts, but * can be a convenient approximation. * * Intrinsic measurements can be useful when the layout system enforcement of no more than one * measurement per child is limiting. Layouts that use them are the `preferredWidth(IntrinsicSize)` * and `preferredHeight(IntrinsicSize)` modifiers. See their samples for when they can be useful. * * @see Layout */ @Stable @JvmDefaultWithCompatibility fun interface MeasurePolicy { /** * The function that defines the measurement and layout. Each [Measurable] in the [measurables] * list corresponds to a layout child of the layout, and children can be measured using the * [Measurable.measure] method. This method takes the [Constraints] which the child should * respect; different children can be measured with different constraints. * * Measuring a child returns a [Placeable], which reveals the size chosen by the child as a * result of its own measurement. According to the children sizes, the parent defines the * position of the children, by [placing][Placeable.PlacementScope.place] the [Placeable]s in * the [MeasureResult.placeChildren] of the returned [MeasureResult]. Therefore the parent needs * to measure its children with appropriate [Constraints], such that whatever valid sizes * children choose, they can be laid out correctly according to the parent's layout algorithm. * This is because there is no measurement negotiation between the parent and children: once a * child chooses its size, the parent needs to handle it correctly. * * Note that a child is allowed to choose a size that does not satisfy its constraints. However, * when this happens, the placeable's [width][Placeable.width] and [height][Placeable.height] * will not represent the real size of the child, but rather the size coerced in the child's * constraints. Therefore, it is common for parents to assume in their layout algorithm that its * children will always respect the constraints. When this does not happen in reality, the * position assigned to the child will be automatically offset to be centered on the space * assigned by the parent under the assumption that constraints were respected. Rarely, when a * parent really needs to know the true size of the child, they can read this from the * placeable's [Placeable.measuredWidth] and [Placeable.measuredHeight]. * * [MeasureResult] objects are usually created using the [MeasureScope.layout] factory, which * takes the calculated size of this layout, its alignment lines, and a block defining the * positioning of the children layouts. */ fun MeasureScope.measure(measurables: List, constraints: Constraints): MeasureResult /** * The function used to calculate [IntrinsicMeasurable.minIntrinsicWidth]. It represents the * minimum width this layout can take, given a specific height, such that the content of the * layout can be painted correctly. There should be no side-effect from implementers of * [minIntrinsicWidth]. */ fun IntrinsicMeasureScope.minIntrinsicWidth( measurables: List, height: Int, ): Int { val mapped = measurables.fastMap { DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Width) } val constraints = Constraints(maxHeight = height) val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection) val layoutResult = layoutReceiver.measure(mapped, constraints) return layoutResult.width } /** * The function used to calculate [IntrinsicMeasurable.minIntrinsicHeight]. It represents the * minimum height this layout can take, given a specific width, such that the content of the * layout will be painted correctly. There should be no side-effect from implementers of * [minIntrinsicHeight]. */ fun IntrinsicMeasureScope.minIntrinsicHeight( measurables: List, width: Int, ): Int { val mapped = measurables.fastMap { DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Min, IntrinsicWidthHeight.Height) } val constraints = Constraints(maxWidth = width) val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection) val layoutResult = layoutReceiver.measure(mapped, constraints) return layoutResult.height } /** * The function used to calculate [IntrinsicMeasurable.maxIntrinsicWidth]. It represents the * minimum width such that increasing it further will not decrease the minimum intrinsic height. * There should be no side-effects from implementers of [maxIntrinsicWidth]. */ fun IntrinsicMeasureScope.maxIntrinsicWidth( measurables: List, height: Int, ): Int { val mapped = measurables.fastMap { DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Max, IntrinsicWidthHeight.Width) } val constraints = Constraints(maxHeight = height) val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection) val layoutResult = layoutReceiver.measure(mapped, constraints) return layoutResult.width } /** * The function used to calculate [IntrinsicMeasurable.maxIntrinsicHeight]. It represents the * minimum height such that increasing it further will not decrease the minimum intrinsic width. * There should be no side-effects from implementers of [maxIntrinsicHeight]. */ fun IntrinsicMeasureScope.maxIntrinsicHeight( measurables: List, width: Int, ): Int { val mapped = measurables.fastMap { DefaultIntrinsicMeasurable(it, IntrinsicMinMax.Max, IntrinsicWidthHeight.Height) } val constraints = Constraints(maxWidth = width) val layoutReceiver = IntrinsicsMeasureScope(this, layoutDirection) val layoutResult = layoutReceiver.measure(mapped, constraints) return layoutResult.height } } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.ui.layout import androidx.collection.IntList import androidx.collection.MutableOrderedScatterSet import androidx.collection.mutableIntListOf import androidx.collection.mutableIntSetOf import androidx.collection.mutableOrderedScatterSetOf import androidx.collection.mutableScatterMapOf import androidx.compose.runtime.Applier import androidx.compose.runtime.Composable import androidx.compose.runtime.ComposeNodeLifecycleCallback import androidx.compose.runtime.CompositionContext import androidx.compose.runtime.PausableComposition import androidx.compose.runtime.PausedComposition import androidx.compose.runtime.ReusableComposeNode import androidx.compose.runtime.ReusableComposition import androidx.compose.runtime.ReusableContentHost import androidx.compose.runtime.ShouldPauseCallback import androidx.compose.runtime.SideEffect import androidx.compose.runtime.collection.mutableVectorOf import androidx.compose.runtime.currentComposer import androidx.compose.runtime.currentCompositeKeyHashCode import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCompositionContext import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.UiComposable import androidx.compose.ui.internal.checkPrecondition import androidx.compose.ui.internal.requirePrecondition import androidx.compose.ui.internal.throwIllegalStateExceptionForNullCheck import androidx.compose.ui.internal.throwIndexOutOfBoundsException import androidx.compose.ui.layout.SubcomposeLayoutState.PausedPrecomposition import androidx.compose.ui.layout.SubcomposeLayoutState.PrecomposedSlotHandle import androidx.compose.ui.materialize import androidx.compose.ui.node.ComposeUiNode.Companion.ApplyOnDeactivatedNodeAssertion import androidx.compose.ui.node.ComposeUiNode.Companion.SetCompositeKeyHash import androidx.compose.ui.node.ComposeUiNode.Companion.SetModifier import androidx.compose.ui.node.ComposeUiNode.Companion.SetResolvedCompositionLocals import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.node.LayoutNode.LayoutState import androidx.compose.ui.node.LayoutNode.UsageByParent import androidx.compose.ui.node.OutOfFrameExecutor import androidx.compose.ui.node.TraversableNode import androidx.compose.ui.node.TraversableNode.Companion.TraverseDescendantsAction import androidx.compose.ui.node.checkMeasuredSize import androidx.compose.ui.node.requireOwner import androidx.compose.ui.node.traverseDescendants import androidx.compose.ui.platform.createPausableSubcomposition import androidx.compose.ui.platform.createSubcomposition import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastForEach import kotlin.jvm.JvmInline /** * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage for * example to use the values calculated during the measurement as params for the composition of the * children. * * Possible use cases: * * You need to know the constraints passed by the parent during the composition and can't solve * your use case with just custom [Layout] or [LayoutModifier]. See * [androidx.compose.foundation.layout.BoxWithConstraints]. * * You want to use the size of one child during the composition of the second child. * * You want to compose your items lazily based on the available size. For example you have a list * of 100 items and instead of composing all of them you only compose the ones which are currently * visible(say 5 of them) and compose next items when the component is scrolled. * * @sample androidx.compose.ui.samples.SubcomposeLayoutSample * @param modifier [Modifier] to apply for the layout. * @param measurePolicy Measure policy which provides ability to subcompose during the measuring. */ @Composable fun SubcomposeLayout( modifier: Modifier = Modifier, measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult, ) { SubcomposeLayout( state = remember { SubcomposeLayoutState() }, modifier = modifier, measurePolicy = measurePolicy, ) } /** * Analogue of [Layout] which allows to subcompose the actual content during the measuring stage for * example to use the values calculated during the measurement as params for the composition of the * children. * * Possible use cases: * * You need to know the constraints passed by the parent during the composition and can't solve * your use case with just custom [Layout] or [LayoutModifier]. See * [androidx.compose.foundation.layout.BoxWithConstraints]. * * You want to use the size of one child during the composition of the second child. * * You want to compose your items lazily based on the available size. For example you have a list * of 100 items and instead of composing all of them you only compose the ones which are currently * visible(say 5 of them) and compose next items when the component is scrolled. * * @sample androidx.compose.ui.samples.SubcomposeLayoutSample * @param state the state object to be used by the layout. * @param modifier [Modifier] to apply for the layout. * @param measurePolicy Measure policy which provides ability to subcompose during the measuring. */ @Composable @UiComposable fun SubcomposeLayout( state: SubcomposeLayoutState, modifier: Modifier = Modifier, measurePolicy: SubcomposeMeasureScope.(Constraints) -> MeasureResult, ) { val compositeKeyHash = currentCompositeKeyHashCode.hashCode() val compositionContext = rememberCompositionContext() val materialized = currentComposer.materialize(modifier) val localMap = currentComposer.currentCompositionLocalMap ReusableComposeNode>( factory = LayoutNode.Constructor, update = { set(state, state.setRoot) set(compositionContext, state.setCompositionContext) set(measurePolicy, state.setMeasurePolicy) set(localMap, SetResolvedCompositionLocals) reconcile(ApplyOnDeactivatedNodeAssertion) set(materialized, SetModifier) set(compositeKeyHash, SetCompositeKeyHash) }, ) if (!currentComposer.skipping) { SideEffect { state.forceRecomposeChildren() } } } /** * The receiver scope of a [SubcomposeLayout]'s measure lambda which adds ability to dynamically * subcompose a content during the measuring on top of the features provided by [MeasureScope]. */ interface SubcomposeMeasureScope : MeasureScope { /** * Performs subcomposition of the provided [content] with given [slotId]. * * @param slotId unique id which represents the slot we are composing into. If you have fixed * amount or slots you can use enums as slot ids, or if you have a list of items maybe an * index in the list or some other unique key can work. To be able to correctly match the * content between remeasures you should provide the object which is equals to the one you * used during the previous measuring. * @param content the composable content which defines the slot. It could emit multiple layouts, * in this case the returned list of [Measurable]s will have multiple elements. **Note:** When * a [SubcomposeLayout] is in a [LookaheadScope], the subcomposition only happens during the * lookahead pass. In the post-lookahead/main pass, [subcompose] will return the list of * [Measurable]s that were subcomposed during the lookahead pass. If the structure of the * subtree emitted from [content] is dependent on incoming constraints, consider using * constraints received from the lookahead pass for both passes. */ fun subcompose(slotId: Any?, content: @Composable () -> Unit): List } /** * State used by [SubcomposeLayout]. * * [slotReusePolicy] the policy defining what slots should be retained to be reused later. */ class SubcomposeLayoutState(private val slotReusePolicy: SubcomposeSlotReusePolicy) { /** State used by [SubcomposeLayout]. */ constructor() : this(NoOpSubcomposeSlotReusePolicy) /** * State used by [SubcomposeLayout]. * * @param maxSlotsToRetainForReuse when non-zero the layout will keep active up to this count * slots which we were used but not used anymore instead of disposing them. Later when you try * to compose a new slot instead of creating a completely new slot the layout would reuse the * previous slot which allows to do less work especially if the slot contents are similar. */ @Deprecated( "This constructor is deprecated", ReplaceWith( "SubcomposeLayoutState(SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse))", "androidx.compose.ui.layout.SubcomposeSlotReusePolicy", ), ) constructor( maxSlotsToRetainForReuse: Int ) : this(SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse)) private var _state: LayoutNodeSubcompositionsState? = null private val state: LayoutNodeSubcompositionsState get() = requireNotNull(_state) { "SubcomposeLayoutState is not attached to SubcomposeLayout" } // Pre-allocated lambdas to update LayoutNode internal val setRoot: LayoutNode.(SubcomposeLayoutState) -> Unit = { _state = subcompositionsState ?: LayoutNodeSubcompositionsState(this, slotReusePolicy).also { subcompositionsState = it } state.makeSureStateIsConsistent() state.slotReusePolicy = slotReusePolicy } internal val setCompositionContext: LayoutNode.(CompositionContext) -> Unit = { state.compositionContext = it } internal val setMeasurePolicy: LayoutNode.((SubcomposeMeasureScope.(Constraints) -> MeasureResult)) -> Unit = { measurePolicy = state.createMeasurePolicy(it) } /** * Composes the content for the given [slotId]. This makes the next scope.subcompose(slotId) * call during the measure pass faster as the content is already composed. * * If the [slotId] was precomposed already but after the future calculations ended up to not be * needed anymore (meaning this slotId is not going to be used during the measure pass anytime * soon) you can use [PrecomposedSlotHandle.dispose] on a returned object to dispose the * content. * * @param slotId unique id which represents the slot to compose into. * @param content the composable content which defines the slot. * @return [PrecomposedSlotHandle] instance which allows you to dispose the content. */ fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle = state.precompose(slotId, content) /** * Creates [PausedPrecomposition], which allows to perform the composition in an incremental * manner. * * @param slotId unique id which represents the slot to compose into. * @param content the composable content which defines the slot.] * @return [PausedPrecomposition] for the given [slotId]. It allows to perform the composition * in an incremental manner. Performing full or partial precomposition makes the next * scope.subcompose(slotId) call during the measure pass faster as the content is already * composed. */ fun createPausedPrecomposition( slotId: Any?, content: @Composable () -> Unit, ): PausedPrecomposition = state.precomposePaused(slotId, content) internal fun forceRecomposeChildren() = state.forceRecomposeChildren() /** * A [PausedPrecomposition] is a subcomposition that can be composed incrementally as it * supports being paused and resumed. * * Pausable subcomposition can be used between frames to prepare a subcomposition before it is * required by the main composition. For example, this is used in lazy lists to prepare list * items in between frames to that are likely to be scrolled in. The composition is paused when * the start of the next frame is near, allowing composition to be spread across multiple frames * without delaying the production of the next frame. * * @see [PausedComposition] */ sealed interface PausedPrecomposition { /** * Returns `true` when the [PausedPrecomposition] is complete. [isComplete] matches the last * value returned from [resume]. Once a [PausedPrecomposition] is [isComplete] the [apply] * method should be called. If the [apply] method is not called synchronously and * immediately after [resume] returns `true` then this [isComplete] can return `false` as * any state changes read by the paused composition while it is paused will cause the * composition to require the paused composition to need to be resumed before it is used. */ val isComplete: Boolean /** * Resume the composition that has been paused. This method should be called until [resume] * returns `true` or [isComplete] is `true` which has the same result as the last result of * calling [resume]. The [shouldPause] parameter is a lambda that returns whether the * composition should be paused. For example, in lazy lists this returns `false` until just * prior to the next frame starting in which it returns `true` * * Calling [resume] after it returns `true` or when `isComplete` is true will throw an * exception. * * @param shouldPause A lambda that is used to determine if the composition should be * paused. This lambda is called often so should be a very simple calculation. Returning * `true` does not guarantee the composition will pause, it should only be considered a * request to pause the composition. Not all composable functions are pausable and only * pausable composition functions will pause. * @return `true` if the composition is complete and `false` if one or more calls to * `resume` are required to complete composition. */ @Suppress("ExecutorRegistration") fun resume(shouldPause: ShouldPauseCallback): Boolean /** * Apply the composition. This is the last step of a paused composition and is required to * be called prior to the composition is usable. * * Calling [apply] should always be proceeded with a check of [isComplete] before it is * called and potentially calling [resume] in a loop until [isComplete] returns `true`. This * can happen if [resume] returned `true` but [apply] was not synchronously called * immediately afterwords. Any state that was read that changed between when [resume] being * called and [apply] being called may require the paused composition to be resumed before * applied. * * @return [PrecomposedSlotHandle] you can use to premeasure the slot as well, or to dispose * the composed content. */ fun apply(): PrecomposedSlotHandle /** * Cancels the paused composition. This should only be used if the composition is going to * be disposed and the entire composition is not going to be used. */ fun cancel() } /** Instance of this interface is returned by [precompose] function. */ interface PrecomposedSlotHandle { /** * This function allows to dispose the content for the slot which was precomposed previously * via [precompose]. * * If this slot was already used during the regular measure pass via * [SubcomposeMeasureScope.subcompose] this function will do nothing. * * This could be useful if after the future calculations this item is not anymore expected * to be used during the measure pass anytime soon. */ fun dispose() /** The amount of placeables composed into this slot. */ val placeablesCount: Int get() = 0 /** * Performs synchronous measure of the placeable at the given [index]. * * @param index the placeable index. Should be smaller than [placeablesCount]. * @param constraints Constraints to measure this placeable with. */ fun premeasure(index: Int, constraints: Constraints) {} /** * Conditionally executes [block] for each [Modifier.Node] of this Composition that is a * [TraversableNode] with a matching [key]. * * See [androidx.compose.ui.node.traverseDescendants] for the complete semantics of this * function. */ fun traverseDescendants(key: Any?, block: (TraversableNode) -> TraverseDescendantsAction) {} /** * Retrieves the latest measured size for a given placeable [index]. This will return * [IntSize.Zero] if this is called before [premeasure]. */ fun getSize(index: Int): IntSize = IntSize.Zero } } /** * This policy allows [SubcomposeLayout] to retain some of slots which we were used but not used * anymore instead of disposing them. Next time when you try to compose a new slot instead of * creating a completely new slot the layout would reuse the kept slot. This allows to do less work * especially if the slot contents are similar. */ interface SubcomposeSlotReusePolicy { /** * This function will be called with [slotIds] set populated with the slot ids available to * reuse. In the implementation you can remove slots you don't want to retain. */ fun getSlotsToRetain(slotIds: SlotIdsSet) /** * Returns true if the content previously composed with [reusableSlotId] is compatible with the * content which is going to be composed for [slotId]. Slots could be considered incompatible if * they display completely different types of the UI. */ fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean /** * Set containing slot ids currently available to reuse. Used by [getSlotsToRetain]. The set * retains the insertion order of its elements, guaranteeing stable iteration order. * * This class works exactly as [MutableSet], but doesn't allow to add new items in it. */ class SlotIdsSet internal constructor( @PublishedApi internal val set: MutableOrderedScatterSet = mutableOrderedScatterSetOf() ) : Collection { override val size: Int get() = set.size override fun isEmpty(): Boolean = set.isEmpty() override fun containsAll(elements: Collection): Boolean { elements.forEach { element -> if (element !in set) { return false } } return true } override fun contains(element: Any?): Boolean = set.contains(element) internal fun add(slotId: Any?) = set.add(slotId) override fun iterator(): MutableIterator = set.asMutableSet().iterator() /** * Removes a [slotId] from this set, if it is present. * * @return `true` if the slot id was removed, `false` if the set was not modified. */ fun remove(slotId: Any?): Boolean = set.remove(slotId) /** * Removes all slot ids from [slotIds] that are also contained in this set. * * @return `true` if any slot id was removed, `false` if the set was not modified. */ fun removeAll(slotIds: Collection): Boolean = set.remove(slotIds) /** * Removes all slot ids that match the given [predicate]. * * @return `true` if any slot id was removed, `false` if the set was not modified. */ fun removeAll(predicate: (Any?) -> Boolean): Boolean { val size = set.size set.removeIf(predicate) return size != set.size } /** * Retains only the slot ids that are contained in [slotIds]. * * @return `true` if any slot id was removed, `false` if the set was not modified. */ fun retainAll(slotIds: Collection): Boolean = set.retainAll(slotIds) /** * Retains only slotIds that match the given [predicate]. * * @return `true` if any slot id was removed, `false` if the set was not modified. */ fun retainAll(predicate: (Any?) -> Boolean): Boolean = set.retainAll(predicate) /** Removes all slot ids from this set. */ fun clear() = set.clear() /** * Remove entries until [size] equals [maxSlotsToRetainForReuse]. Entries inserted last are * removed first. */ fun trimToSize(maxSlotsToRetainForReuse: Int) = set.trimToSize(maxSlotsToRetainForReuse) /** * Iterates over every element stored in this set by invoking the specified [block] lambda. * The iteration order is the same as the insertion order. It is safe to remove the element * passed to [block] during iteration. * * NOTE: This method is obscured by `Collection.forEach` since it is marked with * * @HidesMember, which means in practice this will never get called. Please use * [fastForEach] instead. */ fun forEach(block: (Any?) -> Unit) = set.forEach(block) /** * Iterates over every element stored in this set by invoking the specified [block] lambda. * The iteration order is the same as the insertion order. It is safe to remove the element * passed to [block] during iteration. * * NOTE: this method was added in order to allow for a more performant forEach method. It is * necessary because [forEach] is obscured by `Collection.forEach` since it is marked * with @HidesMember. */ inline fun fastForEach(block: (Any?) -> Unit) = set.forEach(block) } } /** * Creates [SubcomposeSlotReusePolicy] which retains the fixed amount of slots. * * @param maxSlotsToRetainForReuse the [SubcomposeLayout] will retain up to this amount of slots. */ fun SubcomposeSlotReusePolicy(maxSlotsToRetainForReuse: Int): SubcomposeSlotReusePolicy = FixedCountSubcomposeSlotReusePolicy(maxSlotsToRetainForReuse) /** * The inner state containing all the information about active slots and their compositions. It is * stored inside LayoutNode object as in fact we need to keep 1-1 mapping between this state and the * node: when we compose a slot we first create a virtual LayoutNode child to this node and then * save the extra information inside this state. Keeping this state inside LayoutNode also helps us * to retain the pool of reusable slots even when a new SubcomposeLayoutState is applied to * SubcomposeLayout and even when the SubcomposeLayout's LayoutNode is reused via the * ReusableComposeNode mechanism. */ @OptIn(ExperimentalComposeUiApi::class) internal class LayoutNodeSubcompositionsState( private val root: LayoutNode, slotReusePolicy: SubcomposeSlotReusePolicy, ) : ComposeNodeLifecycleCallback { var compositionContext: CompositionContext? = null var slotReusePolicy: SubcomposeSlotReusePolicy = slotReusePolicy set(value) { if (field !== value) { field = value // the new policy will be applied after measure markActiveNodesAsReused(deactivate = false) root.requestRemeasure() } } private var currentIndex = 0 private var currentApproachIndex = 0 private val nodeToNodeState = mutableScatterMapOf() // this map contains active slotIds (without precomposed or reusable nodes) private val slotIdToNode = mutableScatterMapOf() private val scope = Scope() private val approachMeasureScope = ApproachMeasureScopeImpl() private val precomposeMap = mutableScatterMapOf() private val reusableSlotIdsSet = SubcomposeSlotReusePolicy.SlotIdsSet() // SlotHandles precomposed in the approach pass. These slot handles are owned by the approach // pass, hence the approach pass is responsible for disposing them when they are no longer // needed. Note: if `precompose` is called on a slot owned by the approach pass, the // approach will yield ownership to the new caller. When the new caller disposes a slot // that is still needed by approach, the approach pass will be triggered to create // and own the slot. private val approachPrecomposeSlotHandleMap = mutableScatterMapOf() // Slot ids of compositions needed in the approach pass. These compositions are either owned // by the approach pass, or by the caller of [SubcomposeLayoutState#precompose]. For // compositions not created by the approach pass, if they are disposed while the approach pass // still needs it, the approach pass will be triggered to re-create the composition. // The valid slot ids are stored between 0 and currentApproachIndex - 1, beyond index // currentApproachIndex are [UnspecifiedSlotId]s. private val slotIdsOfCompositionsNeededInApproach = mutableVectorOf() /** * `root.foldedChildren` list consist of: * 1) all the active children (used during the last measure pass) * 2) `reusableCount` nodes in the middle of the list which were active and stopped being used. * now we keep them (up to `maxCountOfSlotsToReuse`) in order to reuse next time we will need * to compose a new item * 4) `precomposedCount` nodes in the end of the list which were precomposed and are waiting to * be used during the next measure passes. */ private var reusableCount = 0 private var precomposedCount = 0 override fun onReuse() { markActiveNodesAsReused(deactivate = false) } override fun onDeactivate() { markActiveNodesAsReused(deactivate = true) } override fun onRelease() { disposeCurrentNodes() } fun subcompose(slotId: Any?, content: @Composable () -> Unit): List { makeSureStateIsConsistent() val layoutState = root.layoutState checkPrecondition( layoutState == LayoutState.Measuring || layoutState == LayoutState.LayingOut || layoutState == LayoutState.LookaheadMeasuring || layoutState == LayoutState.LookaheadLayingOut ) { "subcompose can only be used inside the measure or layout blocks" } val node = slotIdToNode.getOrPut(slotId) { val precomposed = precomposeMap.remove(slotId) if (precomposed != null) { val nodeState = nodeToNodeState[precomposed] if (ExtraLoggingEnabled) { nodeState?.record(SLOperation.TookFromPrecomposeMap) } @Suppress("ExceptionMessage") checkPrecondition(precomposedCount > 0) precomposedCount-- precomposed } else { takeNodeFromReusables(slotId) ?: createNodeAt(currentIndex) } } if (root.foldedChildren.getOrNull(currentIndex) !== node) { // the node has a new index in the list val itemIndex = root.foldedChildren.indexOf(node) requirePrecondition(itemIndex >= currentIndex) { "Key \"$slotId\" was already used. If you are using LazyColumn/Row please make " + "sure you provide a unique key for each item." } if (currentIndex != itemIndex) { move(itemIndex, currentIndex) } } currentIndex++ subcompose(node, slotId, pausable = false, content) return if (layoutState == LayoutState.Measuring || layoutState == LayoutState.LayingOut) { node.childMeasurables } else { node.childLookaheadMeasurables } } // This may be called in approach pass, if a node is only emitted in the approach pass, but // not in the lookahead pass. private fun subcompose( node: LayoutNode, slotId: Any?, pausable: Boolean, content: @Composable () -> Unit, ) { val nodeState = nodeToNodeState.getOrPut(node) { NodeState(slotId, {}) } val contentChanged = nodeState.content !== content if (nodeState.pausedComposition != null) { if (contentChanged) { // content did change so it is not safe to apply the current paused composition. nodeState.cancelPausedPrecomposition() } else if (pausable) { // the paused composition is initialized and the content didn't change return } else { // we can apply as we are still composing the same content. nodeState.applyPausedPrecomposition(shouldComplete = true) } } val hasPendingChanges = nodeState.composition?.hasInvalidations ?: true if (contentChanged || hasPendingChanges || nodeState.forceRecompose) { nodeState.content = content subcompose(node, nodeState, pausable) nodeState.forceRecompose = false } } private val outOfFrameExecutor: OutOfFrameExecutor? get() = root.requireOwner().outOfFrameExecutor private fun subcompose(node: LayoutNode, nodeState: NodeState, pausable: Boolean) { requirePrecondition(nodeState.pausedComposition == null) { "new subcompose call while paused composition is still active" } Snapshot.withoutReadObservation { ignoreRemeasureRequests { val existing = nodeState.composition val parentComposition = compositionContext ?: throwIllegalStateExceptionForNullCheck( "parent composition reference not set" ) if (ExtraLoggingEnabled) { nodeState.record( if (existing == null) SLOperation.SubcomposeNew else SLOperation.Subcompose ) if (pausable) { nodeState.record(SLOperation.SubcomposePausable) } if (nodeState.forceReuse) { nodeState.record(SLOperation.SubcomposeForceReuse) } } val composition = if (existing == null || existing.isDisposed) { if (pausable) { createPausableSubcomposition(node, parentComposition) } else { createSubcomposition(node, parentComposition) } } else { existing } nodeState.composition = composition val content = nodeState.content val composable: @Composable () -> Unit = if (outOfFrameExecutor != null) { nodeState.composedWithReusableContentHost = false content } else { nodeState.composedWithReusableContentHost = true { ReusableContentHost(nodeState.active, content) } } if (pausable) { composition as PausableComposition if (nodeState.forceReuse) { nodeState.pausedComposition = composition.setPausableContentWithReuse(composable) } else { nodeState.pausedComposition = composition.setPausableContent(composable) } } else { if (nodeState.forceReuse) { composition.setContentWithReuse(composable) } else { composition.setContent(composable) } } nodeState.forceReuse = false } } } private fun getSlotIdAtIndex(foldedChildren: List, index: Int): Any? { val node = foldedChildren[index] return nodeToNodeState[node]!!.slotId } fun disposeOrReuseStartingFromIndex(startIndex: Int) { reusableCount = 0 val foldedChildren = root.foldedChildren val lastReusableIndex = foldedChildren.size - precomposedCount - 1 var needApplyNotification = false if (startIndex <= lastReusableIndex) { // construct the set of available slot ids reusableSlotIdsSet.clear() for (i in startIndex..lastReusableIndex) { val slotId = getSlotIdAtIndex(foldedChildren, i) reusableSlotIdsSet.add(slotId) } slotReusePolicy.getSlotsToRetain(reusableSlotIdsSet) // iterating backwards so it is easier to remove items var i = lastReusableIndex Snapshot.withoutReadObservation { while (i >= startIndex) { val node = foldedChildren[i] val nodeState = nodeToNodeState[node]!! val slotId = nodeState.slotId if (slotId in reusableSlotIdsSet) { reusableCount++ if (nodeState.active) { node.resetLayoutState() nodeState.reuseComposition(forceDeactivate = false) if (nodeState.composedWithReusableContentHost) { needApplyNotification = true } } } else { ignoreRemeasureRequests { nodeToNodeState.remove(node) nodeState.composition?.dispose() root.removeAt(i, 1) } } // remove it from slotIdToNode so it is not considered active slotIdToNode.remove(slotId) i-- } } } if (needApplyNotification) { Snapshot.sendApplyNotifications() } makeSureStateIsConsistent() } private fun NodeState.deactivateOutOfFrame(executor: OutOfFrameExecutor) { executor.schedule { if (!active) { if (ExtraLoggingEnabled) { record(SLOperation.DeactivateOutOfFrame) } composition?.deactivate() } else { if (ExtraLoggingEnabled) { record(SLOperation.DeactivateOutOfFrameCancelled) } } } } private fun markActiveNodesAsReused(deactivate: Boolean) { precomposedCount = 0 precomposeMap.clear() val foldedChildren = root.foldedChildren val childCount = foldedChildren.size if (reusableCount != childCount) { reusableCount = childCount Snapshot.withoutReadObservation { for (i in 0 until childCount) { val node = foldedChildren[i] val nodeState = nodeToNodeState[node] if (nodeState != null && nodeState.active) { node.resetLayoutState() nodeState.reuseComposition(forceDeactivate = deactivate) nodeState.slotId = ReusedSlotId if (ExtraLoggingEnabled) { if (deactivate) { nodeState.record(SLOperation.SlotToReusedFromOnDeactivate) } else { nodeState.record(SLOperation.SlotToReusedFromOnReuse) } } } } } slotIdToNode.clear() } makeSureStateIsConsistent() } private fun disposeCurrentNodes() { root.ignoreRemeasureRequests { nodeToNodeState.forEachValue { it.composition?.dispose() } root.removeAll() } nodeToNodeState.clear() slotIdToNode.clear() precomposedCount = 0 reusableCount = 0 precomposeMap.clear() makeSureStateIsConsistent() } fun makeSureStateIsConsistent() { val childrenCount = root.foldedChildren.size requirePrecondition(nodeToNodeState.size == childrenCount) { "Inconsistency between the count of nodes tracked by the state " + "(${nodeToNodeState.size}) and the children count on the SubcomposeLayout" + " ($childrenCount). Are you trying to use the state of the" + " disposed SubcomposeLayout?" } requirePrecondition(childrenCount - reusableCount - precomposedCount >= 0) { "Incorrect state. Total children $childrenCount. Reusable children " + "$reusableCount. Precomposed children $precomposedCount" } requirePrecondition(precomposeMap.size == precomposedCount) { "Incorrect state. Precomposed children $precomposedCount. Map size " + "${precomposeMap.size}" } } private fun LayoutNode.resetLayoutState() { measurePassDelegate.measuredByParent = UsageByParent.NotUsed lookaheadPassDelegate?.let { it.measuredByParent = UsageByParent.NotUsed } } private fun takeNodeFromReusables(slotId: Any?): LayoutNode? { if (reusableCount == 0) { return null } val foldedChildren = root.foldedChildren val reusableNodesSectionEnd = foldedChildren.size - precomposedCount val reusableNodesSectionStart = reusableNodesSectionEnd - reusableCount var index = reusableNodesSectionEnd - 1 var chosenIndex = -1 // first try to find a node with exactly the same slotId while (index >= reusableNodesSectionStart) { if (getSlotIdAtIndex(foldedChildren, index) == slotId) { // we have a node with the same slotId chosenIndex = index break } else { index-- } } if (chosenIndex == -1) { // try to find a first compatible slotId from the end of the section index = reusableNodesSectionEnd - 1 while (index >= reusableNodesSectionStart) { val node = foldedChildren[index] val nodeState = nodeToNodeState[node]!! if ( nodeState.slotId === ReusedSlotId || slotReusePolicy.areCompatible(slotId, nodeState.slotId) ) { nodeState.slotId = slotId chosenIndex = index break } index-- } } return if (chosenIndex == -1) { // no compatible nodes found null } else { if (index != reusableNodesSectionStart) { // we need to rearrange the items move(index, reusableNodesSectionStart, 1) } reusableCount-- val node = foldedChildren[reusableNodesSectionStart] val nodeState = nodeToNodeState[node]!! // create a new instance to avoid change notifications if (ExtraLoggingEnabled) { nodeState.record(SLOperation.Reused) } nodeState.activeState = mutableStateOf(true) nodeState.forceReuse = true nodeState.forceRecompose = true node } } fun createMeasurePolicy( block: SubcomposeMeasureScope.(Constraints) -> MeasureResult ): MeasurePolicy { return object : LayoutNode.NoIntrinsicsMeasurePolicy(error = NoIntrinsicsMessage) { override fun MeasureScope.measure( measurables: List, constraints: Constraints, ): MeasureResult { scope.layoutDirection = layoutDirection scope.density = density scope.fontScale = fontScale if (!isLookingAhead && root.lookaheadRoot != null) { // Approach pass currentApproachIndex = 0 val result = approachMeasureScope.block(constraints) val indexAfterMeasure = currentApproachIndex return createMeasureResult(result) { currentApproachIndex = indexAfterMeasure result.placeChildren() // dispose disposeUnusedSlotsInApproach() disposeOrReuseStartingFromIndex(currentIndex) } } else { // Lookahead pass, or the main pass if not in a lookahead scope. currentIndex = 0 val result = scope.block(constraints) val indexAfterMeasure = currentIndex return createMeasureResult(result) { currentIndex = indexAfterMeasure result.placeChildren() if (root.lookaheadRoot == null) { // If this is in lookahead scope, we need to dispose *after* // approach placement, to give approach pass the opportunity to // transfer the ownership of subcompositions before disposing. disposeOrReuseStartingFromIndex(currentIndex) } } } } } } private fun disposeUnusedSlotsInApproach() { // Iterate over the slots owned by approach, and dispose slots if neither lookahead // nor approach needs it. approachPrecomposeSlotHandleMap.removeIf { slotId, handle -> val id = slotIdsOfCompositionsNeededInApproach.indexOf(slotId) if (id < 0 || id >= currentApproachIndex) { if (id >= 0) { // Remove the slotId from the list before disposing slotIdsOfCompositionsNeededInApproach[id] = UnspecifiedSlotId } if (precomposeMap.contains(slotId)) { // Node has not been needed by lookahead, or approach. handle.dispose() } true } else { false } } } private inline fun createMeasureResult( result: MeasureResult, crossinline placeChildrenBlock: () -> Unit, ) = object : MeasureResult by result { override fun placeChildren() { placeChildrenBlock() } } private val NoIntrinsicsMessage = "Asking for intrinsic measurements of SubcomposeLayout " + "layouts is not supported. This includes components that are built on top of " + "SubcomposeLayout, such as lazy lists, BoxWithConstraints, TabRow, etc. To mitigate " + "this:\n" + "- if intrinsic measurements are used to achieve 'match parent' sizing, consider " + "replacing the parent of the component with a custom layout which controls the order in " + "which children are measured, making intrinsic measurement not needed\n" + "- adding a size modifier to the component, in order to fast return the queried " + "intrinsic measurement." fun precompose(slotId: Any?, content: @Composable () -> Unit): PrecomposedSlotHandle { precompose(slotId, content, pausable = false) return createPrecomposedSlotHandle(slotId) } private fun precompose(slotId: Any?, content: @Composable () -> Unit, pausable: Boolean) { if (!root.isAttached) { return } makeSureStateIsConsistent() if (!slotIdToNode.containsKey(slotId)) { // Yield ownership of PrecomposedHandle from approach to the caller of precompose approachPrecomposeSlotHandleMap.remove(slotId) val node = precomposeMap.getOrPut(slotId) { val reusedNode = takeNodeFromReusables(slotId) if (reusedNode != null) { // now move this node to the end where we keep precomposed items val nodeIndex = root.foldedChildren.indexOf(reusedNode) move(nodeIndex, root.foldedChildren.size, 1) precomposedCount++ reusedNode } else { createNodeAt(root.foldedChildren.size).also { precomposedCount++ } } } subcompose(node, slotId, pausable = pausable, content) } } private fun NodeState.reuseComposition(forceDeactivate: Boolean) { if (!forceDeactivate && composedWithReusableContentHost) { // Deactivation through ReusableContentHost is controlled with the active flag active = false } else { // Otherwise, create a new instance to avoid state change notifications activeState = mutableStateOf(false) } if (pausedComposition != null) { // Cancelling disposes composition, so no additional work is needed. cancelPausedPrecomposition() } else if (forceDeactivate) { if (ExtraLoggingEnabled) { record(SLOperation.ReuseForceSyncDeactivation) } composition?.deactivate() } else { val outOfFrameExecutor = outOfFrameExecutor if (outOfFrameExecutor != null) { if (ExtraLoggingEnabled) { record(SLOperation.ReuseScheduleOutOfFrameDeactivation) } deactivateOutOfFrame(outOfFrameExecutor) } else { if (!composedWithReusableContentHost) { if (ExtraLoggingEnabled) { record(SLOperation.ReuseSyncDeactivation) } composition?.deactivate() } else if (ExtraLoggingEnabled) { record(SLOperation.ReuseDeactivationViaHost) } } } } private fun NodeState.cancelPausedPrecomposition() { pausedComposition?.let { it.cancel() pausedComposition = null composition?.dispose() composition = null if (ExtraLoggingEnabled) { record(SLOperation.CancelPausedPrecomposition) } } } private fun disposePrecomposedSlot(slotId: Any?) { makeSureStateIsConsistent() val node = precomposeMap.remove(slotId) if (node != null) { checkPrecondition(precomposedCount > 0) { "No pre-composed items to dispose" } val itemIndex = root.foldedChildren.indexOf(node) checkPrecondition(itemIndex >= root.foldedChildren.size - precomposedCount) { "Item is not in pre-composed item range" } // move this item into the reusable section reusableCount++ precomposedCount-- nodeToNodeState[node]?.cancelPausedPrecomposition() val reusableStart = root.foldedChildren.size - precomposedCount - reusableCount move(itemIndex, reusableStart, 1) disposeOrReuseStartingFromIndex(reusableStart) } // If the slot is not owned by approach (e.g. created for prefetch) and disposed before // approach finishes using it, the approach pass will be invoked to re-create the // composition if needed. if (slotIdsOfCompositionsNeededInApproach.contains(slotId)) { root.requestRemeasure(true) } } private fun createPrecomposedSlotHandle(slotId: Any?): PrecomposedSlotHandle { if (!root.isAttached) { return object : PrecomposedSlotHandle { override fun dispose() {} } } return object : PrecomposedSlotHandle { // Saves indices of placeables that have been premeasured in this handle val hasPremeasured = mutableIntSetOf() override fun dispose() { disposePrecomposedSlot(slotId) } override val placeablesCount: Int get() = precomposeMap[slotId]?.children?.size ?: 0 override fun premeasure(index: Int, constraints: Constraints) { val node = precomposeMap[slotId] if (node != null && node.isAttached) { val size = node.children.size if (index < 0 || index >= size) { throwIndexOutOfBoundsException( "Index ($index) is out of bound of [0, $size)" ) } requirePrecondition(!node.isPlaced) { "Pre-measure called on node that is not placed" } root.ignoreRemeasureRequests { node.requireOwner().measureAndLayout(node.children[index], constraints) } hasPremeasured.add(index) } } override fun traverseDescendants( key: Any?, block: (TraversableNode) -> TraverseDescendantsAction, ) { precomposeMap[slotId]?.nodes?.head?.traverseDescendants(key, block) } override fun getSize(index: Int): IntSize { val node = precomposeMap[slotId] if (node != null && node.isAttached) { val size = node.children.size if (index < 0 || index >= size) { throwIndexOutOfBoundsException( "Index ($index) is out of bound of [0, $size)" ) } if (hasPremeasured.contains(index)) { return IntSize(node.children[index].width, node.children[index].height) } } return IntSize.Zero } } } fun precomposePaused(slotId: Any?, content: @Composable () -> Unit): PausedPrecomposition { if (!root.isAttached) { return object : PausedPrecompositionImpl { override val isComplete: Boolean = true override fun resume(shouldPause: ShouldPauseCallback) = true override fun apply() = createPrecomposedSlotHandle(slotId) override fun cancel() {} } } precompose(slotId, content, pausable = true) return object : PausedPrecompositionImpl { override fun cancel() { if (nodeState?.pausedComposition != null) { // only dispose if the paused composition is still waiting to be applied disposePrecomposedSlot(slotId) } } private val nodeState: NodeState? get() = precomposeMap[slotId]?.let { nodeToNodeState[it] } override val isComplete: Boolean get() = nodeState?.pausedComposition?.isComplete ?: true override fun resume(shouldPause: ShouldPauseCallback): Boolean { val nodeState = nodeState val pausedComposition = nodeState?.pausedComposition return if (pausedComposition != null && !pausedComposition.isComplete) { if (ExtraLoggingEnabled) { nodeState.record(SLOperation.ResumePaused) } val isComplete = Snapshot.withoutReadObservation { try { pausedComposition.resume(shouldPause) } catch (e: Throwable) { val operations = nodeState.operations if (operations != null) { throw SubcomposeLayoutPausableCompositionException( nodeState.operations, slotId, e, ) } else { throw e } } } if (ExtraLoggingEnabled && !isComplete) { nodeState.record(SLOperation.PausePaused) } isComplete } else { true } } override fun apply(): PrecomposedSlotHandle { nodeState?.applyPausedPrecomposition(shouldComplete = false) return createPrecomposedSlotHandle(slotId) } } } fun forceRecomposeChildren() { val childCount = root.foldedChildren.size if (reusableCount != childCount) { // only invalidate children if there are any non-reused ones // in other cases, all of them are going to be invalidated later anyways nodeToNodeState.forEachValue { nodeState -> nodeState.forceRecompose = true } if (root.lookaheadRoot != null) { // If the SubcomposeLayout is in a LookaheadScope, request for a lookahead measure // so that lookahead gets triggered again to recompose children. if (!root.lookaheadMeasurePending) { root.requestLookaheadRemeasure() } } else { if (!root.measurePending) { root.requestRemeasure() } } } } private fun createNodeAt(index: Int) = LayoutNode(isVirtual = true).also { node -> ignoreRemeasureRequests { root.insertAt(index, node) } } private fun move(from: Int, to: Int, count: Int = 1) { ignoreRemeasureRequests { root.move(from, to, count) } } private inline fun ignoreRemeasureRequests(block: () -> T): T = root.ignoreRemeasureRequests(block) private fun NodeState.applyPausedPrecomposition(shouldComplete: Boolean) { val pausedComposition = pausedComposition if (pausedComposition != null) { Snapshot.withoutReadObservation { ignoreRemeasureRequests { try { if (shouldComplete) { while (!pausedComposition.isComplete) { pausedComposition.resume { false } } } pausedComposition.apply() } catch (e: Throwable) { val operations = operations if (operations != null) { throw SubcomposeLayoutPausableCompositionException( operations, slotId, e, ) } else { throw e } } this.pausedComposition = null } } } } private class NodeState( var slotId: Any?, var content: @Composable () -> Unit, var composition: ReusableComposition? = null, ) { var forceRecompose = false var forceReuse = false var pausedComposition: PausedComposition? = null var activeState = mutableStateOf(true) var composedWithReusableContentHost = false var active: Boolean get() = activeState.value set(value) { activeState.value = value } val operations = if (ExtraLoggingEnabled) mutableIntListOf() else null fun record(op: SLOperation) { val operations = operations ?: return operations.add(op.value) if (operations.size >= 50) { operations.removeRange(0, 10) } } } private inner class Scope : SubcomposeMeasureScope { // MeasureScope delegation override var layoutDirection: LayoutDirection = LayoutDirection.Rtl override var density: Float = 0f override var fontScale: Float = 0f override val isLookingAhead: Boolean get() = root.layoutState == LayoutState.LookaheadLayingOut || root.layoutState == LayoutState.LookaheadMeasuring override fun subcompose(slotId: Any?, content: @Composable () -> Unit) = this@LayoutNodeSubcompositionsState.subcompose(slotId, content) override fun layout( width: Int, height: Int, alignmentLines: Map, rulers: (RulerScope.() -> Unit)?, placementBlock: Placeable.PlacementScope.() -> Unit, ): MeasureResult { checkMeasuredSize(width, height) return object : MeasureResult { override val width: Int get() = width override val height: Int get() = height override val alignmentLines: Map get() = alignmentLines override val rulers: (RulerScope.() -> Unit)? get() = rulers override fun placeChildren() { if (isLookingAhead) { val delegate = root.innerCoordinator.lookaheadDelegate if (delegate != null) { delegate.placementScope.placementBlock() return } } root.innerCoordinator.placementScope.placementBlock() } } } } private inner class ApproachMeasureScopeImpl : SubcomposeMeasureScope, MeasureScope by scope { /** * This function retrieves [Measurable]s created for [slotId] based on the subcomposition * that happened in the lookahead pass. If [slotId] was not subcomposed in the lookahead * pass, [subcompose] will return an [emptyList]. */ override fun subcompose(slotId: Any?, content: @Composable () -> Unit): List { val nodeInSlot = slotIdToNode[slotId] if (nodeInSlot != null && root.foldedChildren.indexOf(nodeInSlot) < currentIndex) { // Check that the node has been composed in lookahead. Otherwise, we need to // compose the node in approach pass via approachSubcompose. return nodeInSlot.childMeasurables } else { return approachSubcompose(slotId, content) } } } private fun approachSubcompose( slotId: Any?, content: @Composable () -> Unit, ): List { requirePrecondition(slotIdsOfCompositionsNeededInApproach.size >= currentApproachIndex) { "Error: currentApproachIndex cannot be greater than the size of the" + "approachComposedSlotIds list." } val nodeForSlot = slotIdToNode[slotId] if (slotIdsOfCompositionsNeededInApproach.size == currentApproachIndex) { slotIdsOfCompositionsNeededInApproach.add(slotId) } else { slotIdsOfCompositionsNeededInApproach[currentApproachIndex] = slotId } currentApproachIndex++ val precomposed = precomposeMap.contains(slotId) if (!precomposed && nodeForSlot == null) { // The slot was not composed in the lookahead pass. And it has not been pre-composed in // the approach pass. Hence, we will precompose it for the approach pass, and track it // in approachPrecomposeSlotHandleMap so that it can be disposed when no longer needed // in approach. precompose(slotId, content).also { approachPrecomposeSlotHandleMap[slotId] = it } } else { // A non-null `nodeForSlot` here means that the slot was composed in lookahead // initially, but no longer needed && has not been disposed yet. // Move from lookahead composed to pre-composed, so that it can be disposed when // no longer needed in approach. if (!precomposed && nodeForSlot != null) { // Transfer ownership of the subcomposition from lookahead pass to approach pass. // As a result, the composition can be disposed as soon as approach pass no // longer needs it. // First, move this node to the end where we keep precomposed items val nodeIndex = root.foldedChildren.indexOf(nodeForSlot) move(nodeIndex, root.foldedChildren.size, 1) precomposedCount++ // Remove the slotId from slotIdToNode so that if lookahead were to subcompose // this item, it'll need to take the node out of precomposeMap. slotIdToNode.remove(slotId) precomposeMap[slotId] = nodeForSlot approachPrecomposeSlotHandleMap[slotId] = createPrecomposedSlotHandle(slotId) if (root.isAttached) { makeSureStateIsConsistent() } } // Re-subcompose if needed based on forceRecompose val node = precomposeMap[slotId] val nodeState = node?.let { nodeToNodeState[it] } if (nodeState?.forceRecompose == true) { subcompose(node, slotId, pausable = false, content) } // Finish pausable composition if it has not been completed yet if (nodeState?.pausedComposition != null) { nodeState.applyPausedPrecomposition(shouldComplete = true) } } return precomposeMap[slotId]?.run { measurePassDelegate.childDelegates.also { it.fastForEach { delegate -> delegate.markDetachedFromParentLookaheadPass() } } } ?: emptyList() } } private val ReusedSlotId = object { override fun toString(): String = "ReusedSlotId" } private class FixedCountSubcomposeSlotReusePolicy(private val maxSlotsToRetainForReuse: Int) : SubcomposeSlotReusePolicy { override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) { if (slotIds.size > maxSlotsToRetainForReuse) { slotIds.trimToSize(maxSlotsToRetainForReuse) } } override fun areCompatible(slotId: Any?, reusableSlotId: Any?): Boolean = true } private object NoOpSubcomposeSlotReusePolicy : SubcomposeSlotReusePolicy { override fun getSlotsToRetain(slotIds: SubcomposeSlotReusePolicy.SlotIdsSet) { slotIds.clear() } override fun areCompatible(slotId: Any?, reusableSlotId: Any?) = false } private interface PausedPrecompositionImpl : PausedPrecomposition private val UnspecifiedSlotId = Any() @JvmInline private value class SLOperation(val value: Int) { companion object { val CancelPausedPrecomposition = SLOperation(0) val ReuseForceSyncDeactivation = SLOperation(1) val ReuseScheduleOutOfFrameDeactivation = SLOperation(2) val ReuseSyncDeactivation = SLOperation(3) val ReuseDeactivationViaHost = SLOperation(4) val TookFromPrecomposeMap = SLOperation(5) val Subcompose = SLOperation(6) val SubcomposeNew = SLOperation(7) val SubcomposePausable = SLOperation(8) val SubcomposeForceReuse = SLOperation(9) val DeactivateOutOfFrame = SLOperation(10) val DeactivateOutOfFrameCancelled = SLOperation(11) val SlotToReusedFromOnDeactivate = SLOperation(12) val SlotToReusedFromOnReuse = SLOperation(13) val Reused = SLOperation(14) val ResumePaused = SLOperation(15) val PausePaused = SLOperation(16) val ApplyPaused = SLOperation(17) } } private class SubcomposeLayoutPausableCompositionException( private val operations: IntList, private val slotId: Any?, cause: Throwable?, ) : IllegalStateException(cause) { private fun operationsList(): List = buildList { var currentOperation = operations.size - 1 while (currentOperation >= 0) { val operation = operations[currentOperation] val stringValue = when (SLOperation(operation)) { SLOperation.CancelPausedPrecomposition -> "CancelPausedPrecomposition" SLOperation.ReuseForceSyncDeactivation -> "ReuseForceSyncDeactivation" SLOperation.ReuseScheduleOutOfFrameDeactivation -> "ReuseScheduleOutOfFrameDeactivation" SLOperation.ReuseSyncDeactivation -> "ReuseSyncDeactivation" SLOperation.ReuseDeactivationViaHost -> "ReuseDeactivationViaHost" SLOperation.TookFromPrecomposeMap -> "TookFromPrecomposeMap" SLOperation.Subcompose -> "Subcompose" SLOperation.SubcomposeNew -> "SubcomposeNew" SLOperation.SubcomposePausable -> "SubcomposePausable" SLOperation.SubcomposeForceReuse -> "SubcomposeForceReuse" SLOperation.DeactivateOutOfFrame -> "DeactivateOutOfFrame" SLOperation.DeactivateOutOfFrameCancelled -> "DeactivateOutOfFrameCancelled" SLOperation.SlotToReusedFromOnDeactivate -> "SlotToReusedFromOnDeactivate" SLOperation.SlotToReusedFromOnReuse -> "SlotToReusedFromOnReuse" SLOperation.Reused -> "Reused" SLOperation.ResumePaused -> "ResumePaused" SLOperation.PausePaused -> "PausePaused" SLOperation.ApplyPaused -> "ApplyPaused" else -> "Unexpected $operation" } add("$currentOperation: $stringValue") currentOperation-- } } @Suppress("ListIterator") override val message: String? get() = """ |slotid=$slotId. Last operations: |${operationsList().joinToString("\n")} """ .trimMargin() } private const val ExtraLoggingEnabled = false ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/IntrinsicMeasurable.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.ui.layout /** * A part of the composition that can be measured. This represents a layout. The instance should * never be stored. */ interface IntrinsicMeasurable { /** Data provided by the [ParentDataModifier]. */ val parentData: Any? /** * Calculates the minimum width that the layout can be such that the content of the layout will * be painted correctly. There should be no side-effects from a call to [minIntrinsicWidth]. */ fun minIntrinsicWidth(height: Int): Int /** * Calculates the smallest width beyond which increasing the width never decreases the height. * There should be no side-effects from a call to [maxIntrinsicWidth]. */ fun maxIntrinsicWidth(height: Int): Int /** * Calculates the minimum height that the layout can be such that the content of the layout will * be painted correctly. There should be no side-effects from a call to [minIntrinsicHeight]. */ fun minIntrinsicHeight(width: Int): Int /** * Calculates the smallest height beyond which increasing the height never decreases the width. * There should be no side-effects from a call to [maxIntrinsicHeight]. */ fun maxIntrinsicHeight(width: Int): Int } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/LayoutNode.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.ui.node import androidx.compose.runtime.ComposeNodeLifecycleCallback import androidx.compose.runtime.CompositionLocalMap import androidx.compose.runtime.collection.MutableVector import androidx.compose.runtime.collection.mutableVectorOf import androidx.compose.runtime.tooling.CompositionErrorContext import androidx.compose.runtime.tooling.LocalCompositionErrorContext import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.input.pointer.PointerInputFilter import androidx.compose.ui.input.pointer.PointerInputModifier import androidx.compose.ui.input.pointer.PointerType import androidx.compose.ui.internal.checkPrecondition import androidx.compose.ui.internal.checkPreconditionNotNull import androidx.compose.ui.internal.requirePrecondition import androidx.compose.ui.layout.IntrinsicMeasurable import androidx.compose.ui.layout.IntrinsicMeasureScope import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.LayoutInfo import androidx.compose.ui.layout.LayoutNodeSubcompositionsState import androidx.compose.ui.layout.Measurable import androidx.compose.ui.layout.MeasurePolicy import androidx.compose.ui.layout.MeasureScope import androidx.compose.ui.layout.ModifierInfo import androidx.compose.ui.layout.OnGloballyPositionedModifier import androidx.compose.ui.layout.Placeable import androidx.compose.ui.layout.Remeasurement import androidx.compose.ui.node.LayoutNode.LayoutState.Idle import androidx.compose.ui.node.LayoutNode.LayoutState.LayingOut import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadLayingOut import androidx.compose.ui.node.LayoutNode.LayoutState.LookaheadMeasuring import androidx.compose.ui.node.LayoutNode.LayoutState.Measuring import androidx.compose.ui.node.Nodes.PointerInput import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.platform.simpleIdentityToString import androidx.compose.ui.semantics.SemanticsConfiguration import androidx.compose.ui.semantics.SemanticsInfo import androidx.compose.ui.semantics.generateSemanticsId import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.viewinterop.InteropView import androidx.compose.ui.viewinterop.InteropViewFactoryHolder /** Enable to log changes to the LayoutNode tree. This logging is quite chatty. */ private const val DebugChanges = false private val DefaultDensity = Density(1f) /** An element in the layout hierarchy, built with compose UI. */ @OptIn(InternalComposeUiApi::class) internal class LayoutNode( // Virtual LayoutNode is the temporary concept allows us to a node which is not a real node, // but just a holder for its children - allows us to combine some children into something we // can subcompose in(LayoutNode) without being required to define it as a real layout - we // don't want to define the layout strategy for such nodes, instead the children of the // virtual nodes will be treated as the direct children of the virtual node parent. // This whole concept will be replaced with a proper subcomposition logic which allows to // subcompose multiple times into the same LayoutNode and define offsets. private val isVirtual: Boolean = false, // The unique semantics ID that is used by all semantics modifiers attached to this LayoutNode. // TODO(b/281907968): Implement this with a getter that returns the compositeKeyHash. override var semanticsId: Int = generateSemanticsId(), ) : ComposeNodeLifecycleCallback, Remeasurement, OwnerScope, LayoutInfo, SemanticsInfo, ComposeUiNode, InteroperableComposeUiNode, Owner.OnLayoutCompletedListener { // Params managed by RectManager start: internal var hasPositionalLayerTransformationsInOffsetFromRoot: Boolean = false // this offset contains the combined offset accumulated by the coordinators attached to // this node, not including the offset of the outer one, as the outer offset is part of the // offsetFromRoot of this node, and the rest of the modifiers are affecting offsetFromRoot // for the children. internal var outerToInnerOffset: IntOffset = IntOffset.Max internal var outerToInnerOffsetDirty: Boolean = true // rect in parent is the sum of transformations for parent's coordinators not including the // outer one, and the transformations on this node's outer coordinator. internal var rectInParentDirty: Boolean = true internal var addedToRectList: Boolean = false // Params managed by RectManager end. override var compositeKeyHash: Int = 0 internal var isVirtualLookaheadRoot: Boolean = false /** * This lookaheadRoot references the closest root to the LayoutNode, not the top-level lookahead * root. */ internal var lookaheadRoot: LayoutNode? = null private set(newRoot) { if (newRoot != field) { field = newRoot if (newRoot != null) { layoutDelegate.ensureLookaheadDelegateCreated() forEachCoordinatorIncludingInner { it.ensureLookaheadDelegateCreated() } } else { // When lookahead root is set to null, clear the lookahead pass delegate. // This can happen when lookaheadScope is removed in one of the parents, or // more likely when movableContent moves from a parent in a LookaheadScope to // a parent not in a LookaheadScope. layoutDelegate.onRemovedFromLookaheadScope() } invalidateMeasurements() } } val isPlacedInLookahead: Boolean? get() = lookaheadPassDelegate?.isPlaced private var virtualChildrenCount = 0 // the list of nodes containing the virtual children as is private val _foldedChildren = MutableVectorWithMutationTracking(mutableVectorOf()) { layoutDelegate.markChildrenDirty() } internal val foldedChildren: List get() = _foldedChildren.asList() // the list of nodes where the virtual children are unfolded (their children are represented // as our direct children) private var _unfoldedChildren: MutableVector? = null private fun recreateUnfoldedChildrenIfDirty() { if (unfoldedVirtualChildrenListDirty) { unfoldedVirtualChildrenListDirty = false val unfoldedChildren = _unfoldedChildren ?: mutableVectorOf().also { _unfoldedChildren = it } unfoldedChildren.clear() _foldedChildren.forEach { if (it.isVirtual) { unfoldedChildren.addAll(it._children) } else { unfoldedChildren.add(it) } } layoutDelegate.markChildrenDirty() } } internal val childMeasurables: List get() = measurePassDelegate.childDelegates internal val childLookaheadMeasurables: List get() = lookaheadPassDelegate!!.childDelegates // when the list of our children is modified it will be set to true if we are a virtual node // or it will be set to true on a parent if the parent is a virtual node private var unfoldedVirtualChildrenListDirty = false private fun invalidateUnfoldedVirtualChildren() { if (virtualChildrenCount > 0) { unfoldedVirtualChildrenListDirty = true } if (isVirtual) { // Invalidate all virtual unfolded parent until we reach a non-virtual one this._foldedParent?.invalidateUnfoldedVirtualChildren() } } /** * This should **not** be mutated or even accessed directly from outside of [LayoutNode]. Use * [forEachChild]/[forEachChildIndexed] when there's a need to iterate through the vector. */ internal val _children: MutableVector get() { updateChildrenIfDirty() return if (virtualChildrenCount == 0) { _foldedChildren.vector } else { _unfoldedChildren!! } } /** Update children if the list is not up to date. */ internal fun updateChildrenIfDirty() { if (virtualChildrenCount > 0) { recreateUnfoldedChildrenIfDirty() } } inline fun forEachChild(block: (LayoutNode) -> Unit) = _children.forEach(block) inline fun forEachChildIndexed(block: (Int, LayoutNode) -> Unit) = _children.forEachIndexed(block) /** The children of this LayoutNode, controlled by [insertAt], [move], and [removeAt]. */ internal val children: List get() = _children.asMutableList() /** * The parent node in the LayoutNode hierarchy. This is `null` when the [LayoutNode] is not * attached to a hierarchy or is the root of the hierarchy. */ private var _foldedParent: LayoutNode? = null /* * The parent node in the LayoutNode hierarchy, skipping over virtual nodes. */ internal val parent: LayoutNode? get() { var parent = _foldedParent while (parent?.isVirtual == true) { parent = parent._foldedParent } return parent } /** The view system [Owner]. This `null` until [attach] is called */ internal var owner: Owner? = null private set /** * The [InteropViewFactoryHolder] associated with this node, which is used to instantiate and * manage platform View instances that are hosted in Compose. */ internal var interopViewFactoryHolder: InteropViewFactoryHolder? = null @InternalComposeUiApi override fun getInteropView(): InteropView? = interopViewFactoryHolder?.getInteropView() /** * Returns true if this [LayoutNode] currently has an [LayoutNode.owner]. Semantically, this * means that the LayoutNode is currently a part of a component tree. */ override val isAttached: Boolean get() = owner != null /** * The tree depth of the [LayoutNode]. This is valid only when it is attached to a hierarchy. */ internal var depth: Int = 0 /** * The layout state the node is currently in. * * The mutation of [layoutState] is confined to [LayoutNode], and is therefore read-only outside * LayoutNode. This makes the state machine easier to reason about. */ internal val layoutState get() = layoutDelegate.layoutState /** * The lookahead pass delegate for the [LayoutNode]. This should only be used for measure and * layout related impl during *lookahead*. For the actual measure & layout, use * [measurePassDelegate]. */ internal val lookaheadPassDelegate get() = layoutDelegate.lookaheadPassDelegate /** * The measure pass delegate for the [LayoutNode]. This delegate is responsible for the actual * measure & layout, after lookahead if any. */ internal val measurePassDelegate get() = layoutDelegate.measurePassDelegate /** [requestRemeasure] calls will be ignored while this flag is true. */ private var ignoreRemeasureRequests = false /** * Inserts a child [LayoutNode] at a particular index. If this LayoutNode [owner] is not `null` * then [instance] will become [attach]ed also. [instance] must have a `null` [parent]. */ internal fun insertAt(index: Int, instance: LayoutNode) { checkPrecondition(instance._foldedParent == null || instance.owner == null) { exceptionMessageForParentingOrOwnership(instance) } if (DebugChanges) { println("$instance added to $this at index $index") } instance._foldedParent = this _foldedChildren.add(index, instance) onZSortedChildrenInvalidated() if (instance.isVirtual) { virtualChildrenCount++ } invalidateUnfoldedVirtualChildren() val owner = this.owner if (owner != null) { instance.attach(owner) } if (instance.layoutDelegate.childrenAccessingCoordinatesDuringPlacement > 0) { layoutDelegate.childrenAccessingCoordinatesDuringPlacement++ } if (instance.globallyPositionedObservers > 0) { globallyPositionedObservers++ } } private fun exceptionMessageForParentingOrOwnership(instance: LayoutNode) = "Cannot insert $instance because it already has a parent or an owner." + " This tree: " + debugTreeToString() + " Other tree: " + instance._foldedParent?.debugTreeToString() internal fun onZSortedChildrenInvalidated() { if (isVirtual) { parent?.onZSortedChildrenInvalidated() } else { zSortedChildrenInvalidated = true } } /** Removes one or more children, starting at [index]. */ internal fun removeAt(index: Int, count: Int) { requirePrecondition(count >= 0) { "count ($count) must be greater than 0" } for (i in index + count - 1 downTo index) { // Call detach callbacks before removing from _foldedChildren, so the child is still // visible to parents traversing downwards, such as when clearing focus. onChildRemoved(_foldedChildren[i]) val child = _foldedChildren.removeAt(i) if (DebugChanges) { println("$child removed from $this at index $i") } } } /** Removes all children. */ internal fun removeAll() { for (i in _foldedChildren.size - 1 downTo 0) { onChildRemoved(_foldedChildren[i]) } _foldedChildren.clear() if (DebugChanges) { println("Removed all children from $this") } } private fun onChildRemoved(child: LayoutNode) { if (child.layoutDelegate.childrenAccessingCoordinatesDuringPlacement > 0) { layoutDelegate.childrenAccessingCoordinatesDuringPlacement-- } if (owner != null) { child.detach() } child._foldedParent = null if (child.globallyPositionedObservers > 0) { globallyPositionedObservers-- } child.outerCoordinator.wrappedBy = null if (child.isVirtual) { virtualChildrenCount-- child._foldedChildren.forEach { it.outerCoordinator.wrappedBy = null } } invalidateUnfoldedVirtualChildren() onZSortedChildrenInvalidated() } /** * Moves [count] elements starting at index [from] to index [to]. The [to] index is related to * the position before the change, so, for example, to move an element at position 1 to after * the element at position 2, [from] should be `1` and [to] should be `3`. If the elements were * LayoutNodes A B C D E, calling `move(1, 3, 1)` would result in the LayoutNodes being * reordered to A C B D E. */ internal fun move(from: Int, to: Int, count: Int) { if (from == to) { return // nothing to do } for (i in 0 until count) { // if "from" is after "to," the from index moves because we're inserting before it val fromIndex = if (from > to) from + i else from val toIndex = if (from > to) to + i else to + count - 2 val child = _foldedChildren.removeAt(fromIndex) if (DebugChanges) { println("$child moved in $this from index $fromIndex to $toIndex") } _foldedChildren.add(toIndex, child) } onZSortedChildrenInvalidated() invalidateUnfoldedVirtualChildren() invalidateMeasurements() } override fun isTransparent(): Boolean = outerCoordinator.isTransparent() internal var isSemanticsInvalidated = false internal fun requestAutofill() { // Ignore calls while semantics are being applied (b/378114177). if (isCurrentlyCalculatingSemanticsConfiguration) return val owner = requireOwner() owner.requestAutofill(this) } internal fun invalidateSemantics() { // Ignore calls to invalidate Semantics while semantics are being applied (b/378114177). if (isCurrentlyCalculatingSemanticsConfiguration) return if (nodes.isUpdating || applyingModifierOnAttach) { // We are currently updating the modifier, so just schedule an invalidation. After // applying the modifier, we will notify listeners of semantics changes. isSemanticsInvalidated = true } else { // We are not currently updating the modifier, so instead of scheduling invalidation, // we update the semantics configuration and send the notification event right away. val prev = _semanticsConfiguration _semanticsConfiguration = calculateSemanticsConfiguration() isSemanticsInvalidated = false val owner = requireOwner() owner.semanticsOwner.notifySemanticsChange(this, prev) // This is needed for Accessibility and ContentCapture. Remove after these systems // are migrated to use SemanticsInfo and SemanticListeners. owner.onSemanticsChange() } } // This is needed until we completely move to the new world where we always pre-compute the // semantics configuration. At that point, this can just be a property with a private setter. private var _semanticsConfiguration: SemanticsConfiguration? = null override val semanticsConfiguration: SemanticsConfiguration? get() { // TODO: investigate if there's a better way to approach "half attached" state and // whether or not deactivated nodes should be considered removed or not. if (!isAttached || isDeactivated || !nodes.has(Nodes.Semantics)) return null return _semanticsConfiguration } private var isCurrentlyCalculatingSemanticsConfiguration = false private fun calculateSemanticsConfiguration(): SemanticsConfiguration { // Ignore calls to invalidate Semantics while semantics are being calculated. isCurrentlyCalculatingSemanticsConfiguration = true var config = SemanticsConfiguration() requireOwner().snapshotObserver.observeSemanticsReads(this) { nodes.tailToHead(Nodes.Semantics) { if (it.shouldClearDescendantSemantics) { config = SemanticsConfiguration() config.isClearingSemantics = true } if (it.shouldMergeDescendantSemantics) { config.isMergingSemanticsOfDescendants = true } with(it) { config.applySemantics() } } } isCurrentlyCalculatingSemanticsConfiguration = false return config } /** * Set the [Owner] of this LayoutNode. This LayoutNode must not already be attached. [owner] * must match its [parent].[owner]. */ internal fun attach(owner: Owner) { checkPrecondition(this.owner == null) { "Cannot attach $this as it already is attached. Tree: " + debugTreeToString() } checkPrecondition(_foldedParent == null || _foldedParent?.owner == owner) { "Attaching to a different owner($owner) than the parent's owner(${parent?.owner})." + " This tree: " + debugTreeToString() + " Parent tree: " + _foldedParent?.debugTreeToString() } val parent = this.parent if (parent == null) { measurePassDelegate.isPlaced = true // regular nodes go through markNodeAndSubtreeAsPlaced(), from where we call this // function on rectManager. as root marked as placed here, we need to call it. owner.rectManager.recalculateRectIfDirty(this) lookaheadPassDelegate?.onAttachedToNullParent() } // Use the inner coordinator of first non-virtual parent outerCoordinator.wrappedBy = parent?.innerCoordinator this.owner = owner this.depth = (parent?.depth ?: -1) + 1 pendingModifier?.let { applyModifier(it) } pendingModifier = null owner.onPreAttach(this) // Update lookahead root when attached. For nested cases, we'll always use the // closest lookahead root if (isVirtualLookaheadRoot) { lookaheadRoot = this } else { // Favor lookahead root from parent than locally created scope, unless current node // is a virtual lookahead root lookaheadRoot = _foldedParent?.lookaheadRoot ?: lookaheadRoot if (lookaheadRoot == null && nodes.has(Nodes.ApproachMeasure)) { // This could happen when movableContent containing intermediateLayout is moved lookaheadRoot = this } } if (!isDeactivated) { nodes.markAsAttached() } _foldedChildren.forEach { child -> child.attach(owner) } if (!isDeactivated) { nodes.runAttachLifecycle() } invalidateMeasurements() parent?.invalidateMeasurements() onAttach?.invoke(owner) layoutDelegate.updateParentData() if (!isDeactivated && nodes.has(Nodes.Semantics)) { invalidateSemantics() } owner.onPostAttach(this) } /** * Remove the LayoutNode from the [Owner]. The [owner] must not be `null` before this call and * its [parent]'s [owner] must be `null` before calling this. This will also [detach] all * children. After executing, the [owner] will be `null`. */ internal fun detach() { val owner = owner checkPreconditionNotNull(owner) { "Cannot detach node that is already detached! Tree: " + parent?.debugTreeToString() } val parent = this.parent if (parent != null) { parent.invalidateLayer() parent.invalidateMeasurements() measurePassDelegate.measuredByParent = UsageByParent.NotUsed lookaheadPassDelegate?.let { it.measuredByParent = UsageByParent.NotUsed } } layoutDelegate.resetAlignmentLines() forEachCoordinatorIncludingInner { it.onLayoutNodeDetach() } onDetach?.invoke(owner) nodes.runDetachLifecycle() ignoreRemeasureRequests { _foldedChildren.forEach { child -> child.detach() } } nodes.markAsDetached() owner.onDetach(this) owner.rectManager.remove(this) this.owner = null lookaheadRoot = null depth = 0 measurePassDelegate.onNodeDetached() lookaheadPassDelegate?.onNodeDetached() // Note: Don't call invalidateSemantics() from within detach() because the modifier nodes // are detached before the LayoutNode, and invalidateSemantics() can trigger a call to // calculateSemanticsConfiguration() which will encounter unattached nodes. Instead, just // set the semantics configuration to null over here since we know the node is detached. if (nodes.has(Nodes.Semantics)) { val prev = _semanticsConfiguration _semanticsConfiguration = null isSemanticsInvalidated = false owner.semanticsOwner.notifySemanticsChange(this, prev) // This is needed for Accessibility and ContentCapture. Remove after these systems // are migrated to use SemanticsInfo and SemanticListeners. owner.onSemanticsChange() } } private val _zSortedChildren = mutableVectorOf() private var zSortedChildrenInvalidated = true /** * Returns the children list sorted by their [LayoutNode.zIndex] first (smaller first) and the * order they were placed via [Placeable.placeAt] by parent (smaller first). Please note that * this list contains not placed items as well, so you have to manually filter them. * * Note that the object is reused so you shouldn't save it for later. */ @PublishedApi internal val zSortedChildren: MutableVector get() { if (zSortedChildrenInvalidated) { _zSortedChildren.clear() _zSortedChildren.addAll(_children) _zSortedChildren.sortWith(ZComparator) zSortedChildrenInvalidated = false } return _zSortedChildren } override val isValidOwnerScope: Boolean get() = isAttached override fun toString(): String { return "${simpleIdentityToString(this, null)} children: ${children.size} " + "measurePolicy: $measurePolicy deactivated: $isDeactivated" } internal val hasFixedInnerContentConstraints: Boolean get() { // it is the constraints we have after all the modifiers applied on this node, // the one to be passed into user provided [measurePolicy.measure]. if those // constraints are fixed this means the children size changes can't affect // this LayoutNode size. val innerContentConstraints = innerCoordinator.lastMeasurementConstraints return innerContentConstraints.hasFixedWidth && innerContentConstraints.hasFixedHeight } /** Call this method from the debugger to see a dump of the LayoutNode tree structure */ @Suppress("unused") private fun debugTreeToString(depth: Int = 0): String { val tree = StringBuilder() for (i in 0 until depth) { tree.append(" ") } tree.append("|-") tree.append(toString()) tree.append('\n') forEachChild { child -> tree.append(child.debugTreeToString(depth + 1)) } var treeString = tree.toString() if (depth == 0) { // Delete trailing newline treeString = treeString.substring(0, treeString.length - 1) } return treeString } internal abstract class NoIntrinsicsMeasurePolicy(private val error: String) : MeasurePolicy { override fun IntrinsicMeasureScope.minIntrinsicWidth( measurables: List, height: Int, ) = error(error) override fun IntrinsicMeasureScope.minIntrinsicHeight( measurables: List, width: Int, ) = error(error) override fun IntrinsicMeasureScope.maxIntrinsicWidth( measurables: List, height: Int, ) = error(error) override fun IntrinsicMeasureScope.maxIntrinsicHeight( measurables: List, width: Int, ) = error(error) } /** Blocks that define the measurement and intrinsic measurement of the layout. */ override var measurePolicy: MeasurePolicy = ErrorMeasurePolicy set(value) { if (field != value) { field = value intrinsicsPolicy?.updateFrom(measurePolicy) invalidateMeasurements() } } /** * The intrinsic measurements of this layout, backed up by states to trigger correct * remeasurement for layouts using the intrinsics of this layout when the [measurePolicy] is * changing. */ private var intrinsicsPolicy: IntrinsicsPolicy? = null private fun getOrCreateIntrinsicsPolicy(): IntrinsicsPolicy { return intrinsicsPolicy ?: IntrinsicsPolicy(this, measurePolicy).also { intrinsicsPolicy = it } } fun minLookaheadIntrinsicWidth(height: Int) = getOrCreateIntrinsicsPolicy().minLookaheadIntrinsicWidth(height) fun minLookaheadIntrinsicHeight(width: Int) = getOrCreateIntrinsicsPolicy().minLookaheadIntrinsicHeight(width) fun maxLookaheadIntrinsicWidth(height: Int) = getOrCreateIntrinsicsPolicy().maxLookaheadIntrinsicWidth(height) fun maxLookaheadIntrinsicHeight(width: Int) = getOrCreateIntrinsicsPolicy().maxLookaheadIntrinsicHeight(width) fun minIntrinsicWidth(height: Int) = getOrCreateIntrinsicsPolicy().minIntrinsicWidth(height) fun minIntrinsicHeight(width: Int) = getOrCreateIntrinsicsPolicy().minIntrinsicHeight(width) fun maxIntrinsicWidth(height: Int) = getOrCreateIntrinsicsPolicy().maxIntrinsicWidth(height) fun maxIntrinsicHeight(width: Int) = getOrCreateIntrinsicsPolicy().maxIntrinsicHeight(width) /** The screen density to be used by this layout. */ override var density: Density = DefaultDensity set(value) { if (field != value) { field = value onDensityOrLayoutDirectionChanged() nodes.headToTail { it.onDensityChange() } } } /** The layout direction of the layout node. */ override var layoutDirection: LayoutDirection = LayoutDirection.Ltr set(value) { if (field != value) { field = value onDensityOrLayoutDirectionChanged() nodes.headToTail { it.onLayoutDirectionChange() } } } override var viewConfiguration: ViewConfiguration = DummyViewConfiguration set(value) { if (field != value) { field = value nodes.headToTail(type = PointerInput) { it.onViewConfigurationChange() } } } override var compositionLocalMap = CompositionLocalMap.Empty set(value) { field = value density = value[LocalDensity] layoutDirection = value[LocalLayoutDirection] viewConfiguration = value[LocalViewConfiguration] nodes.headToTail(Nodes.CompositionLocalConsumer) { modifierNode -> val delegatedNode = modifierNode.node if (delegatedNode.isAttached) { autoInvalidateUpdatedNode(delegatedNode) } else { delegatedNode.updatedNodeAwaitingAttachForInvalidation = true } } } private val traceContext: CompositionErrorContext? get() = compositionLocalMap[LocalCompositionErrorContext] fun rethrowWithComposeStackTrace(e: Throwable): Nothing = throw e.also { traceContext?.apply { e.attachComposeStackTrace(this@LayoutNode) } } private fun onDensityOrLayoutDirectionChanged() { // TODO(b/242120396): it seems like we need to update some densities in the node // coordinators here // measure/layout modifiers on the node invalidateMeasurements() // draw modifiers on the node parent?.invalidateLayer() ?: owner?.invalidateRootLayer() // and draw modifiers after graphics layers on the node invalidateLayers() } /** The measured width of this layout and all of its [modifier]s. Shortcut for `size.width`. */ override val width: Int get() = layoutDelegate.width /** * The measured height of this layout and all of its [modifier]s. Shortcut for `size.height`. */ override val height: Int get() = layoutDelegate.height internal val alignmentLinesRequired: Boolean get() = layoutDelegate.run { alignmentLinesOwner.alignmentLines.required || lookaheadAlignmentLinesOwner?.alignmentLines?.required == true } internal val mDrawScope: LayoutNodeDrawScope get() = requireOwner().sharedDrawScope /** * Whether or not this [LayoutNode] and all of its parents have been placed in the hierarchy. */ override val isPlaced: Boolean get() = measurePassDelegate.isPlaced /** * Whether or not this [LayoutNode] was placed by its parent. The node can still be considered * not placed if some of the modifiers on it not placed the placeable. */ val isPlacedByParent: Boolean get() = measurePassDelegate.isPlacedByParent /** * The order in which this node was placed by its parent during the previous `layoutChildren`. * Before the placement the order is set to [NotPlacedPlaceOrder] to all the children. Then * every placed node assigns this variable to [parent]s MeasurePassDelegate's * nextChildPlaceOrder and increments this counter. Not placed items will still have * [NotPlacedPlaceOrder] set. */ internal val placeOrder: Int get() = measurePassDelegate.placeOrder /** Remembers how the node was measured by the parent. */ internal val measuredByParent: UsageByParent get() = measurePassDelegate.measuredByParent /** Remembers how the node was measured by the parent in lookahead. */ internal val measuredByParentInLookahead: UsageByParent get() = lookaheadPassDelegate?.measuredByParent ?: UsageByParent.NotUsed /** Remembers how the node was measured using intrinsics by an ancestor. */ internal var intrinsicsUsageByParent: UsageByParent = UsageByParent.NotUsed /** * We must cache a previous value of [intrinsicsUsageByParent] because measurement is sometimes * skipped. When it is skipped, the subtree must be restored to this value. */ private var previousIntrinsicsUsageByParent: UsageByParent = UsageByParent.NotUsed @Deprecated("Temporary API to support ConstraintLayout prototyping.") internal var canMultiMeasure: Boolean = false internal val nodes = NodeChain(this) internal val innerCoordinator: NodeCoordinator get() = nodes.innerCoordinator internal val layoutDelegate = LayoutNodeLayoutDelegate(this) internal val outerCoordinator: NodeCoordinator get() = nodes.outerCoordinator /** * zIndex defines the drawing order of the LayoutNode. Children with larger zIndex are drawn on * top of others (the original order is used for the nodes with the same zIndex). Default zIndex * is 0. We use sum of the values passed as zIndex to place() by the parent layout and all the * applied modifiers. */ private val zIndex: Float get() = measurePassDelegate.zIndex /** The inner state associated with [androidx.compose.ui.layout.SubcomposeLayout]. */ internal var subcompositionsState: LayoutNodeSubcompositionsState? = null /** The inner-most layer coordinator. Used for performance for NodeCoordinator.findLayer(). */ private var _innerLayerCoordinator: NodeCoordinator? = null internal var innerLayerCoordinatorIsDirty = true internal val innerLayerCoordinator: NodeCoordinator? get() { if (innerLayerCoordinatorIsDirty) { var coordinator: NodeCoordinator? = innerCoordinator val final = outerCoordinator.wrappedBy _innerLayerCoordinator = null while (coordinator != final) { if (coordinator?.layer != null) { _innerLayerCoordinator = coordinator break } coordinator = coordinator?.wrappedBy } innerLayerCoordinatorIsDirty = false } val layerCoordinator = _innerLayerCoordinator if (layerCoordinator != null) { checkPreconditionNotNull(layerCoordinator.layer) { "layer was not set" } } return layerCoordinator } /** * Invalidates the inner-most layer as part of this LayoutNode or from the containing * LayoutNode. This is added for performance so that NodeCoordinator.invalidateLayer() can be * faster. */ internal fun invalidateLayer() { val innerLayerCoordinator = innerLayerCoordinator if (innerLayerCoordinator != null) { innerLayerCoordinator.invalidateLayer() } else { val parent = this.parent parent?.invalidateLayer() ?: owner?.invalidateRootLayer() } } private var _modifier: Modifier = Modifier private var pendingModifier: Modifier? = null internal val applyingModifierOnAttach get() = pendingModifier != null /** The [Modifier] currently applied to this node. */ override var modifier: Modifier get() = _modifier set(value) { requirePrecondition(!isVirtual || modifier === Modifier) { "Modifiers are not supported on virtual LayoutNodes" } requirePrecondition(!isDeactivated) { "modifier is updated when deactivated" } if (isAttached) { applyModifier(value) if (isSemanticsInvalidated) { invalidateSemantics() } } else { pendingModifier = value } } private fun applyModifier(modifier: Modifier) { val hadPointerInput = nodes.has(Nodes.PointerInput) val hadFocusTarget = nodes.has(Nodes.FocusTarget) _modifier = modifier nodes.updateFrom(modifier) val hasPointerInput = nodes.has(Nodes.PointerInput) val hasFocusTarget = nodes.has(Nodes.FocusTarget) layoutDelegate.updateParentData() if (lookaheadRoot == null && nodes.has(Nodes.ApproachMeasure)) { lookaheadRoot = this } if (hadPointerInput != hasPointerInput || hadFocusTarget != hasFocusTarget) { requireOwner().rectManager.updateFlagsFor(this, hasFocusTarget, hasPointerInput) } } private fun resetModifierState() { nodes.resetState() } internal fun invalidateParentData() { layoutDelegate.invalidateParentData() } /** * Coordinates of just the contents of the [LayoutNode], after being affected by all modifiers. */ override val coordinates: LayoutCoordinates get() = innerCoordinator /** Callback to be executed whenever the [LayoutNode] is attached to a new [Owner]. */ internal var onAttach: ((Owner) -> Unit)? = null /** Callback to be executed whenever the [LayoutNode] is detached from an [Owner]. */ internal var onDetach: ((Owner) -> Unit)? = null /** * Flag used by [OnPositionedDispatcher] to identify LayoutNodes that have already had their * [OnGloballyPositionedModifier]'s dispatch called so that they aren't called multiple times. */ internal var needsOnGloballyPositionedDispatch = false /** * Count of attached [GlobalPositionAwareModifierNode] modifiers or children having such * modifiers in their subtree. */ var globallyPositionedObservers: Int = 0 set(value) { if (field != value) { if (value > 0 && field == 0) { parent?.globallyPositionedObservers++ } if (value == 0 && field > 0) { parent?.globallyPositionedObservers-- } field = value } } internal fun place(x: Int, y: Int) { if (intrinsicsUsageByParent == UsageByParent.NotUsed) { // This LayoutNode may have asked children for intrinsics. If so, we should // clear the intrinsics usage for everything that was requested previously. clearSubtreePlacementIntrinsicsUsage() } with(parent?.innerCoordinator?.placementScope ?: requireOwner().placementScope) { measurePassDelegate.placeRelative(x, y) } } /** Place this layout node again on the same position it was placed last time */ internal fun replace() { if (intrinsicsUsageByParent == UsageByParent.NotUsed) { // This LayoutNode may have asked children for intrinsics. If so, we should // clear the intrinsics usage for everything that was requested previously. clearSubtreePlacementIntrinsicsUsage() } measurePassDelegate.replace() } internal fun lookaheadReplace() { if (intrinsicsUsageByParent == UsageByParent.NotUsed) { // This LayoutNode may have asked children for intrinsics. If so, we should // clear the intrinsics usage for everything that was requested previously. clearSubtreePlacementIntrinsicsUsage() } lookaheadPassDelegate!!.replace() } internal fun draw(canvas: Canvas, graphicsLayer: GraphicsLayer?) = withComposeStackTrace(this) { outerCoordinator.draw(canvas, graphicsLayer) } /** * Carries out a hit test on the [PointerInputModifier]s associated with this [LayoutNode] and * all [PointerInputModifier]s on all descendant [LayoutNode]s. * * If [pointerPosition] is within the bounds of any tested [PointerInputModifier]s, the * [PointerInputModifier] is added to [hitTestResult] and true is returned. * * @param pointerPosition The tested pointer position, which is relative to the LayoutNode. * @param hitTestResult The collection that the hit [PointerInputFilter]s will be added to if * hit. */ internal fun hitTest( pointerPosition: Offset, hitTestResult: HitTestResult, pointerType: PointerType = PointerType.Unknown, isInLayer: Boolean = true, ) { val positionInWrapped = outerCoordinator.fromParentPosition(pointerPosition) outerCoordinator.hitTest( NodeCoordinator.PointerInputSource, positionInWrapped, hitTestResult, pointerType, isInLayer, ) } @Suppress("UNUSED_PARAMETER") internal fun hitTestSemantics( pointerPosition: Offset, hitSemanticsEntities: HitTestResult, pointerType: PointerType = PointerType.Touch, isInLayer: Boolean = true, ) { val positionInWrapped = outerCoordinator.fromParentPosition(pointerPosition) outerCoordinator.hitTest( NodeCoordinator.SemanticsSource, positionInWrapped, hitSemanticsEntities, pointerType = PointerType.Touch, isInLayer = isInLayer, ) } internal fun rescheduleRemeasureOrRelayout(it: LayoutNode) { when (it.layoutState) { Idle -> { // this node was scheduled for remeasure or relayout while it was not // placed. such requests are ignored for non-placed nodes so we have to // re-schedule remeasure or relayout. if (it.lookaheadMeasurePending) { it.requestLookaheadRemeasure(forceRequest = true) } else { if (it.lookaheadLayoutPending) { it.requestLookaheadRelayout(forceRequest = true) } if (it.measurePending) { it.requestRemeasure(forceRequest = true) } else if (it.layoutPending) { it.requestRelayout(forceRequest = true) } } } else -> throw IllegalStateException("Unexpected state ${it.layoutState}") } } /** Used to request a new measurement + layout pass from the owner. */ internal fun requestRemeasure( forceRequest: Boolean = false, scheduleMeasureAndLayout: Boolean = true, invalidateIntrinsics: Boolean = true, ) { if (!ignoreRemeasureRequests && !isVirtual) { val owner = owner ?: return owner.onRequestMeasure( layoutNode = this, forceRequest = forceRequest, scheduleMeasureAndLayout = scheduleMeasureAndLayout, ) if (invalidateIntrinsics) { measurePassDelegate.invalidateIntrinsicsParent(forceRequest) } } } /** * Used to request a new lookahead measurement, lookahead layout, and subsequently measure and * layout from the owner. */ internal fun requestLookaheadRemeasure( forceRequest: Boolean = false, scheduleMeasureAndLayout: Boolean = true, invalidateIntrinsics: Boolean = true, ) { checkPrecondition(lookaheadRoot != null) { "Lookahead measure cannot be requested on a node that is not a part of the " + "LookaheadScope" } val owner = owner ?: return if (!ignoreRemeasureRequests && !isVirtual) { owner.onRequestMeasure( layoutNode = this, affectsLookahead = true, forceRequest = forceRequest, scheduleMeasureAndLayout = scheduleMeasureAndLayout, ) if (invalidateIntrinsics) { lookaheadPassDelegate!!.invalidateIntrinsicsParent(forceRequest) } } } /** * This gets called when both lookahead measurement (if in a LookaheadScope) and actual * measurement need to be re-done. Such events include modifier change, attach/detach, etc. */ internal fun invalidateMeasurements() { if (isVirtual) { // If the node is virtual, we need to invalidate the parent node (as it is non-virtual) // instead so that children get properly invalidated. parent?.invalidateMeasurements() return } if (lookaheadRoot != null) { requestLookaheadRemeasure() } else { requestRemeasure() } } internal fun invalidateOnPositioned() { // If we've already scheduled a measure, the positioned callbacks will get called anyway if ( globallyPositionedObservers == 0 || layoutPending || measurePending || needsOnGloballyPositionedDispatch ) return requireOwner().requestOnPositionedCallback(this) } internal fun onCoordinatorRectChanged(coordinator: NodeCoordinator) { val rectManager = owner?.rectManager val placementPending = layoutState != Idle || measurePending || layoutPending if (addedToRectList && rectManager != null) { if (coordinator === outerCoordinator) { // transformations on the outer coordinator update the offset from parent rectInParentDirty = true if (!placementPending) { // during placement we get it called right after rectManager.recalculateRectIfDirty(this) } } else { // transformations on other coordinators invalidate outerToInnerOffset // and offset from parent for each child outerToInnerOffsetDirty = true forEachChild { it.rectInParentDirty = true // during placement it is guaranteed to get recalculateRectIfDirty() call on // each child after the parent finish its placement. we don't want to call it // straight away, as there are might be multiple changes on the same layout // node, and we want to apply them once in batch. if (!placementPending) { rectManager.recalculateRectIfDirty(it) } } // Since there has been an update to a coordinator somewhere in the // modifier chain of this layout node, we might have onRectChanged // callbacks that need to be notified of that change. As a result, even // if the outer rect of this layout node hasn't changed, we want to // invalidate the callbacks for them rectManager.invalidateCallbacksFor(this) } } layoutDelegate.measurePassDelegate.requestLayoutIfCoordinatesAreUsedAndNotifyChildren() } internal inline fun ignoreRemeasureRequests(block: () -> T): T { ignoreRemeasureRequests = true val result = block() ignoreRemeasureRequests = false return result } /** Used to request a new layout pass from the owner. */ internal fun requestRelayout(forceRequest: Boolean = false) { if (!isVirtual) { owner?.onRequestRelayout(this, forceRequest = forceRequest) } } internal fun requestLookaheadRelayout(forceRequest: Boolean = false) { if (!isVirtual) { owner?.onRequestRelayout(this, affectsLookahead = true, forceRequest) } } internal fun dispatchOnPositionedCallbacks() { if (layoutState != Idle || layoutPending || measurePending || isDeactivated) { return // it hasn't yet been properly positioned, so don't make a call } if (!isPlaced) { return // it hasn't been placed, so don't make a call } nodes.headToTail(Nodes.GlobalPositionAware) { it.onGloballyPositioned(it.requireCoordinator(Nodes.GlobalPositionAware)) } } /** * This returns a new List of Modifiers and the coordinates and any extra information that may * be useful. This is used for tooling to retrieve layout modifier and layer information. */ override fun getModifierInfo(): List = nodes.getModifierInfo() /** Invalidates layers defined on this LayoutNode. */ internal fun invalidateLayers() { forEachCoordinator { coordinator -> coordinator.layer?.invalidate() } innerCoordinator.layer?.invalidate() } internal fun lookaheadRemeasure( constraints: Constraints? = layoutDelegate.lastLookaheadConstraints ): Boolean { // Only lookahead remeasure when the constraints are valid and the node is in // a LookaheadScope (by checking whether the lookaheadScope is set) return if (constraints != null && lookaheadRoot != null) { lookaheadPassDelegate!!.remeasure(constraints) } else { false } } /** Return true if the measured size has been changed */ internal fun remeasure(constraints: Constraints? = layoutDelegate.lastConstraints): Boolean { return if (constraints != null) { if (intrinsicsUsageByParent == UsageByParent.NotUsed) { // This LayoutNode may have asked children for intrinsics. If so, we should // clear the intrinsics usage for everything that was requested previously. clearSubtreeIntrinsicsUsage() } measurePassDelegate.remeasure(constraints) } else { false } } /** * Tracks whether another measure pass is needed for the LayoutNode. Mutation to * [measurePending] is confined to LayoutNodeLayoutDelegate. It can only be set true from * outside of LayoutNode via [markMeasurePending]. It is cleared (i.e. set false) during the * measure pass ( i.e. in [LayoutNodeLayoutDelegate.performMeasure]). */ internal val measurePending: Boolean get() = layoutDelegate.measurePending /** * Tracks whether another layout pass is needed for the LayoutNode. Mutation to [layoutPending] * is confined to LayoutNode. It can only be set true from outside of LayoutNode via * [markLayoutPending]. It is cleared (i.e. set false) during the layout pass (i.e. in * layoutChildren). */ internal val layoutPending: Boolean get() = layoutDelegate.layoutPending internal val lookaheadMeasurePending: Boolean get() = layoutDelegate.lookaheadMeasurePending internal val lookaheadLayoutPending: Boolean get() = layoutDelegate.lookaheadLayoutPending /** Marks the layoutNode dirty for another layout pass. */ internal fun markLayoutPending() = layoutDelegate.markLayoutPending() /** Marks the layoutNode dirty for another measure pass. */ internal fun markMeasurePending() = layoutDelegate.markMeasurePending() /** Marks the layoutNode dirty for another lookahead layout pass. */ internal fun markLookaheadLayoutPending() = layoutDelegate.markLookaheadLayoutPending() fun invalidateSubtree(isRootOfInvalidation: Boolean = true) { if (isRootOfInvalidation) { parent?.invalidateLayer() ?: owner?.invalidateRootLayer() } invalidateSemantics() requestRemeasure() nodes.headToTail(Nodes.Layout) { it.requireCoordinator(Nodes.Layout).layer?.invalidate() } // TODO: invalidate parent data _children.forEach { it.invalidateSubtree(false) } } fun invalidateMeasurementForSubtree() { requestRemeasure() _children.forEach { it.invalidateMeasurementForSubtree() } } fun invalidateDrawForSubtree(isRootOfInvalidation: Boolean = true) { if (isRootOfInvalidation) { parent?.invalidateLayer() ?: owner?.invalidateRootLayer() } nodes.headToTail(Nodes.Layout) { it.requireCoordinator(Nodes.Layout).layer?.invalidate() } _children.forEach { it.invalidateDrawForSubtree(false) } } /** Marks the layoutNode dirty for another lookahead measure pass. */ internal fun markLookaheadMeasurePending() = layoutDelegate.markLookaheadMeasurePending() override fun forceRemeasure() { // we do not schedule measure and layout as we are going to call it manually right after if (lookaheadRoot != null) { requestLookaheadRemeasure(scheduleMeasureAndLayout = false) } else { requestRemeasure(scheduleMeasureAndLayout = false) } val lastConstraints = layoutDelegate.lastConstraints if (lastConstraints != null) { owner?.measureAndLayout(this, lastConstraints) } else { owner?.measureAndLayout() } } override fun onLayoutComplete() { innerCoordinator.visitNodes(Nodes.OnPlaced) { it.onPlaced(innerCoordinator) } } /** Calls [block] on all [LayoutModifierNodeCoordinator]s in the NodeCoordinator chain. */ internal inline fun forEachCoordinator(block: (LayoutModifierNodeCoordinator) -> Unit) { var coordinator: NodeCoordinator? = outerCoordinator val inner = innerCoordinator while (coordinator !== inner) { block(coordinator as LayoutModifierNodeCoordinator) coordinator = coordinator.wrapped } } /** Calls [block] on all [NodeCoordinator]s in the NodeCoordinator chain. */ internal inline fun forEachCoordinatorIncludingInner(block: (NodeCoordinator) -> Unit) { var delegate: NodeCoordinator? = outerCoordinator val final = innerCoordinator.wrapped while (delegate != final && delegate != null) { block(delegate) delegate = delegate.wrapped } } /** * Walks the subtree and clears all [intrinsicsUsageByParent] that this LayoutNode's measurement * used intrinsics on. * * The layout that asks for intrinsics of its children is the node to call this to request all * of its subtree to be cleared. * * We can't do clearing as part of measure() because the child's measure() call is normally done * after the intrinsics is requested and we don't want to clear the usage at that point. */ internal fun clearSubtreeIntrinsicsUsage() { // save the usage in case we short-circuit the measure call previousIntrinsicsUsageByParent = intrinsicsUsageByParent intrinsicsUsageByParent = UsageByParent.NotUsed forEachChild { if (it.intrinsicsUsageByParent != UsageByParent.NotUsed) { it.clearSubtreeIntrinsicsUsage() } } } /** * Walks the subtree and clears all [intrinsicsUsageByParent] that this LayoutNode's layout * block used intrinsics on. * * The layout that asks for intrinsics of its children is the node to call this to request all * of its subtree to be cleared. * * We can't do clearing as part of measure() because the child's measure() call is normally done * after the intrinsics is requested and we don't want to clear the usage at that point. */ private fun clearSubtreePlacementIntrinsicsUsage() { // save the usage in case we short-circuit the measure call previousIntrinsicsUsageByParent = intrinsicsUsageByParent intrinsicsUsageByParent = UsageByParent.NotUsed forEachChild { if (it.intrinsicsUsageByParent == UsageByParent.InLayoutBlock) { it.clearSubtreePlacementIntrinsicsUsage() } } } /** * For a subtree that skips measurement, this resets the [intrinsicsUsageByParent] to what it * was prior to [clearSubtreeIntrinsicsUsage]. */ internal fun resetSubtreeIntrinsicsUsage() { forEachChild { it.intrinsicsUsageByParent = it.previousIntrinsicsUsageByParent if (it.intrinsicsUsageByParent != UsageByParent.NotUsed) { it.resetSubtreeIntrinsicsUsage() } } } override val parentInfo: SemanticsInfo? get() = parent override val childrenInfo: List get() = children override var isDeactivated = false private set override fun onReuse() { requirePrecondition(isAttached) { "onReuse is only expected on attached node" } interopViewFactoryHolder?.onReuse() subcompositionsState?.onReuse() isCurrentlyCalculatingSemanticsConfiguration = false if (isDeactivated) { isDeactivated = false // we don't need to reset state as it was done when deactivated } else { resetModifierState() } val oldSemanticsId = semanticsId // semanticsId is used as the identity. we need to remove from rectlist before changing it owner?.rectManager?.remove(this) semanticsId = generateSemanticsId() owner?.onPreLayoutNodeReused(this, oldSemanticsId) // resetModifierState detaches all nodes, so we need to re-attach them upon reuse. nodes.markAsAttached() nodes.runAttachLifecycle() if (nodes.has(Nodes.Semantics)) { invalidateSemantics() } rescheduleRemeasureOrRelayout(this) owner?.onPostLayoutNodeReused(this, oldSemanticsId) // Sometimes, while scrolling with reuse, a child LayoutNode, might not // require measure or layout at all, but at a minimum we need to update RectManager with // the correct information. owner?.rectManager?.recalculateRectIfDirty(this) } override fun onDeactivate() { interopViewFactoryHolder?.onDeactivate() subcompositionsState?.onDeactivate() isDeactivated = true resetModifierState() // if the node is detached the semantics were already updated without this node. if (isAttached) { _semanticsConfiguration = null isSemanticsInvalidated = false } owner?.onLayoutNodeDeactivated(this) } override fun onRelease() { interopViewFactoryHolder?.onRelease() subcompositionsState?.onRelease() forEachCoordinatorIncludingInner { it.onRelease() } } internal companion object { private val ErrorMeasurePolicy: NoIntrinsicsMeasurePolicy = object : NoIntrinsicsMeasurePolicy(error = "Undefined intrinsics block and it is required") { override fun MeasureScope.measure( measurables: List, constraints: Constraints, ) = error("Undefined measure and it is required") } /** Constant used by [placeOrder]. */ @Suppress("ConstPropertyName") internal const val NotPlacedPlaceOrder = Int.MAX_VALUE /** Pre-allocated constructor to be used with ComposeNode */ internal val Constructor: () -> LayoutNode = { LayoutNode() } /** * All of these values are only used in tests. The real ViewConfiguration should be set in * Layout() */ internal val DummyViewConfiguration = object : ViewConfiguration { override val longPressTimeoutMillis: Long get() = 400L override val doubleTapTimeoutMillis: Long get() = 300L override val doubleTapMinTimeMillis: Long get() = 40L override val touchSlop: Float get() = 16f override val minimumTouchTargetSize: DpSize get() = DpSize.Zero } /** Comparator allowing to sort nodes by zIndex and placement order. */ internal val ZComparator = Comparator { node1, node2 -> if (node1.zIndex == node2.zIndex) { // if zIndex is the same we use the placement order node1.placeOrder.compareTo(node2.placeOrder) } else { node1.zIndex.compareTo(node2.zIndex) } } } /** * Describes the current state the [LayoutNode] is in. A [LayoutNode] is expected to be in * [LookaheadMeasuring] first, followed by [LookaheadLayingOut] if it is in a LookaheadScope. * After the lookahead is finished, [Measuring] and then [LayingOut] will happen as needed. */ internal enum class LayoutState { /** Node is currently being measured. */ Measuring, /** Node is being measured in lookahead. */ LookaheadMeasuring, /** Node is currently being laid out. */ LayingOut, /** Node is being laid out in lookahead. */ LookaheadLayingOut, /** * Node is not currently measuring or laying out. It could be pending measure or pending * layout depending on the [measurePending] and [layoutPending] flags. */ Idle, } internal enum class UsageByParent { InMeasureBlock, InLayoutBlock, NotUsed, } } internal inline fun withComposeStackTrace(layoutNode: LayoutNode, block: () -> T): T = try { block() } catch (e: Throwable) { layoutNode.rethrowWithComposeStackTrace(e) } /** Returns [LayoutNode.owner] or throws if it is null. */ internal fun LayoutNode.requireOwner(): Owner { val owner = owner checkPreconditionNotNull(owner) { "LayoutNode should be attached to an owner" } return owner } /** * Inserts a child [LayoutNode] at a last index. If this LayoutNode [LayoutNode.isAttached] then * [child] will become [LayoutNode.isAttached] also. [child] must have a `null` [LayoutNode.parent]. */ internal fun LayoutNode.add(child: LayoutNode) { insertAt(children.size, child) } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/node/ModifierNodeElement.kt ```kotlin /* * Copyright 2022 The Android Open Source Project * * 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 androidx.compose.ui.node import androidx.compose.ui.Modifier import androidx.compose.ui.platform.InspectableValue import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.platform.ValueElement import androidx.compose.ui.tryPopulateReflectively /** * A [Modifier.Element] which manages an instance of a particular [Modifier.Node] implementation. A * given [Modifier.Node] implementation can only be used when a [ModifierNodeElement] which creates * and updates that implementation is applied to a Layout. * * A [ModifierNodeElement] should be very lightweight, and do little more than hold the information * necessary to create and maintain an instance of the associated [Modifier.Node] type. * * @sample androidx.compose.ui.samples.ModifierNodeElementSample * @sample androidx.compose.ui.samples.SemanticsModifierNodeSample * @see Modifier.Node * @see Modifier.Element */ abstract class ModifierNodeElement : Modifier.Element, InspectableValue { private var _inspectorValues: InspectorInfo? = null private val inspectorValues: InspectorInfo get() = _inspectorValues ?: InspectorInfo() .apply { name = this@ModifierNodeElement::class.simpleName inspectableProperties() } .also { _inspectorValues = it } final override val nameFallback: String? get() = inspectorValues.name final override val valueOverride: Any? get() = inspectorValues.value final override val inspectableElements: Sequence get() = inspectorValues.properties /** * This will be called the first time the modifier is applied to the Layout and it should * construct and return the corresponding [Modifier.Node] instance. */ abstract fun create(): N /** * Called when a modifier is applied to a Layout whose inputs have changed from the previous * application. This function will have the current node instance passed in as a parameter, and * it is expected that the node will be brought up to date. */ abstract fun update(node: N) /** * Populates an [InspectorInfo] object with attributes to display in the layout inspector. This * is called by tooling to resolve the properties of this modifier. By convention, implementors * should set the [name][InspectorInfo.name] to the function name of the modifier. * * The default implementation will attempt to reflectively populate the inspector info with the * properties declared on the subclass. It will also set the [name][InspectorInfo.name] property * to the name of this instance's class by default (not the name of the modifier function). * Modifier property population depends on the kotlin-reflect library. If it is not in the * classpath at runtime, the default implementation of this function will populate the * properties with an error message. * * If you override this function and provide the properties you wish to display, you do not need * to call `super`. Doing so may result in duplicate properties appearing in the layout * inspector. */ open fun InspectorInfo.inspectableProperties() { tryPopulateReflectively(this@ModifierNodeElement) } /** * Require hashCode() to be implemented. Using a data class is sufficient. Singletons and * modifiers with no parameters may implement this function by returning an arbitrary constant. */ abstract override fun hashCode(): Int /** * Require equals() to be implemented. Using a data class is sufficient. Singletons may * implement this function with referential equality (`this === other`). Modifiers with no * inputs may implement this function by checking the type of the other object. */ abstract override fun equals(other: Any?): Boolean } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/input/pointer/PointerInputEventProcessor.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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. */ @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress") package androidx.compose.ui.input.pointer import androidx.collection.LongSparseArray import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Matrix import androidx.compose.ui.node.HitTestResult import androidx.compose.ui.node.InternalCoreApi import androidx.compose.ui.node.LayoutNode import androidx.compose.ui.util.fastForEach internal interface PositionCalculator { fun screenToLocal(positionOnScreen: Offset): Offset fun localToScreen(localPosition: Offset): Offset } internal interface MatrixPositionCalculator : PositionCalculator { /** * Takes a matrix which transforms some coordinate system to local coordinates, and updates the * matrix to transform to screen coordinates instead. */ fun localToScreen(localTransform: Matrix) } /** The core element that receives [PointerInputEvent]s and process them in Compose UI. */ internal class PointerInputEventProcessor(val root: LayoutNode) { private val hitPathTracker = HitPathTracker(root.coordinates) private val pointerInputChangeEventProducer = PointerInputChangeEventProducer() private val hitResult = HitTestResult() /** * [process] doesn't currently support reentrancy. This prevents reentrant calls from causing a * crash with an early exit. */ private var isProcessing = false /** * Receives [PointerInputEvent]s and process them through the tree rooted on [root]. * * @param pointerEvent The [PointerInputEvent] to process. * @return the result of processing. * @see ProcessResult * @see PointerInputEvent */ fun process( @OptIn(InternalCoreApi::class) pointerEvent: PointerInputEvent, positionCalculator: PositionCalculator, isInBounds: Boolean = true, ): ProcessResult { if (isProcessing) { // Processing currently does not support reentrancy. return ProcessResult( dispatchedToAPointerInputModifier = false, anyMovementConsumed = false, anyChangeConsumed = false, ) } try { isProcessing = true // Gets a new PointerInputChangeEvent with the PointerInputEvent. @OptIn(InternalCoreApi::class) val internalPointerEvent = pointerInputChangeEventProducer.produce(pointerEvent, positionCalculator) var isHover = true for (i in 0 until internalPointerEvent.changes.size()) { val pointerInputChange = internalPointerEvent.changes.valueAt(i) if (pointerInputChange.pressed || pointerInputChange.previousPressed) { isHover = false break } } // Add new hit paths to the tracker due to down events. for (i in 0 until internalPointerEvent.changes.size()) { val pointerInputChange = internalPointerEvent.changes.valueAt(i) if (isHover || pointerInputChange.changedToDownIgnoreConsumed()) { root.hitTest(pointerInputChange.position, hitResult, pointerInputChange.type) if (hitResult.isNotEmpty()) { hitPathTracker.addHitPath( pointerId = pointerInputChange.id, pointerInputNodes = hitResult, // Prunes PointerIds (and changes) to support dynamically // adding/removing pointer input modifier nodes. // Note: We do not do this for hover because hover relies on those // non hit PointerIds to trigger hover exit events. prunePointerIdsAndChangesNotInNodesList = pointerInputChange.changedToDownIgnoreConsumed(), ) hitResult.clear() } } } // Dispatch to PointerInputFilters val dispatchedToSomething = hitPathTracker.dispatchChanges(internalPointerEvent, isInBounds) val anyMovementConsumed = if (internalPointerEvent.suppressMovementConsumption) { false } else { var result = false for (i in 0 until internalPointerEvent.changes.size()) { val event = internalPointerEvent.changes.valueAt(i) if (event.positionChangedIgnoreConsumed() && event.isConsumed) { result = true break } } result } var anyChangeConsumed = false for (i in 0 until internalPointerEvent.changes.size()) { val change = internalPointerEvent.changes.valueAt(i) if (change.isConsumed) { anyChangeConsumed = true break } } return ProcessResult( dispatchedToAPointerInputModifier = dispatchedToSomething, anyMovementConsumed = anyMovementConsumed, anyChangeConsumed = anyChangeConsumed, ) } finally { isProcessing = false } } /** * Responds appropriately to Android ACTION_CANCEL events. * * Specifically, [PointerInputFilter.onCancel] is invoked on tracked [PointerInputFilter]s and * and this [PointerInputEventProcessor] is reset such that it is no longer tracking any * [PointerInputFilter]s and expects the next [PointerInputEvent] it processes to represent only * new pointers. */ fun processCancel() { if (!isProcessing) { // Processing currently does not support reentrancy. pointerInputChangeEventProducer.clear() hitPathTracker.processCancel() } } /** * In some cases we need to clear the HIT Modifier.Node(s) cached from previous events because * they are no longer relevant. */ fun clearPreviouslyHitModifierNodes() { hitPathTracker.clearPreviouslyHitModifierNodeCache() } } /** Produces [InternalPointerEvent]s by tracking changes between [PointerInputEvent]s */ @OptIn(InternalCoreApi::class) private class PointerInputChangeEventProducer { private val previousPointerInputData: LongSparseArray = LongSparseArray() /** Produces [InternalPointerEvent]s by tracking changes between [PointerInputEvent]s */ fun produce( pointerInputEvent: PointerInputEvent, positionCalculator: PositionCalculator, ): InternalPointerEvent { // Set initial capacity to avoid resizing - we know the size the map will be. val changes: LongSparseArray = LongSparseArray(pointerInputEvent.pointers.size) pointerInputEvent.pointers.fastForEach { val previousTime: Long val previousPosition: Offset val previousDown: Boolean val previousData = previousPointerInputData[it.id.value] if (previousData == null) { previousTime = it.uptime previousPosition = it.position previousDown = false } else { previousTime = previousData.uptime previousDown = previousData.down previousPosition = positionCalculator.screenToLocal(previousData.positionOnScreen) } changes.put( it.id.value, PointerInputChange( id = it.id, uptimeMillis = it.uptime, position = it.position, pressed = it.down, pressure = it.pressure, previousUptimeMillis = previousTime, previousPosition = previousPosition, previousPressed = previousDown, isInitiallyConsumed = false, type = it.type, historical = it.historical, scrollDelta = it.scrollDelta, scaleGestureFactor = it.scaleGestureFactor, panGestureOffset = it.panGestureOffset, originalEventPosition = it.originalEventPosition, ), ) if (it.down) { previousPointerInputData.put( it.id.value, PointerInputData(it.uptime, it.positionOnScreen, it.down), ) } else { previousPointerInputData.remove(it.id.value) } } return InternalPointerEvent(changes, pointerInputEvent) } /** Clears all tracked information. */ fun clear() { previousPointerInputData.clear() } private class PointerInputData( val uptime: Long, val positionOnScreen: Offset, val down: Boolean, ) } /** The result of a call to [PointerInputEventProcessor.process]. */ @kotlin.jvm.JvmInline internal value class ProcessResult(val value: Int) { /** It's true when any [PointerInputFilter] has processed a [PointerInputChange] */ val dispatchedToAPointerInputModifier inline get() = (value and 0x1) != 0 /** It's true when [PointerInputChange] was consumed and Pointer's position was changed */ val anyMovementConsumed inline get() = (value and 0x2) != 0 /** It's true when any [PointerInputChange] was consumed. */ val anyChangeConsumed inline get() = (value and 0x4) != 0 } /** * Constructs a new ProcessResult. * * @param dispatchedToAPointerInputModifier True if the dispatch resulted in at least 1 * [PointerInputModifier] receiving the event. * @param anyMovementConsumed True if any movement occurred and was consumed. */ internal fun ProcessResult( dispatchedToAPointerInputModifier: Boolean, anyMovementConsumed: Boolean, anyChangeConsumed: Boolean, ): ProcessResult { return ProcessResult( value = dispatchedToAPointerInputModifier.toInt() or (anyMovementConsumed.toInt() shl 1) or (anyChangeConsumed.toInt() shl 2) ) } private inline fun Boolean.toInt() = if (this) 1 else 0 ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/DrawModifier.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.ui.draw import androidx.collection.MutableObjectList import androidx.collection.mutableObjectListOf import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Canvas import androidx.compose.ui.graphics.GraphicsContext import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.graphics.shadow.DropShadowPainter import androidx.compose.ui.graphics.shadow.InnerShadowPainter import androidx.compose.ui.graphics.shadow.ShadowContext import androidx.compose.ui.internal.JvmDefaultWithCompatibility import androidx.compose.ui.internal.checkPrecondition import androidx.compose.ui.internal.checkPreconditionNotNull import androidx.compose.ui.node.DrawModifierNode import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.Nodes import androidx.compose.ui.node.ObserverModifierNode import androidx.compose.ui.node.invalidateDraw import androidx.compose.ui.node.observeReads import androidx.compose.ui.node.requireCoordinator import androidx.compose.ui.node.requireDensity import androidx.compose.ui.node.requireGraphicsContext import androidx.compose.ui.node.requireLayoutDirection import androidx.compose.ui.platform.InspectorInfo import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.toIntSize import androidx.compose.ui.unit.toSize /** A [Modifier.Element] that draws into the space of the layout. */ @JvmDefaultWithCompatibility interface DrawModifier : Modifier.Element { fun ContentDrawScope.draw() } /** * [DrawModifier] implementation that supports building a cache of objects to be referenced across * draw calls */ @JvmDefaultWithCompatibility interface DrawCacheModifier : DrawModifier { /** * Callback invoked to re-build objects to be re-used across draw calls. This is useful to * conditionally recreate objects only if the size of the drawing environment changes, or if * state parameters that are inputs to objects change. This method is guaranteed to be called * before [DrawModifier.draw]. * * @param params The params to be used to build the cache. */ fun onBuildCache(params: BuildDrawCacheParams) } /** * The set of parameters which could be used to build the drawing cache. * * @see DrawCacheModifier.onBuildCache */ interface BuildDrawCacheParams { /** The current size of the drawing environment */ val size: Size /** The current layout direction. */ val layoutDirection: LayoutDirection /** The current screen density to provide the ability to convert between */ val density: Density } /** Draw into a [Canvas] behind the modified content. */ fun Modifier.drawBehind(onDraw: DrawScope.() -> Unit) = this then DrawBehindElement(onDraw) private class DrawBehindElement(val onDraw: DrawScope.() -> Unit) : ModifierNodeElement() { override fun create() = DrawBackgroundModifier(onDraw) override fun update(node: DrawBackgroundModifier) { node.onDraw = onDraw } override fun InspectorInfo.inspectableProperties() { name = "drawBehind" properties["onDraw"] = onDraw } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is DrawBehindElement) return false if (onDraw !== other.onDraw) return false return true } override fun hashCode(): Int { return onDraw.hashCode() } } internal class DrawBackgroundModifier(var onDraw: DrawScope.() -> Unit) : Modifier.Node(), DrawModifierNode { override fun ContentDrawScope.draw() { onDraw() drawContent() } } /** * Draw into a [DrawScope] with content that is persisted across draw calls as long as the size of * the drawing area is the same or any state objects that are read have not changed. In the event * that the drawing area changes, or the underlying state values that are being read change, this * method is invoked again to recreate objects to be used during drawing * * For example, a [androidx.compose.ui.graphics.LinearGradient] that is to occupy the full bounds of * the drawing area can be created once the size has been defined and referenced for subsequent draw * calls without having to re-allocate. * * @sample androidx.compose.ui.samples.DrawWithCacheModifierSample * @sample androidx.compose.ui.samples.DrawWithCacheModifierStateParameterSample * @sample androidx.compose.ui.samples.DrawWithCacheContentSample */ fun Modifier.drawWithCache(onBuildDrawCache: CacheDrawScope.() -> DrawResult) = this then DrawWithCacheElement(onBuildDrawCache) private class DrawWithCacheElement(val onBuildDrawCache: CacheDrawScope.() -> DrawResult) : ModifierNodeElement() { override fun create(): CacheDrawModifierNodeImpl { return CacheDrawModifierNodeImpl(CacheDrawScope(), onBuildDrawCache) } override fun update(node: CacheDrawModifierNodeImpl) { node.block = onBuildDrawCache } override fun InspectorInfo.inspectableProperties() { name = "drawWithCache" properties["onBuildDrawCache"] = onBuildDrawCache } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is DrawWithCacheElement) return false if (onBuildDrawCache !== other.onBuildDrawCache) return false return true } override fun hashCode(): Int { return onBuildDrawCache.hashCode() } } fun CacheDrawModifierNode( onBuildDrawCache: CacheDrawScope.() -> DrawResult ): CacheDrawModifierNode { return CacheDrawModifierNodeImpl(CacheDrawScope(), onBuildDrawCache) } /** * Expands on the [androidx.compose.ui.node.DrawModifierNode] by adding the ability to invalidate * the draw cache for changes in things like shapes and bitmaps (see Modifier.border for a usage * examples). */ sealed interface CacheDrawModifierNode : DrawModifierNode { fun invalidateDrawCache() } /** * Wrapper [GraphicsContext] implementation that maintains a list of the [GraphicsLayer] instances * that were created through this instance so it can release only those [GraphicsLayer]s when it is * disposed of within the corresponding Modifier is disposed */ private class ScopedGraphicsContext : GraphicsContext { private var allocatedGraphicsLayers: MutableObjectList? = null var graphicsContext: GraphicsContext? = null set(value) { releaseGraphicsLayers() field = value } override fun createGraphicsLayer(): GraphicsLayer { val gContext = graphicsContext checkPrecondition(gContext != null) { "GraphicsContext not provided" } val layer = gContext.createGraphicsLayer() val layers = allocatedGraphicsLayers if (layers == null) { mutableObjectListOf(layer).also { allocatedGraphicsLayers = it } } else { layers.add(layer) } return layer } override fun releaseGraphicsLayer(layer: GraphicsLayer) { graphicsContext?.releaseGraphicsLayer(layer) } override val shadowContext: ShadowContext get() { val gContext = graphicsContext checkPrecondition(gContext != null) { "GraphicsContext not provided" } return gContext.shadowContext } fun releaseGraphicsLayers() { allocatedGraphicsLayers?.let { layers -> layers.forEach { layer -> releaseGraphicsLayer(layer) } layers.clear() } } } private class CacheDrawModifierNodeImpl( private val cacheDrawScope: CacheDrawScope, block: CacheDrawScope.() -> DrawResult, ) : Modifier.Node(), CacheDrawModifierNode, ObserverModifierNode, BuildDrawCacheParams { private var isCacheValid = false private var cachedGraphicsContext: ScopedGraphicsContext? = null var block: CacheDrawScope.() -> DrawResult = block set(value) { field = value invalidateDrawCache() } init { cacheDrawScope.cacheParams = this cacheDrawScope.graphicsContextProvider = { graphicsContext } } override val density: Density get() = requireDensity() override val layoutDirection: LayoutDirection get() = requireLayoutDirection() override val size: Size get() = requireCoordinator(Nodes.Draw).size.toSize() val graphicsContext: GraphicsContext get() { var localGraphicsContext = cachedGraphicsContext if (localGraphicsContext == null) { localGraphicsContext = ScopedGraphicsContext().also { cachedGraphicsContext = it } } if (localGraphicsContext.graphicsContext == null) { localGraphicsContext.graphicsContext = requireGraphicsContext() } return localGraphicsContext } override fun onDetach() { super.onDetach() cachedGraphicsContext?.releaseGraphicsLayers() } override fun onReset() { super.onReset() invalidateDrawCache() } override fun onMeasureResultChanged() { invalidateDrawCache() } override fun onObservedReadsChanged() { invalidateDrawCache() } override fun invalidateDrawCache() { // Release all previously allocated graphics layers to the recycling pool // if a layer is needed in a subsequent draw, it will be obtained from the pool again and // reused cachedGraphicsContext?.releaseGraphicsLayers() isCacheValid = false cacheDrawScope.drawResult = null invalidateDraw() } override fun onDensityChange() { invalidateDrawCache() } override fun onLayoutDirectionChange() { invalidateDrawCache() } private fun getOrBuildCachedDrawBlock(contentDrawScope: ContentDrawScope): DrawResult { if (!isCacheValid) { cacheDrawScope.apply { drawResult = null this.contentDrawScope = contentDrawScope observeReads { block() } checkPreconditionNotNull(drawResult) { "DrawResult not defined, did you forget to call onDraw?" } } isCacheValid = true } return cacheDrawScope.drawResult!! } override fun ContentDrawScope.draw() { getOrBuildCachedDrawBlock(this).block(this) } } /** * Handle to a drawing environment that enables caching of content based on the resolved size. * Consumers define parameters and refer to them in the captured draw callback provided in * [onDrawBehind] or [onDrawWithContent]. * * [onDrawBehind] will draw behind the layout's drawing contents however, [onDrawWithContent] will * provide the ability to draw before or after the layout's contents */ class CacheDrawScope internal constructor() : Density { internal var cacheParams: BuildDrawCacheParams = EmptyBuildDrawCacheParams internal var drawResult: DrawResult? = null internal var contentDrawScope: ContentDrawScope? = null internal var graphicsContextProvider: (() -> GraphicsContext)? = null /** Provides the dimensions of the current drawing environment */ val size: Size get() = cacheParams.size /** Provides the [LayoutDirection]. */ val layoutDirection: LayoutDirection get() = cacheParams.layoutDirection /** * Returns a managed [GraphicsLayer] instance. This [GraphicsLayer] maybe newly created or * return a previously allocated instance. Consumers are not expected to release this instance * as it is automatically recycled upon invalidation of the CacheDrawScope and released when the * [DrawCacheModifier] is detached. */ fun obtainGraphicsLayer(): GraphicsLayer = graphicsContextProvider!!.invoke().createGraphicsLayer() /** * Returns the [ShadowContext] used to create [InnerShadowPainter] and [DropShadowPainter] to * render inner and drop shadows respectively */ fun obtainShadowContext(): ShadowContext = graphicsContextProvider!!.invoke().shadowContext /** * Record the drawing commands into the [GraphicsLayer] with the [Density], [LayoutDirection] * and [Size] are given from the provided [CacheDrawScope] */ fun GraphicsLayer.record( density: Density = this@CacheDrawScope, layoutDirection: LayoutDirection = this@CacheDrawScope.layoutDirection, size: IntSize = this@CacheDrawScope.size.toIntSize(), block: ContentDrawScope.() -> Unit, ) { val scope = contentDrawScope!! with(scope) { val prevDensity = drawContext.density val prevLayoutDirection = drawContext.layoutDirection record(size) { drawContext.apply { this.density = density this.layoutDirection = layoutDirection } try { block(scope) } finally { drawContext.apply { this.density = prevDensity this.layoutDirection = prevLayoutDirection } } } } } /** Issue drawing commands to be executed before the layout content is drawn */ fun onDrawBehind(block: DrawScope.() -> Unit): DrawResult = onDrawWithContent { block() drawContent() } /** Issue drawing commands before or after the layout's drawing contents */ fun onDrawWithContent(block: ContentDrawScope.() -> Unit): DrawResult { return DrawResult(block).also { drawResult = it } } override val density: Float get() = cacheParams.density.density override val fontScale: Float get() = cacheParams.density.fontScale } private object EmptyBuildDrawCacheParams : BuildDrawCacheParams { override val size: Size = Size.Unspecified override val layoutDirection: LayoutDirection = LayoutDirection.Ltr override val density: Density = Density(1f, 1f) } /** * Holder to a callback to be invoked during draw operations. This lambda captures and reuses * parameters defined within the CacheDrawScope receiver scope lambda. */ class DrawResult internal constructor(internal var block: ContentDrawScope.() -> Unit) /** * Creates a [DrawModifier] that allows the developer to draw before or after the layout's contents. * It also allows the modifier to adjust the layout's canvas. */ fun Modifier.drawWithContent(onDraw: ContentDrawScope.() -> Unit): Modifier = this then DrawWithContentElement(onDraw) private class DrawWithContentElement(val onDraw: ContentDrawScope.() -> Unit) : ModifierNodeElement() { override fun create() = DrawWithContentModifier(onDraw) override fun update(node: DrawWithContentModifier) { node.onDraw = onDraw } override fun InspectorInfo.inspectableProperties() { name = "drawWithContent" properties["onDraw"] = onDraw } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is DrawWithContentElement) return false if (onDraw !== other.onDraw) return false return true } override fun hashCode(): Int { return onDraw.hashCode() } } private class DrawWithContentModifier(var onDraw: ContentDrawScope.() -> Unit) : Modifier.Node(), DrawModifierNode { override fun ContentDrawScope.draw() { onDraw() } } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsModifier.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.ui.semantics import androidx.compose.ui.Modifier import androidx.compose.ui.internal.JvmDefaultWithCompatibility import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.node.SemanticsModifierNode import androidx.compose.ui.platform.AtomicInt import androidx.compose.ui.platform.InspectorInfo private var lastIdentifier = AtomicInt(0) internal fun generateSemanticsId() = lastIdentifier.addAndGet(1) /** * A [Modifier.Element] that adds semantics key/value for use in testing, accessibility, and similar * use cases. */ @JvmDefaultWithCompatibility interface SemanticsModifier : Modifier.Element { @Deprecated( message = "SemanticsModifier.id is now unused and has been set to a fixed value. " + "Retrieve the id from LayoutInfo instead.", replaceWith = ReplaceWith(""), ) val id: Int get() = -1 /** * The SemanticsConfiguration holds substantive data, especially a list of key/value pairs such * as (label -> "buttonName"). */ val semanticsConfiguration: SemanticsConfiguration } internal class CoreSemanticsModifierNode( var mergeDescendants: Boolean, var isClearingSemantics: Boolean, var properties: SemanticsPropertyReceiver.() -> Unit, ) : Modifier.Node(), SemanticsModifierNode { override val shouldClearDescendantSemantics: Boolean get() = isClearingSemantics override val shouldMergeDescendantSemantics: Boolean get() = mergeDescendants override fun SemanticsPropertyReceiver.applySemantics() { properties() } } internal class EmptySemanticsModifier : Modifier.Node(), SemanticsModifierNode { override fun SemanticsPropertyReceiver.applySemantics() {} } /** * Add semantics key/value pairs to the layout node, for use in testing, accessibility, etc. * * The provided lambda receiver scope provides "key = value"-style setters for any * [SemanticsPropertyKey]. Additionally, chaining multiple semantics modifiers is also a supported * style. * * The resulting semantics produce two [SemanticsNode] trees: * * The "unmerged tree" rooted at [SemanticsOwner.unmergedRootSemanticsNode] has one [SemanticsNode] * per layout node which has any [SemanticsModifier] on it. This [SemanticsNode] contains all the * properties set in all the [SemanticsModifier]s on that node. * * The "merged tree" rooted at [SemanticsOwner.rootSemanticsNode] has equal-or-fewer nodes: it * simplifies the structure based on [mergeDescendants] and [clearAndSetSemantics]. For most * purposes (especially accessibility, or the testing of accessibility), the merged semantics tree * should be used. * * @param mergeDescendants Whether the semantic information provided by the owning component and its * descendants should be treated as one logical entity. Most commonly set on * screen-reader-focusable items such as buttons or form fields. In the merged semantics tree, all * descendant nodes (except those themselves marked [mergeDescendants]) will disappear from the * tree, and their properties will get merged into the parent's configuration (using a merging * algorithm that varies based on the type of property -- for example, text properties will get * concatenated, separated by commas). In the unmerged semantics tree, the node is simply marked * with [SemanticsConfiguration.isMergingSemanticsOfDescendants]. * @param properties properties to add to the semantics. [SemanticsPropertyReceiver] will be * provided in the scope to allow access for common properties and its values. * * Note: The [properties] block should be used to set semantic properties or semantic actions. * Don't call [SemanticsModifierNode.applySemantics] from within the [properties] block. It will * result in an infinite loop. */ fun Modifier.semantics( mergeDescendants: Boolean = false, properties: (SemanticsPropertyReceiver.() -> Unit), ): Modifier = this then AppendedSemanticsElement(mergeDescendants = mergeDescendants, properties = properties) // Implement SemanticsModifier to allow tooling to inspect the semantics configuration internal class AppendedSemanticsElement( val mergeDescendants: Boolean, val properties: (SemanticsPropertyReceiver.() -> Unit), ) : ModifierNodeElement(), SemanticsModifier { // This should only ever be called by layout inspector override val semanticsConfiguration: SemanticsConfiguration get() = SemanticsConfiguration().apply { isMergingSemanticsOfDescendants = mergeDescendants properties() } override fun create(): CoreSemanticsModifierNode { return CoreSemanticsModifierNode( mergeDescendants = mergeDescendants, isClearingSemantics = false, properties = properties, ) } override fun update(node: CoreSemanticsModifierNode) { node.mergeDescendants = mergeDescendants node.properties = properties } override fun InspectorInfo.inspectableProperties() { name = "semantics" properties["mergeDescendants"] = mergeDescendants addSemanticsPropertiesFrom(semanticsConfiguration) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AppendedSemanticsElement) return false if (mergeDescendants != other.mergeDescendants) return false if (properties !== other.properties) return false return true } override fun hashCode(): Int { var result = mergeDescendants.hashCode() result = 31 * result + properties.hashCode() return result } } /** * Clears the semantics of all the descendant nodes and sets new semantics. * * In the merged semantics tree, this clears the semantic information provided by the node's * descendants (but not those of the layout node itself, if any) and sets the provided semantics. * (In the unmerged tree, the semantics node is marked with * "[SemanticsConfiguration.isClearingSemantics]", but nothing is actually cleared.) * * Compose's default semantics provide baseline usability for screen-readers, but this can be used * to provide a more polished screen-reader experience: for example, clearing the semantics of a * group of tiny buttons, and setting equivalent actions on the card containing them. * * @param properties properties to add to the semantics. [SemanticsPropertyReceiver] will be * provided in the scope to allow access for common properties and its values. * * Note: The [properties] lambda should be used to set semantic properties or semantic actions. * Don't call [SemanticsModifierNode.applySemantics] from within the [properties] block. It will * result in an infinite loop. */ fun Modifier.clearAndSetSemantics(properties: (SemanticsPropertyReceiver.() -> Unit)): Modifier = this then ClearAndSetSemanticsElement(properties) // Implement SemanticsModifier to allow tooling to inspect the semantics configuration internal class ClearAndSetSemanticsElement(val properties: SemanticsPropertyReceiver.() -> Unit) : ModifierNodeElement(), SemanticsModifier { // This should only ever be called by layout inspector override val semanticsConfiguration: SemanticsConfiguration get() = SemanticsConfiguration().apply { isMergingSemanticsOfDescendants = false isClearingSemantics = true properties() } override fun create(): CoreSemanticsModifierNode { return CoreSemanticsModifierNode( mergeDescendants = false, isClearingSemantics = true, properties = properties, ) } override fun update(node: CoreSemanticsModifierNode) { node.properties = properties } override fun InspectorInfo.inspectableProperties() { name = "clearAndSetSemantics" addSemanticsPropertiesFrom(semanticsConfiguration) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ClearAndSetSemanticsElement) return false if (properties !== other.properties) return false return true } override fun hashCode(): Int { return properties.hashCode() } } private fun InspectorInfo.addSemanticsPropertiesFrom( semanticsConfiguration: SemanticsConfiguration ) { properties["properties"] = semanticsConfiguration.associate { (key, value) -> key.name to value } } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/semantics/SemanticsProperties.kt ```kotlin /* * Copyright 2019 The Android Open Source Project * * 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 androidx.compose.ui.semantics import androidx.compose.runtime.Immutable import androidx.compose.ui.autofill.ContentDataType import androidx.compose.ui.autofill.ContentType import androidx.compose.ui.autofill.FillableData import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Shape import androidx.compose.ui.state.ToggleableState import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.ImeAction import kotlin.reflect.KProperty /** * General semantics properties, mainly used for accessibility and testing. * * Each of these is intended to be set by the respective SemanticsPropertyReceiver extension instead * of used directly. */ /*@VisibleForTesting*/ object SemanticsProperties { /** @see SemanticsPropertyReceiver.contentDescription */ val ContentDescription = AccessibilityKey>( name = "ContentDescription", mergePolicy = { parentValue, childValue -> parentValue?.toMutableList()?.also { it.addAll(childValue) } ?: childValue }, ) /** @see SemanticsPropertyReceiver.stateDescription */ val StateDescription = AccessibilityKey("StateDescription") /** @see SemanticsPropertyReceiver.progressBarRangeInfo */ val ProgressBarRangeInfo = AccessibilityKey("ProgressBarRangeInfo") /** @see SemanticsPropertyReceiver.paneTitle */ val PaneTitle = AccessibilityKey( name = "PaneTitle", mergePolicy = { _, _ -> throw IllegalStateException( "merge function called on unmergeable property PaneTitle." ) }, ) /** @see SemanticsPropertyReceiver.selectableGroup */ val SelectableGroup = AccessibilityKey("SelectableGroup") /** @see SemanticsPropertyReceiver.collectionInfo */ val CollectionInfo = AccessibilityKey("CollectionInfo") /** @see SemanticsPropertyReceiver.collectionItemInfo */ val CollectionItemInfo = AccessibilityKey("CollectionItemInfo") /** @see SemanticsPropertyReceiver.heading */ val Heading = AccessibilityKey("Heading") /** @see SemanticsPropertyReceiver.textEntryKey */ val TextEntryKey = AccessibilityKey("TextEntryKey") /** @see SemanticsPropertyReceiver.disabled */ val Disabled = AccessibilityKey("Disabled") /** @see SemanticsPropertyReceiver.liveRegion */ val LiveRegion = AccessibilityKey("LiveRegion") /** @see SemanticsPropertyReceiver.focused */ val Focused = AccessibilityKey("Focused") /** @see SemanticsPropertyReceiver.isContainer */ @Deprecated("Use `isTraversalGroup` instead.", replaceWith = ReplaceWith("IsTraversalGroup")) // TODO(mnuzen): `isContainer` should not need to be an accessibility key after a new // pruning API is added. See b/347038246 for more details. val IsContainer = AccessibilityKey("IsContainer") /** @see SemanticsPropertyReceiver.isTraversalGroup */ val IsTraversalGroup = SemanticsPropertyKey("IsTraversalGroup") /** @see SemanticsPropertyReceiver.IsSensitiveData */ val IsSensitiveData = SemanticsPropertyKey("IsSensitiveData") /** @see SemanticsPropertyReceiver.invisibleToUser */ @Deprecated( "Use `hideFromAccessibility` instead.", replaceWith = ReplaceWith("HideFromAccessibility"), ) // Retain for binary compatibility with aosp/3341487 in 1.7 val InvisibleToUser = SemanticsPropertyKey( name = "InvisibleToUser", mergePolicy = { parentValue, _ -> parentValue }, ) /** @see SemanticsPropertyReceiver.hideFromAccessibility */ val HideFromAccessibility = SemanticsPropertyKey( name = "HideFromAccessibility", mergePolicy = { parentValue, _ -> parentValue }, ) /** @see SemanticsPropertyReceiver.contentType */ val ContentType = SemanticsPropertyKey( name = "ContentType", mergePolicy = { parentValue, _ -> // Never merge autofill types parentValue }, ) /** @see SemanticsPropertyReceiver.contentDataType */ val ContentDataType = SemanticsPropertyKey( name = "ContentDataType", mergePolicy = { parentValue, _ -> // Never merge autofill data types parentValue }, ) /** @see SemanticsPropertyReceiver.fillableData */ val FillableData = SemanticsPropertyKey( name = "FillableData", mergePolicy = { parentValue, _ -> // Never merge autofill types parentValue }, ) /** @see SemanticsPropertyReceiver.traversalIndex */ val TraversalIndex = SemanticsPropertyKey( name = "TraversalIndex", mergePolicy = { parentValue, _ -> // Never merge traversal indices parentValue }, ) /** @see SemanticsPropertyReceiver.horizontalScrollAxisRange */ val HorizontalScrollAxisRange = AccessibilityKey("HorizontalScrollAxisRange") /** @see SemanticsPropertyReceiver.verticalScrollAxisRange */ val VerticalScrollAxisRange = AccessibilityKey("VerticalScrollAxisRange") /** @see SemanticsPropertyReceiver.popup */ val IsPopup = AccessibilityKey( name = "IsPopup", mergePolicy = { _, _ -> throw IllegalStateException( "merge function called on unmergeable property IsPopup. " + "A popup should not be a child of a clickable/focusable node." ) }, ) /** @see SemanticsPropertyReceiver.dialog */ val IsDialog = AccessibilityKey( name = "IsDialog", mergePolicy = { _, _ -> throw IllegalStateException( "merge function called on unmergeable property IsDialog. " + "A dialog should not be a child of a clickable/focusable node." ) }, ) /** * The type of user interface element. Accessibility services might use this to describe the * element or do customizations. Most roles can be automatically resolved by the semantics * properties of this element. But some elements with subtle differences need an exact role. If * an exact role is not listed in [Role], this property should not be set and the framework will * automatically resolve it. * * @see SemanticsPropertyReceiver.role */ val Role = AccessibilityKey("Role") { parentValue, _ -> parentValue } /** @see SemanticsPropertyReceiver.testTag */ val TestTag = SemanticsPropertyKey( name = "TestTag", isImportantForAccessibility = false, mergePolicy = { parentValue, _ -> // Never merge TestTags, to avoid leaking internal test tags to parents. parentValue }, ) /** * Marks a link within a text node (a link is represented by a * [androidx.compose.ui.text.LinkAnnotation]) for identification during automated testing. This * property is for internal use only and not intended for general use by developers. */ val LinkTestMarker = SemanticsPropertyKey( name = "LinkTestMarker", isImportantForAccessibility = false, mergePolicy = { parentValue, _ -> parentValue }, ) /** @see SemanticsPropertyReceiver.text */ val Text = AccessibilityKey>( name = "Text", mergePolicy = { parentValue, childValue -> parentValue?.toMutableList()?.also { it.addAll(childValue) } ?: childValue }, ) /** @see SemanticsPropertyReceiver.textSubstitution */ val TextSubstitution = SemanticsPropertyKey(name = "TextSubstitution") /** @see SemanticsPropertyReceiver.isShowingTextSubstitution */ val IsShowingTextSubstitution = SemanticsPropertyKey("IsShowingTextSubstitution") /** @see SemanticsPropertyReceiver.inputText */ val InputText = AccessibilityKey(name = "InputText") /** @see SemanticsPropertyReceiver.editableText */ val EditableText = AccessibilityKey(name = "EditableText") /** @see SemanticsPropertyReceiver.textSelectionRange */ val TextSelectionRange = AccessibilityKey("TextSelectionRange") /** @see SemanticsPropertyReceiver.onImeAction */ val ImeAction = AccessibilityKey("ImeAction") /** @see SemanticsPropertyReceiver.selected */ val Selected = AccessibilityKey("Selected") /** @see SemanticsPropertyReceiver.toggleableState */ val ToggleableState = AccessibilityKey("ToggleableState") /** @see SemanticsPropertyReceiver.password */ val Password = AccessibilityKey("Password") /** @see SemanticsPropertyReceiver.error */ val Error = AccessibilityKey("Error") /** @see SemanticsPropertyReceiver.indexForKey */ val IndexForKey = SemanticsPropertyKey<(Any) -> Int>("IndexForKey") /** @see SemanticsPropertyReceiver.isEditable */ val IsEditable = SemanticsPropertyKey("IsEditable") /** @see SemanticsPropertyReceiver.maxTextLength */ val MaxTextLength = SemanticsPropertyKey("MaxTextLength") /** @see SemanticsPropertyReceiver.shape */ val Shape = SemanticsPropertyKey( name = "Shape", isImportantForAccessibility = false, mergePolicy = { parentValue, _ -> // Never merge shapes parentValue }, ) } /** * Ths object defines keys of the actions which can be set in semantics and performed on the * semantics node. * * Each of these is intended to be set by the respective SemanticsPropertyReceiver extension instead * of used directly. */ /*@VisibleForTesting*/ object SemanticsActions { /** @see SemanticsPropertyReceiver.getTextLayoutResult */ val GetTextLayoutResult = ActionPropertyKey<(MutableList) -> Boolean>("GetTextLayoutResult") /** @see SemanticsPropertyReceiver.onClick */ val OnClick = ActionPropertyKey<() -> Boolean>("OnClick") /** @see SemanticsPropertyReceiver.onLongClick */ val OnLongClick = ActionPropertyKey<() -> Boolean>("OnLongClick") /** @see SemanticsPropertyReceiver.scrollBy */ val ScrollBy = ActionPropertyKey<(x: Float, y: Float) -> Boolean>("ScrollBy") /** @see SemanticsPropertyReceiver.scrollByOffset */ val ScrollByOffset = SemanticsPropertyKey Offset>("ScrollByOffset") /** @see SemanticsPropertyReceiver.scrollToIndex */ val ScrollToIndex = ActionPropertyKey<(Int) -> Boolean>("ScrollToIndex") @Suppress("unused") @Deprecated( message = "Use `SemanticsActions.OnFillData` instead.", replaceWith = ReplaceWith("OnFillData", "androidx.compose.ui.semantics.SemanticsActions.OnFillData"), level = DeprecationLevel.WARNING, ) val OnAutofillText = ActionPropertyKey<(AnnotatedString) -> Boolean>("OnAutofillText") /** @see SemanticsPropertyReceiver.onFillData */ val OnFillData = ActionPropertyKey<(FillableData) -> Boolean>("OnFillData") /** @see SemanticsPropertyReceiver.setProgress */ val SetProgress = ActionPropertyKey<(progress: Float) -> Boolean>("SetProgress") /** @see SemanticsPropertyReceiver.setSelection */ val SetSelection = ActionPropertyKey<(Int, Int, Boolean) -> Boolean>("SetSelection") /** @see SemanticsPropertyReceiver.setText */ val SetText = ActionPropertyKey<(AnnotatedString) -> Boolean>("SetText") /** @see SemanticsPropertyReceiver.setTextSubstitution */ val SetTextSubstitution = ActionPropertyKey<(AnnotatedString) -> Boolean>("SetTextSubstitution") /** @see SemanticsPropertyReceiver.showTextSubstitution */ val ShowTextSubstitution = ActionPropertyKey<(Boolean) -> Boolean>("ShowTextSubstitution") /** @see SemanticsPropertyReceiver.clearTextSubstitution */ val ClearTextSubstitution = ActionPropertyKey<() -> Boolean>("ClearTextSubstitution") /** @see SemanticsPropertyReceiver.insertTextAtCursor */ val InsertTextAtCursor = ActionPropertyKey<(AnnotatedString) -> Boolean>("InsertTextAtCursor") /** @see SemanticsPropertyReceiver.onImeAction */ val OnImeAction = ActionPropertyKey<() -> Boolean>("PerformImeAction") // b/322269946 @Suppress("unused") @Deprecated( message = "Use `SemanticsActions.OnImeAction` instead.", replaceWith = ReplaceWith( "OnImeAction", "androidx.compose.ui.semantics.SemanticsActions.OnImeAction", ), level = DeprecationLevel.ERROR, ) val PerformImeAction = ActionPropertyKey<() -> Boolean>("PerformImeAction") /** @see SemanticsPropertyReceiver.copyText */ val CopyText = ActionPropertyKey<() -> Boolean>("CopyText") /** @see SemanticsPropertyReceiver.cutText */ val CutText = ActionPropertyKey<() -> Boolean>("CutText") /** @see SemanticsPropertyReceiver.pasteText */ val PasteText = ActionPropertyKey<() -> Boolean>("PasteText") /** @see SemanticsPropertyReceiver.expand */ val Expand = ActionPropertyKey<() -> Boolean>("Expand") /** @see SemanticsPropertyReceiver.collapse */ val Collapse = ActionPropertyKey<() -> Boolean>("Collapse") /** @see SemanticsPropertyReceiver.dismiss */ val Dismiss = ActionPropertyKey<() -> Boolean>("Dismiss") /** @see SemanticsPropertyReceiver.requestFocus */ val RequestFocus = ActionPropertyKey<() -> Boolean>("RequestFocus") /** @see SemanticsPropertyReceiver.customActions */ val CustomActions = AccessibilityKey>( name = "CustomActions", mergePolicy = { parentValue, childValue -> parentValue.orEmpty() + childValue }, ) /** @see SemanticsPropertyReceiver.pageUp */ val PageUp = ActionPropertyKey<() -> Boolean>("PageUp") /** @see SemanticsPropertyReceiver.pageLeft */ val PageLeft = ActionPropertyKey<() -> Boolean>("PageLeft") /** @see SemanticsPropertyReceiver.pageDown */ val PageDown = ActionPropertyKey<() -> Boolean>("PageDown") /** @see SemanticsPropertyReceiver.pageRight */ val PageRight = ActionPropertyKey<() -> Boolean>("PageRight") /** @see SemanticsPropertyReceiver.getScrollViewportLength */ val GetScrollViewportLength = ActionPropertyKey<(MutableList) -> Boolean>("GetScrollViewportLength") } /** * SemanticsPropertyKey is the infrastructure for setting key/value pairs inside semantics blocks in * a type-safe way. Each key has one particular statically defined value type T. */ class SemanticsPropertyKey( /** The name of the property. Should be the same as the constant from which it is accessed. */ val name: String, internal val mergePolicy: (T?, T) -> T? = { parentValue, childValue -> parentValue ?: childValue }, ) { /** * Whether this type of property provides information relevant to accessibility services. * * Most built-in semantics properties are relevant to accessibility, but a very common exception * is testTag. Nodes with only a testTag still need to be included in the AccessibilityNodeInfo * tree because UIAutomator tests rely on that, but we mark them `isImportantForAccessibility = * false` on the AccessibilityNodeInfo to inform accessibility services that they are best * ignored. * * The default value is false and it is not exposed as a public API. That's because it is * impossible in the first place for `SemanticsPropertyKey`s defined outside the UI package to * be relevant to accessibility, because for each accessibility-relevant SemanticsProperty type * to get plumbed into the AccessibilityNodeInfo, the private `createNodeInfo` implementation * must also have a line of code. */ internal var isImportantForAccessibility = false private set /** * If this value is non-null, this semantics property will be exposed as an accessibility extra * via AccessibilityNodeInfo.getExtras with this value used as the key for the extra. */ internal var accessibilityExtraKey: String? = null internal constructor(name: String, isImportantForAccessibility: Boolean) : this(name) { this.isImportantForAccessibility = isImportantForAccessibility } internal constructor( name: String, isImportantForAccessibility: Boolean, mergePolicy: (T?, T) -> T?, accessibilityExtraKey: String? = null, ) : this(name, mergePolicy) { this.isImportantForAccessibility = isImportantForAccessibility this.accessibilityExtraKey = accessibilityExtraKey } /** * Method implementing the semantics merge policy of a particular key. * * When mergeDescendants is set on a semantics node, then this function will called for each * descendant node of a given key in depth-first-search order. The parent value accumulates the * result of merging the values seen so far, similar to reduce(). * * The default implementation returns the parent value if one exists, otherwise uses the child * element. This means by default, a SemanticsNode with mergeDescendants = true winds up with * the first value found for each key in its subtree in depth-first-search order. */ fun merge(parentValue: T?, childValue: T): T? { return mergePolicy(parentValue, childValue) } /** Throws [UnsupportedOperationException]. Should not be called. */ // TODO(KT-6519): Remove this getter // TODO(KT-32770): Cannot deprecate this either as the getter is considered called by "by" final operator fun getValue(thisRef: SemanticsPropertyReceiver, property: KProperty<*>): T { return throwSemanticsGetNotSupported() } final operator fun setValue( thisRef: SemanticsPropertyReceiver, property: KProperty<*>, value: T, ) { thisRef[this] = value } override fun toString(): String { return "AccessibilityKey: $name" } } private fun throwSemanticsGetNotSupported(): T { throw UnsupportedOperationException( "You cannot retrieve a semantics property directly - " + "use one of the SemanticsConfiguration.getOr* methods instead" ) } @Suppress("NOTHING_TO_INLINE") // inline to avoid different static initialization order on different targets. // See https://youtrack.jetbrains.com/issue/KT-65040 for more information. internal inline fun AccessibilityKey(name: String) = SemanticsPropertyKey(name = name, isImportantForAccessibility = true) @Suppress("NOTHING_TO_INLINE") // inline to avoid different static initialization order on different targets // See https://youtrack.jetbrains.com/issue/KT-65040 for more information. internal inline fun AccessibilityKey(name: String, noinline mergePolicy: (T?, T) -> T?) = SemanticsPropertyKey(name = name, isImportantForAccessibility = true, mergePolicy = mergePolicy) /** * Standard accessibility action. * * @param label The description of this action * @param action The function to invoke when this action is performed. The function should return a * boolean result indicating whether the action is successfully handled. For example, a scroll * forward action should return false if the widget is not enabled or has reached the end of the * list. If multiple semantics blocks with the same AccessibilityAction are provided, the * resulting AccessibilityAction's label/action will be the label/action of the outermost modifier * with this key and nonnull label/action, or null if no nonnull label/action is found. */ class AccessibilityAction>(val label: String?, val action: T?) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is AccessibilityAction<*>) return false if (label != other.label) return false if (action != other.action) return false return true } override fun hashCode(): Int { var result = label?.hashCode() ?: 0 result = 31 * result + action.hashCode() return result } override fun toString(): String { return "AccessibilityAction(label=$label, action=$action)" } } @Suppress("NOTHING_TO_INLINE") // inline to break static initialization cycle issue private inline fun > ActionPropertyKey(name: String) = AccessibilityKey>( name = name, mergePolicy = { parentValue, childValue -> AccessibilityAction( parentValue?.label ?: childValue.label, parentValue?.action ?: childValue.action, ) }, ) /** * Custom accessibility action. * * @param label The description of this action * @param action The function to invoke when this action is performed. The function should have no * arguments and return a boolean result indicating whether the action is successfully handled. */ class CustomAccessibilityAction(val label: String, val action: () -> Boolean) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CustomAccessibilityAction) return false if (label != other.label) return false if (action !== other.action) return false return true } override fun hashCode(): Int { var result = label.hashCode() result = 31 * result + action.hashCode() return result } override fun toString(): String { return "CustomAccessibilityAction(label=$label, action=$action)" } } /** * Accessibility range information, to represent the status of a progress bar or seekable progress * bar. * * @param current current value in the range. Must not be NaN. * @param range range of this node * @param steps if greater than `0`, specifies the number of discrete values, evenly distributed * between across the whole value range. If `0`, any value from the range specified can be chosen. * Cannot be less than `0`. */ class ProgressBarRangeInfo( val current: Float, val range: ClosedFloatingPointRange, /*@IntRange(from = 0)*/ val steps: Int = 0, ) { init { require(!current.isNaN()) { "current must not be NaN" } } companion object { /** Accessibility range information to present indeterminate progress bar */ val Indeterminate = ProgressBarRangeInfo(0f, 0f..0f) } override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is ProgressBarRangeInfo) return false if (current != other.current) return false if (range != other.range) return false if (steps != other.steps) return false return true } override fun hashCode(): Int { var result = current.hashCode() result = 31 * result + range.hashCode() result = 31 * result + steps return result } override fun toString(): String { return "ProgressBarRangeInfo(current=$current, range=$range, steps=$steps)" } } /** * Information about the collection. * * A collection of items has [rowCount] rows and [columnCount] columns. For example, a vertical list * is a collection with one column, as many rows as the list items that are important for * accessibility; A table is a collection with several rows and several columns. * * @param rowCount the number of rows in the collection, or -1 if unknown * @param columnCount the number of columns in the collection, or -1 if unknown */ class CollectionInfo(val rowCount: Int, val columnCount: Int) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is CollectionInfo) return false if (rowCount != other.rowCount) return false if (columnCount != other.columnCount) return false return true } override fun hashCode(): Int { var result = rowCount.hashCode() result = 31 * result + columnCount.hashCode() return result } override fun toString(): String { return "CollectionInfo(rowCount=$rowCount, columnCount=$columnCount)" } } /** * Information about the item of a collection. * * A collection item is contained in a collection, it starts at a given [rowIndex] and [columnIndex] * in the collection, and spans one or more rows and columns. For example, a header of two related * table columns starts at the first row and the first column, spans one row and two columns. * * @param rowIndex the index of the row at which item is located * @param rowSpan the number of rows the item spans * @param columnIndex the index of the column at which item is located * @param columnSpan the number of columns the item spans */ class CollectionItemInfo( val rowIndex: Int, val rowSpan: Int, val columnIndex: Int, val columnSpan: Int, ) /** * The scroll state of one axis if this node is scrollable. * * @param value current 0-based scroll position value (either in pixels, or lazy-item count) * @param maxValue maximum bound for [value], or [Float.POSITIVE_INFINITY] if still unknown * @param reverseScrolling for horizontal scroll, when this is `true`, 0 [value] will mean right, * when`false`, 0 [value] will mean left. For vertical scroll, when this is `true`, 0 [value] will * mean bottom, when `false`, 0 [value] will mean top */ class ScrollAxisRange( val value: () -> Float, val maxValue: () -> Float, val reverseScrolling: Boolean = false, ) { override fun toString(): String = "ScrollAxisRange(value=${value()}, maxValue=${maxValue()}, " + "reverseScrolling=$reverseScrolling)" } /** * The type of user interface element. Accessibility services might use this to describe the element * or do customizations. Most roles can be automatically resolved by the semantics properties of * this element. But some elements with subtle differences need an exact role. If an exact role is * not listed, [SemanticsPropertyReceiver.role] should not be set and the framework will * automatically resolve it. */ @Immutable @kotlin.jvm.JvmInline value class Role private constructor(@Suppress("unused") private val value: Int) { companion object { /** * This element is a button control. Associated semantics properties for accessibility: * [SemanticsProperties.Disabled], [SemanticsActions.OnClick] */ val Button = Role(0) /** * This element is a Checkbox which is a component that represents two states (checked / * unchecked). Associated semantics properties for accessibility: * [SemanticsProperties.Disabled], [SemanticsProperties.StateDescription], * [SemanticsActions.OnClick] */ val Checkbox = Role(1) /** * This element is a Switch which is a two state toggleable component that provides on/off * like options. Associated semantics properties for accessibility: * [SemanticsProperties.Disabled], [SemanticsProperties.StateDescription], * [SemanticsActions.OnClick] */ val Switch = Role(2) /** * This element is a RadioButton which is a component to represent two states, selected and * not selected. Associated semantics properties for accessibility: * [SemanticsProperties.Disabled], [SemanticsProperties.StateDescription], * [SemanticsActions.OnClick] */ val RadioButton = Role(3) /** * This element is a Tab which represents a single page of content using a text label and/or * icon. A Tab also has two states: selected and not selected. Associated semantics * properties for accessibility: [SemanticsProperties.Disabled], * [SemanticsProperties.StateDescription], [SemanticsActions.OnClick] */ val Tab = Role(4) /** * This element is an image. Associated semantics properties for accessibility: * [SemanticsProperties.ContentDescription] */ val Image = Role(5) /** * This element is associated with a drop down menu. Associated semantics properties for * accessibility: [SemanticsActions.OnClick] */ val DropdownList = Role(6) /** * This element is a value picker. It should support the following accessibility actions to * enable selection of the next and previous values: * * [android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_FORWARD]: Select the next * value. * * [android.view.accessibility.AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD]: Select the * previous value. * * These actions allow accessibility services to interact with this node programmatically on * behalf of users, facilitating navigation within sets of selectable values. */ val ValuePicker = Role(7) /** * This element is a Carousel. This means that even if Pager actions are added, this element * will behave like a regular List collection. * * Associated semantics properties for Pager accessibility actions: * [SemanticsActions.PageUp],[SemanticsActions.PageDown],[SemanticsActions.PageLeft], * [SemanticsActions.PageRight] */ val Carousel = Role(8) } override fun toString() = when (this) { Button -> "Button" Checkbox -> "Checkbox" Switch -> "Switch" RadioButton -> "RadioButton" Tab -> "Tab" Image -> "Image" DropdownList -> "DropdownList" ValuePicker -> "Picker" Carousel -> "Carousel" else -> "Unknown" } } /** * The mode of live region. Live region indicates to accessibility services they should * automatically notify the user about changes to the node's content description or text, or to the * content descriptions or text of the node's children (where applicable). */ @Immutable @kotlin.jvm.JvmInline value class LiveRegionMode private constructor(@Suppress("unused") private val value: Int) { companion object { /** * Live region mode specifying that accessibility services should announce changes to this * node. */ val Polite = LiveRegionMode(0) /** * Live region mode specifying that accessibility services should interrupt ongoing speech * to immediately announce changes to this node. */ val Assertive = LiveRegionMode(1) } override fun toString() = when (this) { Polite -> "Polite" Assertive -> "Assertive" else -> "Unknown" } } /** * SemanticsPropertyReceiver is the scope provided by semantics {} blocks, letting you set key/value * pairs primarily via extension functions. */ interface SemanticsPropertyReceiver { operator fun set(key: SemanticsPropertyKey, value: T) } /** * Developer-set content description of the semantics node. * * If this is not set, accessibility services will present the [text][SemanticsProperties.Text] of * this node as the content. * * This typically should not be set directly by applications, because some screen readers will cease * presenting other relevant information when this property is present. This is intended to be used * via Foundation components which are inherently intractable to automatically describe, such as * Image, Icon, and Canvas. */ var SemanticsPropertyReceiver.contentDescription: String get() = throwSemanticsGetNotSupported() set(value) { set(SemanticsProperties.ContentDescription, listOf(value)) } /** * Developer-set state description of the semantics node. * * For example: on/off. If this not set, accessibility services will derive the state from other * semantics properties, like [ProgressBarRangeInfo], but it is not guaranteed and the format will * be decided by accessibility services. */ var SemanticsPropertyReceiver.stateDescription by SemanticsProperties.StateDescription /** * The semantics represents a range of possible values with a current value. For example, when used * on a slider control, this will allow screen readers to communicate the slider's state. */ var SemanticsPropertyReceiver.progressBarRangeInfo by SemanticsProperties.ProgressBarRangeInfo /** * The node is marked as heading for accessibility. * * @see SemanticsProperties.Heading */ fun SemanticsPropertyReceiver.heading() { this[SemanticsProperties.Heading] = Unit } /** * The node is marked as a text entry key for accessibility. This is used to indicate that this * composable acts as a key within a text entry interface, such as a custom on-screen keyboard. * Accessibility services can use this information to provide a better experience for users * interacting with custom text input methods. * * See * [AccessibilityNodeInfo.setTextEntryKey](https://developer.android.com/reference/android/view/accessibility/AccessibilityNodeInfo#setTextEntryKey(boolean)) * for more details. * * @see SemanticsProperties.TextEntryKey */ fun SemanticsPropertyReceiver.textEntryKey() { this[SemanticsProperties.TextEntryKey] = Unit } /** * Accessibility-friendly title for a screen's pane. For accessibility purposes, a pane is a * visually distinct portion of a window, such as the contents of a open drawer. In order for * accessibility services to understand a pane's window-like behavior, you should give descriptive * titles to your app's panes. Accessibility services can then provide more granular information to * users when a pane's appearance or content changes. * * @see SemanticsProperties.PaneTitle */ var SemanticsPropertyReceiver.paneTitle by SemanticsProperties.PaneTitle /** * Whether this semantics node is disabled. Note that proper [SemanticsActions] should still be * added when this property is set. * * @see SemanticsProperties.Disabled */ fun SemanticsPropertyReceiver.disabled() { this[SemanticsProperties.Disabled] = Unit } /** * This node is marked as live region for accessibility. This indicates to accessibility services * they should automatically notify the user about changes to the node's content description or * text, or to the content descriptions or text of the node's children (where applicable). It should * be used with caution, especially with assertive mode which immediately stops the current audio * and the user does not hear the rest of the content. An example of proper use is a Snackbar which * is marked as [LiveRegionMode.Polite]. * * @see SemanticsProperties.LiveRegion * @see LiveRegionMode */ var SemanticsPropertyReceiver.liveRegion by SemanticsProperties.LiveRegion /** * Whether this semantics node is focused. The presence of this property indicates this node is * focusable * * @see SemanticsProperties.Focused */ var SemanticsPropertyReceiver.focused by SemanticsProperties.Focused /** * Whether this semantics node is a container. This is defined as a node whose function is to serve * as a boundary or border in organizing its children. * * @see SemanticsProperties.IsContainer */ @Deprecated("Use `isTraversalGroup` instead.", replaceWith = ReplaceWith("isTraversalGroup")) @Suppress("DEPRECATION") var SemanticsPropertyReceiver.isContainer by SemanticsProperties.IsContainer /** * Whether this semantics node is a traversal group. * * See https://developer.android.com/develop/ui/compose/accessibility/traversal * * @see SemanticsProperties.IsTraversalGroup */ var SemanticsPropertyReceiver.isTraversalGroup by SemanticsProperties.IsTraversalGroup /** * Whether this semantics node should only allow interactions from * [android.accessibilityservice.AccessibilityService]s with the * [android.accessibilityservice.AccessibilityServiceInfo.isAccessibilityTool] property set to true. * * This property allows the node to remain visible and interactive to Accessibility Services * declared as accessibility tools that assist users with disabilities, while simultaneously hiding * this node and its generated AccessibilityEvents from other Accessibility Services that are not * declared as accessibility tools. * * If looking for a way to hide the node from all Accessibility Services then consider * [SemanticsProperties.HideFromAccessibility] instead. * * @see SemanticsProperties.IsSensitiveData */ var SemanticsPropertyReceiver.isSensitiveData by SemanticsProperties.IsSensitiveData /** * Whether this node is specially known to be invisible to the user. * * For example, if the node is currently occluded by a dark semitransparent pane above it, then for * all practical purposes the node is invisible to the user, but the system cannot automatically * determine that. To make the screen reader linear navigation skip over this type of invisible * node, this property can be set. * * If looking for a way to hide semantics of small items from screen readers because they're * redundant with semantics of their parent, consider [SemanticsModifier.clearAndSetSemantics] * instead. */ @Deprecated( "Use `hideFromAccessibility()` instead.", replaceWith = ReplaceWith("hideFromAccessibility()"), ) @Suppress("DEPRECATION") // Retain for binary compatibility with aosp/3341487 in 1.7 fun SemanticsPropertyReceiver.invisibleToUser() { this[SemanticsProperties.InvisibleToUser] = Unit } /** * If present, this node is considered hidden from accessibility services. * * For example, if the node is currently occluded by a dark semitransparent pane above it, then for * all practical purposes the node should not be announced to the user. Since the system cannot * automatically determine that, this property can be set to make the screen reader linear * navigation skip over this type of node. * * If looking for a way to clear semantics of small items from the UI tree completely because they * are redundant with semantics of their parent, consider [SemanticsModifier.clearAndSetSemantics] * instead. */ fun SemanticsPropertyReceiver.hideFromAccessibility() { this[SemanticsProperties.HideFromAccessibility] = Unit } /** * Content field type information. * * This API can be used to indicate to Autofill services what _kind of field_ is associated with * this node. Not to be confused with the _data type_ to be entered into the field. * * @see SemanticsProperties.ContentType */ var SemanticsPropertyReceiver.contentType by SemanticsProperties.ContentType /** * Content data type information. * * This API can be used to indicate to Autofill services what _kind of data_ is meant to be * suggested for this field. Not to be confused with the _type_ of the field. * * @see SemanticsProperties.ContentType */ var SemanticsPropertyReceiver.contentDataType by SemanticsProperties.ContentDataType /** * The current value of a component that can be autofilled. * * This property is used to expose the component's current data *to* the autofill service. The * service can then read this value, for example, to save it for future autofill suggestions. * * This is the counterpart to the [onFillData] action, which is used to *receive* data from the * autofill service. * * @sample androidx.compose.ui.samples.AutofillableTextFieldWithFillableDataSemantics * @see SemanticsProperties.FillableData */ var SemanticsPropertyReceiver.fillableData by SemanticsProperties.FillableData /** * A value to manually control screenreader traversal order. * * This API can be used to customize TalkBack traversal order. When the `traversalIndex` property is * set on a traversalGroup or on a screenreader-focusable node, then the sorting algorithm will * prioritize nodes with smaller `traversalIndex`s earlier. The default traversalIndex value is * zero, and traversalIndices are compared at a peer level. * * For example,` traversalIndex = -1f` can be used to force a top bar to be ordered earlier, and * `traversalIndex = 1f` to make a bottom bar ordered last, in the edge cases where this does not * happen by default. As another example, if you need to reorder two Buttons within a Row, then you * can set `isTraversalGroup = true` on the Row, and set `traversalIndex` on one of the Buttons. * * Note that if `traversalIndex` seems to have no effect, be sure to set `isTraversalGroup = true` * as well. */ var SemanticsPropertyReceiver.traversalIndex by SemanticsProperties.TraversalIndex /** The horizontal scroll state of this node if this node is scrollable. */ var SemanticsPropertyReceiver.horizontalScrollAxisRange by SemanticsProperties.HorizontalScrollAxisRange /** The vertical scroll state of this node if this node is scrollable. */ var SemanticsPropertyReceiver.verticalScrollAxisRange by SemanticsProperties.VerticalScrollAxisRange /** * Whether this semantics node represents a Popup. Not to be confused with if this node is _part of_ * a Popup. */ fun SemanticsPropertyReceiver.popup() { this[SemanticsProperties.IsPopup] = Unit } /** * Whether this element is a Dialog. Not to be confused with if this element is _part of_ a Dialog. */ fun SemanticsPropertyReceiver.dialog() { this[SemanticsProperties.IsDialog] = Unit } /** * The type of user interface element. Accessibility services might use this to describe the element * or do customizations. Most roles can be automatically resolved by the semantics properties of * this element. But some elements with subtle differences need an exact role. If an exact role is * not listed in [Role], this property should not be set and the framework will automatically * resolve it. */ var SemanticsPropertyReceiver.role by SemanticsProperties.Role /** * Test tag attached to this semantics node. * * This can be used to find nodes in testing frameworks: * - In Compose's built-in unit test framework, use with * [onNodeWithTag][androidx.compose.ui.test.onNodeWithTag]. * - For newer AccessibilityNodeInfo-based integration test frameworks, it can be matched in the * extras with key "androidx.compose.ui.semantics.testTag" * - For legacy AccessibilityNodeInfo-based integration tests, it's optionally exposed as the * resource id if [testTagsAsResourceId] is true (for matching with 'By.res' in UIAutomator). */ var SemanticsPropertyReceiver.testTag by SemanticsProperties.TestTag /** * Text of the semantics node. It must be real text instead of developer-set content description. * * @see SemanticsPropertyReceiver.editableText */ var SemanticsPropertyReceiver.text: AnnotatedString get() = throwSemanticsGetNotSupported() set(value) { set(SemanticsProperties.Text, listOf(value)) } /** * Text substitution of the semantics node. This property is only available after calling * [SemanticsActions.SetTextSubstitution]. */ var SemanticsPropertyReceiver.textSubstitution by SemanticsProperties.TextSubstitution /** * Whether this element is showing the text substitution. This property is only available after * calling [SemanticsActions.SetTextSubstitution]. */ var SemanticsPropertyReceiver.isShowingTextSubstitution by SemanticsProperties.IsShowingTextSubstitution /** * The raw value of the text field after input transformations have been applied. * * This is an actual user input of the fields, e.g. a real password, after any input transformations * that might change or reject that input have been applied. This value is not affected by visual * transformations. */ var SemanticsPropertyReceiver.inputText by SemanticsProperties.InputText /** * A visual value of the text field after output transformations that change the visual * representation of the field's state have been applied. * * This is the value displayed to the user, for example "*******" in a password field. */ var SemanticsPropertyReceiver.editableText by SemanticsProperties.EditableText /** Text selection range for the text field. */ var SemanticsPropertyReceiver.textSelectionRange by SemanticsProperties.TextSelectionRange /** * Contains the IME action provided by the node. * * For example, "go to next form field" or "submit". * * A node that specifies an action should also specify a callback to perform the action via * [onImeAction]. */ @Deprecated("Pass the ImeAction to onImeAction instead.") @get:Deprecated("Pass the ImeAction to onImeAction instead.") @set:Deprecated("Pass the ImeAction to onImeAction instead.") var SemanticsPropertyReceiver.imeAction by SemanticsProperties.ImeAction /** * Whether this element is selected (out of a list of possible selections). * * The presence of this property indicates that the element is selectable. */ var SemanticsPropertyReceiver.selected by SemanticsProperties.Selected /** * This semantics marks node as a collection and provides the required information. * * @see collectionItemInfo */ var SemanticsPropertyReceiver.collectionInfo by SemanticsProperties.CollectionInfo /** * This semantics marks node as an items of a collection and provides the required information. * * If you mark items of a collection, you should also be marking the collection with * [collectionInfo]. */ var SemanticsPropertyReceiver.collectionItemInfo by SemanticsProperties.CollectionItemInfo /** * The state of a toggleable component. * * The presence of this property indicates that the element is toggleable. */ var SemanticsPropertyReceiver.toggleableState by SemanticsProperties.ToggleableState /** Whether this semantics node is editable, e.g. an editable text field. */ var SemanticsPropertyReceiver.isEditable by SemanticsProperties.IsEditable /** The node is marked as a password. */ fun SemanticsPropertyReceiver.password() { this[SemanticsProperties.Password] = Unit } /** * Mark semantics node that contains invalid input or error. * * @param [description] a localized description explaining an error to the accessibility user */ fun SemanticsPropertyReceiver.error(description: String) { this[SemanticsProperties.Error] = description } /** * The index of an item identified by a given key. The key is usually defined during the creation of * the container. If the key did not match any of the items' keys, the [mapping] must return -1. */ fun SemanticsPropertyReceiver.indexForKey(mapping: (Any) -> Int) { this[SemanticsProperties.IndexForKey] = mapping } /** * Limits the number of characters that can be entered, e.g. in an editable text field. By default * this value is -1, signifying there is no maximum text length limit. */ var SemanticsPropertyReceiver.maxTextLength by SemanticsProperties.MaxTextLength /** The shape of the UI element. */ var SemanticsPropertyReceiver.shape by SemanticsProperties.Shape /** * The node is marked as a collection of horizontally or vertically stacked selectable elements. * * Unlike [collectionInfo] which marks a collection of any elements and asks developer to provide * all the required information like number of elements etc., this semantics will populate the * number of selectable elements automatically. Note that if you use this semantics with lazy * collections, it won't get the number of elements in the collection. * * @see SemanticsPropertyReceiver.selected */ fun SemanticsPropertyReceiver.selectableGroup() { this[SemanticsProperties.SelectableGroup] = Unit } /** Custom actions which are defined by app developers. */ var SemanticsPropertyReceiver.customActions by SemanticsActions.CustomActions /** * Action to get a Text/TextField node's [TextLayoutResult]. The result is the first element of * layout (the argument of the AccessibilityAction). * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.GetTextLayoutResult] is called. */ fun SemanticsPropertyReceiver.getTextLayoutResult( label: String? = null, action: ((MutableList) -> Boolean)?, ) { this[SemanticsActions.GetTextLayoutResult] = AccessibilityAction(label, action) } /** * Action to be performed when the node is clicked (single-tapped). * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.OnClick] is called. */ fun SemanticsPropertyReceiver.onClick(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.OnClick] = AccessibilityAction(label, action) } /** * Action to be performed when the node is long clicked (long-pressed). * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.OnLongClick] is called. */ fun SemanticsPropertyReceiver.onLongClick(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.OnLongClick] = AccessibilityAction(label, action) } /** * Action to asynchronously scroll by a specified amount. * * [scrollByOffset] should be preferred in most cases, since it is synchronous and returns the * amount of scroll that was actually consumed. * * Expected to be used in conjunction with [verticalScrollAxisRange]/[horizontalScrollAxisRange]. * * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.ScrollBy] is called. */ fun SemanticsPropertyReceiver.scrollBy( label: String? = null, action: ((x: Float, y: Float) -> Boolean)?, ) { this[SemanticsActions.ScrollBy] = AccessibilityAction(label, action) } /** * Action to scroll by a specified amount and return how much of the offset was actually consumed. * E.g. if the node can't scroll at all in the given direction, [Offset.Zero] should be returned. * The action should not return until the scroll operation has finished. * * Expected to be used in conjunction with [verticalScrollAxisRange]/[horizontalScrollAxisRange]. * * Unlike [scrollBy], this action is synchronous, and returns the amount of scroll consumed. * * @param action Action to be performed when [SemanticsActions.ScrollByOffset] is called. */ fun SemanticsPropertyReceiver.scrollByOffset(action: suspend (offset: Offset) -> Offset) { this[SemanticsActions.ScrollByOffset] = action } /** * Action to scroll a container to the index of one of its items. * * The [action] should throw an [IllegalArgumentException] if the index is out of bounds. */ fun SemanticsPropertyReceiver.scrollToIndex(label: String? = null, action: (Int) -> Boolean) { this[SemanticsActions.ScrollToIndex] = AccessibilityAction(label, action) } /** * Action to autofill a TextField. * * Expected to be used in conjunction with [contentType] and [contentDataType] properties. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.OnAutofillText] is called. */ @Deprecated( message = "Use onFillData instead", replaceWith = ReplaceWith("onFillData"), level = DeprecationLevel.WARNING, ) fun SemanticsPropertyReceiver.onAutofillText( label: String? = null, action: ((AnnotatedString) -> Boolean)?, ) { @Suppress("DEPRECATION") this[SemanticsActions.OnAutofillText] = AccessibilityAction(label, action) } /** * Action that an autofill service can invoke to fill the component with data. * * The [action] will be called by the system, passing the [FillableData] that should be used to * update the component's state. * * This is the counterpart to the [fillableData] property, which is used to *provide* the * component's current data to the autofill service. * * @sample androidx.compose.ui.samples.AutofillableTextFieldWithFillableDataSemantics * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.OnFillData] is called. The lambda * receives the [FillableData] from the autofill service. */ fun SemanticsPropertyReceiver.onFillData( label: String? = null, action: ((FillableData) -> Boolean)?, ) { this[SemanticsActions.OnFillData] = AccessibilityAction(label, action) } /** * Action to set the current value of the progress bar. * * Expected to be used in conjunction with progressBarRangeInfo. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.SetProgress] is called. */ fun SemanticsPropertyReceiver.setProgress(label: String? = null, action: ((Float) -> Boolean)?) { this[SemanticsActions.SetProgress] = AccessibilityAction(label, action) } /** * Action to set the text contents of this node. * * Expected to be used on editable text fields. * * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.SetText] is called. */ fun SemanticsPropertyReceiver.setText( label: String? = null, action: ((AnnotatedString) -> Boolean)?, ) { this[SemanticsActions.SetText] = AccessibilityAction(label, action) } /** * Action to set the text substitution of this node. * * Expected to be used on non-editable text. * * Note, this action doesn't show the text substitution. Please call * [SemanticsPropertyReceiver.showTextSubstitution] to show the text substitution. * * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.SetTextSubstitution] is called. */ fun SemanticsPropertyReceiver.setTextSubstitution( label: String? = null, action: ((AnnotatedString) -> Boolean)?, ) { this[SemanticsActions.SetTextSubstitution] = AccessibilityAction(label, action) } /** * Action to show or hide the text substitution of this node. * * Expected to be used on non-editable text. * * Note, this action only takes effect when the node has the text substitution. * * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.ShowTextSubstitution] is called. */ fun SemanticsPropertyReceiver.showTextSubstitution( label: String? = null, action: ((Boolean) -> Boolean)?, ) { this[SemanticsActions.ShowTextSubstitution] = AccessibilityAction(label, action) } /** * Action to clear the text substitution of this node. * * Expected to be used on non-editable text. * * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.ClearTextSubstitution] is called. */ fun SemanticsPropertyReceiver.clearTextSubstitution( label: String? = null, action: (() -> Boolean)?, ) { this[SemanticsActions.ClearTextSubstitution] = AccessibilityAction(label, action) } /** * Action to insert text into this node at the current cursor position, or replacing the selection * if text is selected. * * Expected to be used on editable text fields. * * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.InsertTextAtCursor] is called. */ fun SemanticsPropertyReceiver.insertTextAtCursor( label: String? = null, action: ((AnnotatedString) -> Boolean)?, ) { this[SemanticsActions.InsertTextAtCursor] = AccessibilityAction(label, action) } /** * Action to invoke the IME action handler configured on the node, as well as specify the type of * IME action provided by the node. * * Expected to be used on editable text fields. * * @param imeActionType The IME type, such as [ImeAction.Next] or [ImeAction.Search] * @param label Optional label for this action. * @param action Action to be performed when [SemanticsActions.OnImeAction] is called. * @see SemanticsProperties.ImeAction * @see SemanticsActions.OnImeAction */ fun SemanticsPropertyReceiver.onImeAction( imeActionType: ImeAction, label: String? = null, action: (() -> Boolean)?, ) { this[SemanticsProperties.ImeAction] = imeActionType this[SemanticsActions.OnImeAction] = AccessibilityAction(label, action) } // b/322269946 @Suppress("unused") @Deprecated( message = "Use `SemanticsPropertyReceiver.onImeAction` instead.", replaceWith = ReplaceWith( "onImeAction(imeActionType = ImeAction.Default, label = label, action = action)", "androidx.compose.ui.semantics.onImeAction", "androidx.compose.ui.text.input.ImeAction", ), level = DeprecationLevel.ERROR, ) fun SemanticsPropertyReceiver.performImeAction(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.OnImeAction] = AccessibilityAction(label, action) } /** * Action to set text selection by character index range. * * If this action is provided, the selection data must be provided using [textSelectionRange]. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.SetSelection] is called. The * parameters to the action are: `startIndex`, `endIndex`, and whether the indices are relative to * the original text or the transformed text (when a `VisualTransformation` is applied). */ fun SemanticsPropertyReceiver.setSelection( label: String? = null, action: ((startIndex: Int, endIndex: Int, relativeToOriginalText: Boolean) -> Boolean)?, ) { this[SemanticsActions.SetSelection] = AccessibilityAction(label, action) } /** * Action to copy the text to the clipboard. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.CopyText] is called. */ fun SemanticsPropertyReceiver.copyText(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.CopyText] = AccessibilityAction(label, action) } /** * Action to cut the text and copy it to the clipboard. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.CutText] is called. */ fun SemanticsPropertyReceiver.cutText(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.CutText] = AccessibilityAction(label, action) } /** * This function adds the [SemanticsActions.PasteText] to the [SemanticsPropertyReceiver]. Use it to * indicate that element is open for accepting paste data from the clipboard. There is no need to * check if the clipboard data available as this is done by the framework. For this action to be * triggered, the element must also have the [SemanticsProperties.Focused] property set. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.PasteText] is called. * @see focused */ fun SemanticsPropertyReceiver.pasteText(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.PasteText] = AccessibilityAction(label, action) } /** * Action to expand an expandable node. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.Expand] is called. */ fun SemanticsPropertyReceiver.expand(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.Expand] = AccessibilityAction(label, action) } /** * Action to collapse an expandable node. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.Collapse] is called. */ fun SemanticsPropertyReceiver.collapse(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.Collapse] = AccessibilityAction(label, action) } /** * Action to dismiss a dismissible node. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.Dismiss] is called. */ fun SemanticsPropertyReceiver.dismiss(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.Dismiss] = AccessibilityAction(label, action) } /** * Action that gives input focus to this node. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.RequestFocus] is called. */ fun SemanticsPropertyReceiver.requestFocus(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.RequestFocus] = AccessibilityAction(label, action) } /** * Action to page up. * * Using [Role.Carousel] will prevent this action from being sent to accessibility services. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.PageUp] is called. * @see [Role.Carousel] for more information. */ fun SemanticsPropertyReceiver.pageUp(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.PageUp] = AccessibilityAction(label, action) } /** * Action to page down. * * Using [Role.Carousel] will prevent this action from being sent to accessibility services. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.PageDown] is called. * @see [Role.Carousel] for more information. */ fun SemanticsPropertyReceiver.pageDown(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.PageDown] = AccessibilityAction(label, action) } /** * Action to page left. * * Using [Role.Carousel] will prevent this action from being sent to accessibility services. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.PageLeft] is called. * @see [Role.Carousel] for more information. */ fun SemanticsPropertyReceiver.pageLeft(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.PageLeft] = AccessibilityAction(label, action) } /** * Action to page right. * * Using [Role.Carousel] will prevent this action from being sent to accessibility services. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.PageRight] is called. * @see [Role.Carousel] for more information. */ fun SemanticsPropertyReceiver.pageRight(label: String? = null, action: (() -> Boolean)?) { this[SemanticsActions.PageRight] = AccessibilityAction(label, action) } /** * Action to get a scrollable's active view port amount for scrolling actions. * * @param label Optional label for this action. * @param action Action to be performed when the [SemanticsActions.GetScrollViewportLength] is * called. */ fun SemanticsPropertyReceiver.getScrollViewportLength( label: String? = null, action: (() -> Float?), ) { this[SemanticsActions.GetScrollViewportLength] = AccessibilityAction(label) { val viewport = action.invoke() if (viewport == null) { false } else { it.add(viewport) true } } } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusRequester.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.ui.focus import androidx.compose.runtime.Stable import androidx.compose.runtime.annotation.RememberInComposition import androidx.compose.runtime.collection.MutableVector import androidx.compose.runtime.collection.mutableVectorOf import androidx.compose.ui.focus.FocusDirection.Companion.Enter import androidx.compose.ui.node.Nodes import androidx.compose.ui.node.visitChildren private const val FocusRequesterNotInitialized = """ FocusRequester is not initialized. Here are some possible fixes: 1. Remember the FocusRequester: val focusRequester = remember { FocusRequester() } 2. Did you forget to add a Modifier.focusRequester() ? 3. Are you attempting to request focus during composition? Focus requests should be made in response to some event. Eg Modifier.clickable { focusRequester.requestFocus() } """ private const val InvalidFocusRequesterInvocation = """ Please check whether the focusRequester is FocusRequester.Cancel or FocusRequester.Default before invoking any functions on the focusRequester. """ /** * The [FocusRequester] is used in conjunction with * [Modifier.focusRequester][androidx.compose.ui.focus.focusRequester] to send requests to change * focus. * * @sample androidx.compose.ui.samples.RequestFocusSample * @see androidx.compose.ui.focus.focusRequester */ @Stable class FocusRequester @RememberInComposition constructor() { internal val focusRequesterNodes: MutableVector = mutableVectorOf() /** * Use this function to request focus. If the system grants focus to a component associated with * this [FocusRequester], its [onFocusChanged] modifiers will receive a [FocusState] object * where [FocusState.isFocused] is true. * * @sample androidx.compose.ui.samples.RequestFocusSample */ @Deprecated( message = "use the version the has a FocusDirection", replaceWith = ReplaceWith("this.requestFocus()"), level = DeprecationLevel.HIDDEN, ) fun requestFocus() { requestFocus(Enter) } /** * Use this function to request focus with a specific direction. If the system grants focus to a * component associated with this [FocusRequester], its [onFocusChanged] modifiers will receive * a [FocusState] object where [FocusState.isFocused] is true. * * @param focusDirection The direction passed to the [FocusTargetModifierNode] to indicate the * direction that the focus request comes from. * @return `true` if the focus was successfully requested or `false` if the focus request was * canceled. * @sample androidx.compose.ui.samples.RequestFocusSample */ fun requestFocus(focusDirection: FocusDirection = Enter): Boolean { return findFocusTarget { it.requestFocus(focusDirection) } } /** * Deny requests to clear focus. * * Use this function to send a request to capture focus. If a component captures focus, it will * send a [FocusState] object to its associated [onFocusChanged] modifiers where * [FocusState.isCaptured]() == true. * * When a component is in a Captured state, all focus requests from other components are * declined. * * @return true if the focus was successfully captured by one of the [focus][focusTarget] * modifiers associated with this [FocusRequester]. False otherwise. * @sample androidx.compose.ui.samples.CaptureFocusSample */ fun captureFocus(): Boolean { if (focusRequesterNodes.isEmpty()) { println("$FocusWarning: $FocusRequesterNotInitialized") return false } focusRequesterNodes.forEach { if (it.captureFocus()) { return true } } return false } /** * Use this function to send a request to free focus when one of the components associated with * this [FocusRequester] is in a Captured state. If a component frees focus, it will send a * [FocusState] object to its associated [onFocusChanged] modifiers where * [FocusState.isCaptured]() == false. * * When a component is in a Captured state, all focus requests from other components are * declined. . * * @return true if the captured focus was successfully released. i.e. At the end of this * operation, one of the components associated with this [focusRequester] freed focus. * @sample androidx.compose.ui.samples.CaptureFocusSample */ fun freeFocus(): Boolean { if (focusRequesterNodes.isEmpty()) { println("$FocusWarning: $FocusRequesterNotInitialized") return false } focusRequesterNodes.forEach { if (it.freeFocus()) { return true } } return false } /** * Use this function to request the focus target to save a reference to the currently focused * child in its saved instance state. After calling this, focus can be restored to the saved * child by making a call to [restoreFocusedChild]. * * @return true if the focus target associated with this [FocusRequester] has a focused child * and we successfully saved a reference to it. * @sample androidx.compose.ui.samples.RestoreFocusSample */ // TODO: Deprecate once focus restoration is enabled by default via flags. // @Deprecated( // message = // "The focused child is now saved automatically whenever focus changes. Just call" + // " restoreFocusedChild to restore focus.", // level = DeprecationLevel.WARNING, // ) fun saveFocusedChild(): Boolean { if (focusRequesterNodes.isEmpty()) { println("$FocusWarning: $FocusRequesterNotInitialized") return false } focusRequesterNodes.forEach { if (it.saveFocusedChild()) return true } return false } /** * Use this function to restore focus to one of the children of the node pointed to by this * [FocusRequester]. This restores focus to a previously focused child that was saved by using * [saveFocusedChild]. * * @return true if we successfully restored focus to one of the children of the [focusTarget] * associated with this [FocusRequester] * @sample androidx.compose.ui.samples.RestoreFocusSample */ fun restoreFocusedChild(): Boolean { if (focusRequesterNodes.isEmpty()) { println("$FocusWarning: $FocusRequesterNotInitialized") return false } var success = false focusRequesterNodes.forEach { success = it.restoreFocusedChild() || success } return success } companion object { /** * Default [focusRequester], which when used in [Modifier.focusProperties][focusProperties] * implies that we want to use the default system focus order, that is based on the position * of the items on the screen. */ val Default = FocusRequester() /** * Cancelled [focusRequester], which when used in * [Modifier.focusProperties][focusProperties] implies that we want to block focus search * from proceeding in the specified [direction][FocusDirection]. * * @sample androidx.compose.ui.samples.CancelFocusMoveSample */ val Cancel = FocusRequester() /** Used to indicate that the focus has been redirected during an enter/exit lambda. */ internal val Redirect = FocusRequester() /** * Convenient way to create multiple [FocusRequester] instances. * * @sample androidx.compose.ui.samples.CreateFocusRequesterRefsSample */ object FocusRequesterFactory { operator fun component1() = FocusRequester() operator fun component2() = FocusRequester() operator fun component3() = FocusRequester() operator fun component4() = FocusRequester() operator fun component5() = FocusRequester() operator fun component6() = FocusRequester() operator fun component7() = FocusRequester() operator fun component8() = FocusRequester() operator fun component9() = FocusRequester() operator fun component10() = FocusRequester() operator fun component11() = FocusRequester() operator fun component12() = FocusRequester() operator fun component13() = FocusRequester() operator fun component14() = FocusRequester() operator fun component15() = FocusRequester() operator fun component16() = FocusRequester() } /** * Convenient way to create multiple [FocusRequester]s, which can to be used to request * focus, or to specify a focus traversal order. * * @sample androidx.compose.ui.samples.CreateFocusRequesterRefsSample */ fun createRefs(): FocusRequesterFactory = FocusRequesterFactory } /** * This function searches down the hierarchy and calls [onFound] for all focus nodes associated * with this [FocusRequester]. * * @param onFound the callback that is run when the child is found. * @return false if no focus nodes were found or if the FocusRequester is * [FocusRequester.Cancel]. Returns a logical or of the result of calling [onFound] for each * focus node associated with this [FocusRequester]. */ internal inline fun findFocusTarget(onFound: (FocusTargetNode) -> Boolean): Boolean { check(this !== Default) { InvalidFocusRequesterInvocation } check(this !== Cancel) { InvalidFocusRequesterInvocation } if (focusRequesterNodes.isEmpty()) { println("$FocusWarning: $FocusRequesterNotInitialized") return false } var success = false focusRequesterNodes.forEach { node -> node.visitChildren(Nodes.FocusTarget) { if (onFound(it)) { success = true return@forEach } } } return success } } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/focus/FocusManager.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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 androidx.compose.ui.focus import androidx.compose.ui.internal.JvmDefaultWithCompatibility @JvmDefaultWithCompatibility interface FocusManager { /** * Call this function to clear focus from the currently focused component, and set the focus to * the root focus modifier. * * @param force: Whether we should forcefully clear focus regardless of whether we have any * components that have Captured focus. * @sample androidx.compose.ui.samples.ClearFocusSample */ fun clearFocus(force: Boolean = false) /** * Moves focus in the specified [direction][FocusDirection]. * * If you are not satisfied with the default focus order, consider setting a custom order using * [Modifier.focusProperties()][focusProperties]. * * @return true if focus was moved successfully. false if the focused item is unchanged. * @sample androidx.compose.ui.samples.MoveFocusSample */ fun moveFocus(focusDirection: FocusDirection): Boolean } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/platform/CompositionLocals.kt ```kotlin /* * Copyright 2020 The Android Open Source Project * * 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. */ @file:Suppress("DEPRECATION") package androidx.compose.ui.platform import androidx.annotation.RestrictTo import androidx.annotation.VisibleForTesting import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocal import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.ProvidableCompositionLocal import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalWithComputedDefaultOf import androidx.compose.runtime.retain.LocalRetainedValuesStore import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.autofill.Autofill import androidx.compose.ui.autofill.AutofillManager import androidx.compose.ui.autofill.AutofillTree import androidx.compose.ui.draw.DrawModifier import androidx.compose.ui.focus.FocusManager import androidx.compose.ui.graphics.GraphicsContext import androidx.compose.ui.graphics.layer.GraphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.input.InputModeManager import androidx.compose.ui.input.pointer.PointerIconService import androidx.compose.ui.layout.Layout import androidx.compose.ui.node.Owner import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.input.TextInputService import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.intl.LocaleList import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection import androidx.lifecycle.LifecycleOwner /** The CompositionLocal to provide communication with platform accessibility service. */ val LocalAccessibilityManager = staticCompositionLocalOf { null } /** * The CompositionLocal that can be used to trigger autofill actions. Eg. * [Autofill.requestAutofillForNode]. */ @Deprecated( """ Use the new semantics-based Autofill APIs androidx.compose.ui.autofill.ContentType and androidx.compose.ui.autofill.ContentDataType instead. """ ) val LocalAutofill = staticCompositionLocalOf { null } /** * The CompositionLocal that can be used to add [AutofillNode][import * androidx.compose.ui.autofill.AutofillNode]s to the autofill tree. The [AutofillTree] is a * temporary data structure that will be replaced by Autofill Semantics (b/138604305). */ @Deprecated( """ Use the new semantics-based Autofill APIs androidx.compose.ui.autofill.ContentType and androidx.compose.ui.autofill.ContentDataType instead. """ ) val LocalAutofillTree = staticCompositionLocalOf { noLocalProvidedFor("LocalAutofillTree") } /** * The CompositionLocal that can be used to trigger autofill actions. Eg. [AutofillManager.commit]. */ val LocalAutofillManager = staticCompositionLocalOf { noLocalProvidedFor("LocalAutofillManager") } /** The CompositionLocal to provide communication with platform clipboard service. */ @Deprecated( "Use LocalClipboard instead which supports suspend functions", ReplaceWith("LocalClipboard", "androidx.compose.ui.platform.LocalClipboard"), ) val LocalClipboardManager = staticCompositionLocalOf { noLocalProvidedFor("LocalClipboardManager") } /** The CompositionLocal to provide communication with platform clipboard service. */ val LocalClipboard = staticCompositionLocalOf { noLocalProvidedFor("LocalClipboard") } /** * The CompositionLocal to provide access to a [GraphicsContext] instance for creation of * [GraphicsLayer]s. * * Consumers that access this Local directly and call [GraphicsContext.createGraphicsLayer] are * responsible for calling [GraphicsContext.releaseGraphicsLayer]. * * It is recommended that consumers invoke [rememberGraphicsLayer][import * androidx.compose.ui.graphics.rememberGraphicsLayer] instead to ensure that a [GraphicsLayer] is * released when the corresponding composable is disposed. */ val LocalGraphicsContext = staticCompositionLocalOf { noLocalProvidedFor("LocalGraphicsContext") } /** * Provides the [Density] to be used to transform between * [density-independent pixel units (DP)][androidx.compose.ui.unit.Dp] and pixel units or * [scale-independent pixel units (SP)][androidx.compose.ui.unit.TextUnit] and pixel units. This is * typically used when a [DP][androidx.compose.ui.unit.Dp] is provided and it must be converted in * the body of [Layout] or [DrawModifier]. */ val LocalDensity = staticCompositionLocalOf { noLocalProvidedFor("LocalDensity") } /** The CompositionLocal that can be used to control focus within Compose. */ val LocalFocusManager = staticCompositionLocalOf { noLocalProvidedFor("LocalFocusManager") } /** The CompositionLocal to provide platform font loading methods. */ @Suppress("DEPRECATION") @Deprecated( "LocalFontLoader is replaced with LocalFontFamilyResolver", replaceWith = ReplaceWith("LocalFontFamilyResolver"), ) @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val LocalFontLoader = staticCompositionLocalOf { noLocalProvidedFor("LocalFontLoader") } /** The CompositionLocal for compose font resolution from FontFamily. */ val LocalFontFamilyResolver = staticCompositionLocalOf { noLocalProvidedFor("LocalFontFamilyResolver") } /** The CompositionLocal to provide haptic feedback to the user. */ val LocalHapticFeedback = staticCompositionLocalOf { noLocalProvidedFor("LocalHapticFeedback") } /** * The CompositionLocal to provide an instance of InputModeManager which controls the current input * mode. */ val LocalInputModeManager = staticCompositionLocalOf { noLocalProvidedFor("LocalInputManager") } /** The CompositionLocal to provide the layout direction. */ val LocalLayoutDirection = staticCompositionLocalOf { noLocalProvidedFor("LocalLayoutDirection") } /** The providable CompositionLocal to provide the locale list. This list can never be empty. */ @get:VisibleForTesting @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) val LocalProvidableLocaleList: ProvidableCompositionLocal = staticCompositionLocalOf { noLocalProvidedFor("LocalProvidableLocaleList") } /** The CompositionLocal to provide the locale list. This list will never be empty. */ val LocalLocaleList: CompositionLocal get() = LocalProvidableLocaleList /** The CompositionLocal to provide the locale. */ val LocalLocale: CompositionLocal = compositionLocalWithComputedDefaultOf { LocalLocaleList.currentValue.first() } /** The CompositionLocal to provide communication with platform text input service. */ @Deprecated("Use PlatformTextInputModifierNode instead.") val LocalTextInputService = staticCompositionLocalOf { null } /** * The [CompositionLocal] to provide a [SoftwareKeyboardController] that can control the current * software keyboard. * * Will be null if the software keyboard cannot be controlled. */ val LocalSoftwareKeyboardController = staticCompositionLocalOf { null } /** The CompositionLocal to provide text-related toolbar. */ val LocalTextToolbar = staticCompositionLocalOf { noLocalProvidedFor("LocalTextToolbar") } /** The CompositionLocal to provide functionality related to URL, e.g. open URI. */ val LocalUriHandler = staticCompositionLocalOf { noLocalProvidedFor("LocalUriHandler") } /** The CompositionLocal that provides the ViewConfiguration. */ val LocalViewConfiguration = staticCompositionLocalOf { noLocalProvidedFor("LocalViewConfiguration") } /** * The CompositionLocal that provides information about the window that hosts the current [Owner]. */ val LocalWindowInfo = staticCompositionLocalOf { noLocalProvidedFor("LocalWindowInfo") } /** The CompositionLocal containing the current [LifecycleOwner]. */ @Deprecated( "Moved to lifecycle-runtime-compose library in androidx.lifecycle.compose package.", ReplaceWith("androidx.lifecycle.compose.LocalLifecycleOwner"), ) expect val LocalLifecycleOwner: ProvidableCompositionLocal internal val LocalPointerIconService = staticCompositionLocalOf { null } /** @see LocalScrollCaptureInProgress */ internal val LocalProvidableScrollCaptureInProgress = compositionLocalOf { false } /** * True when the system is currently capturing the contents of a scrollable in this compose view or * any parent compose view. */ val LocalScrollCaptureInProgress: CompositionLocal get() = LocalProvidableScrollCaptureInProgress /** * Text cursor blinking * - _true_ normal cursor behavior (interactive blink) * - _false_ never blink (always on) * * The default of _true_ is the user-expected system behavior for Text editing. * * Typically you should not set _false_ outside of screenshot tests without also providing a * `cursorBrush` to `BasicTextField` to implement a custom design */ val LocalCursorBlinkEnabled: ProvidableCompositionLocal = staticCompositionLocalOf { true } @ExperimentalComposeUiApi @Composable internal fun ProvideCommonCompositionLocals( owner: Owner, uriHandler: UriHandler, content: @Composable () -> Unit, ) { CompositionLocalProvider( LocalAccessibilityManager provides owner.accessibilityManager, LocalAutofill provides owner.autofill, LocalAutofillManager provides owner.autofillManager, LocalAutofillTree provides owner.autofillTree, LocalClipboardManager provides owner.clipboardManager, LocalClipboard provides owner.clipboard, LocalDensity provides owner.density, LocalFocusManager provides owner.focusOwner, @Suppress("DEPRECATION") LocalFontLoader providesDefault @Suppress("DEPRECATION") owner.fontLoader, LocalFontFamilyResolver providesDefault owner.fontFamilyResolver, LocalHapticFeedback provides owner.hapticFeedBack, LocalInputModeManager provides owner.inputModeManager, LocalLayoutDirection provides owner.layoutDirection, LocalTextInputService provides owner.textInputService, LocalSoftwareKeyboardController provides owner.softwareKeyboardController, LocalTextToolbar provides owner.textToolbar, LocalUriHandler provides uriHandler, LocalViewConfiguration provides owner.viewConfiguration, LocalWindowInfo provides owner.windowInfo, LocalPointerIconService provides owner.pointerIconService, LocalGraphicsContext provides owner.graphicsContext, LocalRetainedValuesStore provides owner.retainedValuesStore, LocalProvidableLocaleList provides owner.localeList, content = content, ) } private fun noLocalProvidedFor(name: String): Nothing { error("CompositionLocal $name not present") } ``` ## File: compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/ComposeUiFlags.kt ```kotlin /* * Copyright 2024 The Android Open Source Project * * 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. */ @file:JvmName("ComposeRuntimeFlags") package androidx.compose.ui import androidx.compose.ui.node.findNearestAncestor import kotlin.jvm.JvmField import kotlin.jvm.JvmName /** * This is a collection of flags which are used to guard against regressions in some of the * "riskier" refactors or new feature support that is added to this module. These flags are always * "on" in the published artifact of this module, however these flags allow end consumers of this * module to toggle them "off" in case this new path is causing a regression. * * These flags are considered temporary, and there should be no expectation for these flags be * around for an extended period of time. If you have a regression that one of these flags fixes, it * is strongly encouraged for you to file a bug ASAP. * * **Usage:** * * In order to turn a feature off in a debug environment, it is recommended to set this to false in * as close to the initial loading of the application as possible. Changing this value after compose * library code has already been loaded can result in undefined behavior. * * class MyApplication : Application() { * override fun onCreate() { * ComposeUiFlags.SomeFeatureEnabled = false * super.onCreate() * } * } * * In order to turn this off in a release environment, it is recommended to additionally utilize R8 * rules which force a single value for the entire build artifact. This can result in the new code * paths being completely removed from the artifact, which can often have nontrivial positive * performance impact. * * -assumevalues class androidx.compose.ui.ComposeUiFlags { * public static int isRectTrackingEnabled return false * } */ @ExperimentalComposeUiApi object ComposeUiFlags { /** * This enables fixes for View focus. The changes are large enough to require a flag to allow * disabling them. */ // TODO: b/455588830 @field:Suppress("MutableBareField") @JvmField var isViewFocusFixEnabled: Boolean = false /** * This flag enables an alternate approach to fixing the issues addressed by the * [isViewFocusFixEnabled] flag. */ // TODO: b/455592447 @field:Suppress("MutableBareField") @JvmField var isBypassUnfocusableComposeViewEnabled: Boolean = true /** Enable initial focus when a focusable is added to a screen with no focusable content. */ // TODO: b/455601824 @field:Suppress("MutableBareField") @JvmField var isInitialFocusOnFocusableAvailable: Boolean = false /** * Enable focus restoration, by always saving focus. This flag depends on * [isInitialFocusOnFocusableAvailable] also being true. */ // TODO: b/485962036 @field:Suppress("MutableBareField") @JvmField var isFocusRestorationEnabled: Boolean = false /** Flag for enabling indirect pointer event navigation gestures in Compose. */ // TODO: b/455601135 @field:Suppress("MutableBareField") @JvmField var isIndirectPointerNavigationGestureDetectorEnabled: Boolean = true /** Flag enables optimized focus change dispatching logic. */ // TODO: b/455603009 @field:Suppress("MutableBareField") @JvmField var isOptimizedFocusEventDispatchEnabled: Boolean = true /** This flag enables setting the shape semantics property in the graphicsLayer modifiers. */ // TODO: b/455600081 @field:Suppress("MutableBareField") @JvmField var isGraphicsLayerShapeSemanticsEnabled: Boolean = true /** * Enables a fix where [TraversableNode] traversal method [findNearestAncestor] will take into * consideration any delegates that might also be traversable. */ // TODO: b/485962494 @field:Suppress("MutableBareField") @JvmField var isTraversableDelegatesFixEnabled: Boolean = true /** * Enables a change where off-screen children of the partially visible merging nodes (e.g. a * Text node of a Button) inside scrollable container are now also reported in the semantics * tree for Accessibility needs. * * Enabled is correct, and it should be enabled in all apps. */ // TODO: b/484259656 @field:Suppress("MutableBareField") @JvmField var isAccessibilityShouldIncludeOffscreenChildrenEnabled: Boolean = true /** * Enables support of trackpad gesture events. * * If enabled, [androidx.compose.ui.input.pointer.PointerEvent]s can have type of * [androidx.compose.ui.input.pointer.PointerEventType.PanMove] and * [androidx.compose.ui.input.pointer.PointerEventType.ScaleChange], corresponding to * system-recognized gestures on a trackpad. * * These trackpad gestures will also generally be treated as mouse, with the exact behavior * depending on platform specifics. */ // TODO: b/475634969 remove the temporary flag @field:Suppress("MutableBareField") @JvmField var isTrackpadGestureHandlingEnabled: Boolean = true /** * Enable the integration of [LocalUiMediaScope] at the root compose view which provides various * signals for adapting the UI across different devices. * * This feature is experimental and is disabled by default. */ // TODO: b/485160699 - Remove once the API goes stable @field:Suppress("MutableBareField") @JvmField var isMediaQueryIntegrationEnabled: Boolean = false /** * Enables hit test to continue searching for "semantic nodes" if the initial node that is hit * is unimportant from an accessibility semantics node point of view. */ // TODO: b/487663967 @field:Suppress("MutableBareField") @JvmField var isSkipNonImportantSemanticsNodesHitTestEnabled: Boolean = true } ``` ================================================ FILE: .claude/skills/compose-expert/references/state-management.md ================================================ # Jetpack Compose State Management Reference ## State Fundamentals State in Compose is observable data that triggers recomposition when changed. ### Creating State Use type-specific state holders for efficiency: ```kotlin // General-purpose state (Any type) val name = mutableStateOf("Alice") // Primitive specializations (avoid boxing) val count = mutableIntStateOf(0) val progress = mutableFloatStateOf(0.5f) val enabled = mutableStateOf(true) // Boolean has no specialization ``` **Pitfall:** Using `mutableStateOf()` instead of `mutableIntStateOf()` causes unnecessary boxing on every read/write. Primitive specializations are located in `androidx.compose.runtime` (source: `State.kt`). ## remember vs rememberSaveable Both associate state with a composition key, but differ in persistence scope. ### remember - Lives for the composition's lifetime - Lost on process death, configuration changes, back navigation - Best for UI state: selection, expanded/collapsed, scroll position ```kotlin @Composable fun Counter() { var count by remember { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text("Count: $count") } } ``` ### rememberSaveable - Survives process death and configuration changes - Uses `Bundle`-compatible types by default (String, Int, Boolean, etc.) - For custom types, provide a `Saver` or use `@Parcelize` - Best for data that represents user input or navigation state ```kotlin @Composable fun SearchScreen() { var query by rememberSaveable { mutableStateOf("") } // survives configuration change } // Custom type requires explicit Saver data class User(val id: Int, val name: String) val userSaver = Saver( save = { "${it.id}:${it.name}" }, restore = { parts -> User(parts.split(":")[0].toInt(), parts.split(":")[1]) } ) var user by rememberSaveable(stateSaver = userSaver) { mutableStateOf(User(1, "Alice")) } ``` **Pitfall:** Assuming `rememberSaveable` works with all types. Custom classes need explicit `Saver` or `@Parcelize`. See `SaveableStateRegistry` in `androidx.compose.runtime.saveable`. ## State Hoisting Move state up to a parent composable to enable reusability and testing. ### Stateful vs Stateless Pattern ```kotlin // ❌ Stateful version (tightly coupled) @Composable fun Counter() { var count by remember { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text(count.toString()) } } // ✅ Stateless version (reusable, testable) @Composable fun Counter( count: Int, onCountChange: (Int) -> Unit ) { Button(onClick = { onCountChange(count + 1) }) { Text(count.toString()) } } // ✅ Wrapper composable (provides state, uses stateless child) @Composable fun StatefulCounter() { var count by remember { mutableIntStateOf(0) } Counter(count = count, onCountChange = { count = it }) } ``` **Rule:** Push state as high as needed, but no higher. If only one child needs state, keep it there. If multiple children or parents need it, hoist up. ## derivedStateOf Computes a value from existing state, recomputing only when dependencies change. ```kotlin // ❌ Wrong: recomputes on every recomposition val isEven = count % 2 == 0 // ✅ Correct: recomputes only when count changes val isEven = derivedStateOf { count % 2 == 0 } ``` **When to use:** - Expensive computations from state (e.g., filtering, sorting lists) - Combining multiple state values - Creating intermediate state for conditional logic ```kotlin @Composable fun UserList(users: List, filterText: String) { val filteredUsers = derivedStateOf { users.filter { it.name.contains(filterText, ignoreCase = true) } } LazyColumn { items(filteredUsers.value.size) { index -> UserRow(filteredUsers.value[index]) } } } ``` **Pitfall:** Using `derivedStateOf` for cheap operations (String concatenation, simple conditions) adds overhead. Only use when the computation is non-trivial. **Pitfall:** Accessing `.value` in a lambda passed to a child composable doesn't create a dependency. Use `snapshotFlow` for callbacks. ## snapshotFlow Converts Compose state to Kotlin Flow for side effects and external APIs. ```kotlin @Composable fun SearchScreen(viewModel: SearchViewModel) { var query by remember { mutableStateOf("") } LaunchedEffect(Unit) { snapshotFlow { query } .debounce(500) .distinctUntilChanged() .collect { viewModel.search(it) } } } ``` **Key behaviors:** - Emits initial value, then only on changes - Works with derivedStateOf, collections, and nested state - Runs in the composition's coroutine scope (launched via `LaunchedEffect`) **Pitfall:** Accessing state directly in a `LaunchedEffect` doesn't track changes: ```kotlin // ❌ Won't re-run when query changes LaunchedEffect(Unit) { viewModel.search(query) // Capture at launch time only } // ✅ Re-runs when query changes LaunchedEffect(query) { viewModel.search(query) } ``` ## SnapshotStateList and SnapshotStateMap Observable collections that trigger recomposition on structural changes. ```kotlin val items = remember { mutableStateListOf() } items.add(Item(1, "First")) items[0] = Item(1, "Updated") items.removeAt(0) val map = remember { mutableStateMapOf() } map["key"] = "value" // Triggers recomposition ``` **Important:** Changes to list contents trigger recomposition, but changes to list *elements* (if they're mutable objects) do not. ```kotlin data class Item(val id: Int, var name: String) val items = remember { mutableStateListOf(Item(1, "First")) } // ✅ Triggers recomposition (list structure changed) items[0] = Item(1, "Updated") // ❌ Does NOT trigger recomposition (object mutated in-place) items[0].name = "Updated" // Mutated but list reference unchanged // ✅ Correct: use copy() or mutableStateOf for nested state items[0] = items[0].copy(name = "Updated") ``` See source: `androidx.compose.runtime.snapshots` for collection implementation. ## @Stable and @Immutable Annotations These annotations help the compiler optimize recomposition (strong skipping mode). ### @Immutable - All public fields are read-only primitives or other `@Immutable` types - Instances never change after construction - Compiler can skip recomposition if parameter unchanged ```kotlin @Immutable data class User(val id: Int, val name: String) ``` ### @Stable - Implements structural equality (`equals`) - Public properties are read-only or observable - Changes are always notified to Compose (through state objects) - Weaker guarantee than `@Immutable`, but suitable for types with observable state ```kotlin @Stable class UserViewModel { val userName: State = mutableStateOf("") val isLoading: State = mutableStateOf(false) // Observable state, not direct properties } ``` **Pitfall:** Not annotating data classes used as parameters. Unannotated types are assumed unstable, triggering unnecessary recompositions. ```kotlin // ❌ Treated as unstable, causes recomposition class Config(val title: String, val color: Color) // ✅ Properly annotated @Immutable class Config(val title: String, val color: Color) ``` ## Strong Skipping Mode In Compose 1.6+, strong skipping mode applies stricter recomposition logic. **What changed:** - Composables skip recomposition if *all* parameters have unchanged identity and value - Unannotated parameter types are treated as unstable (always recompose) - `@Stable` and `@Immutable` annotations are now critical for performance - Lambda parameters always cause recomposition (they're new instances) **Enable strong skipping:** ```gradle composeOptions { kotlinCompilerExtensionVersion = "1.5.4+" // enables by default } ``` **Practical impact:** ```kotlin // ❌ These create new instances, always recompose child @Composable fun Parent() { Child(title = buildString { append("Title") }) Child(config = Config(...)) // Unstable type } // ✅ Cache instances @Composable fun Parent() { val title = remember { "Title" } val config = remember { Config(...) } Child(title = title) Child(config = config) } ``` ## State in ViewModels: StateFlow vs Compose State ### StateFlow (Recommended for ViewModel) - Survives composition recomposition and configuration changes - Works with lifecycle (`collectAsStateWithLifecycle`) - Thread-safe, works across layers ```kotlin class UserViewModel : ViewModel() { private val _uiState = MutableStateFlow(UiState.Loading) val uiState: StateFlow = _uiState.asStateFlow() } @Composable fun UserScreen(viewModel: UserViewModel) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() when (uiState) { is UiState.Loading -> LoadingScreen() is UiState.Success -> SuccessScreen((uiState as UiState.Success).data) is UiState.Error -> ErrorScreen((uiState as UiState.Error).message) } } ``` ### Compose State (For UI-only state) - Use for temporary, UI-local state - Don't hoist to ViewModel - Lost on back navigation ```kotlin @Composable fun SearchScreen(viewModel: SearchViewModel) { var showFilters by remember { mutableStateOf(false) } // UI-only val searchResults by viewModel.searchResults.collectAsStateWithLifecycle() SearchUI( results = searchResults, showFilters = showFilters, onToggleFilters = { showFilters = !showFilters } ) } ``` **Key difference:** `collectAsStateWithLifecycle()` (in `androidx.lifecycle:lifecycle-runtime-compose`) collects only when the composable is in a STARTED state, avoiding memory leaks. ## Common Anti-Patterns ### State in Local Variables ```kotlin // ❌ Lost on recomposition @Composable fun Counter() { var count = 0 // Reset to 0 on every recomposition Button(onClick = { count++ }) { Text(count.toString()) } } // ✅ Correct @Composable fun Counter() { var count by remember { mutableIntStateOf(0) } Button(onClick = { count++ }) { Text(count.toString()) } } ``` ### Reading State in Wrong Scope ```kotlin // ❌ Reads happen inside lambda; changes don't re-launch effect var count by remember { mutableIntStateOf(0) } LaunchedEffect(Unit) { while (true) { delay(1000) println(count) // Always prints 0 } } // ✅ Pass state to LaunchedEffect key LaunchedEffect(count) { println("Count changed: $count") } ``` ### Creating State in Lambdas ```kotlin // ❌ Creates new state on every call val onButtonClick = { val newValue = remember { mutableStateOf(0) } // ERROR: Can't call remember in lambda } // ✅ Create state at composition level var value by remember { mutableIntStateOf(0) } val onButtonClick = { value++ } ``` --- **Source references:** `androidx.compose.runtime.State`, `androidx.compose.runtime.saveable`, `androidx.lifecycle.runtime.compose` --- ## produceState Bridge between suspend functions and Compose state: ```kotlin @Composable fun UserProfile(userId: String): State = produceState(initialValue = null, userId) { value = repository.getUser(userId) } ``` Use when you need to convert a suspend function result into observable State. The coroutine is scoped to the composition and cancelled when the composable leaves. Can also observe flows: ```kotlin @Composable fun NetworkStatus(): State = produceState(initialValue = false) { connectivityManager.observeNetworkState().collect { value = it } } ``` --- ## rememberUpdatedState Capture latest callback value in long-running effects: ```kotlin @Composable fun Timer(onTimeout: () -> Unit) { val currentOnTimeout by rememberUpdatedState(onTimeout) LaunchedEffect(true) { delay(5000L) currentOnTimeout() // Always calls the latest onTimeout, even if it changed } } ``` Use when: a LaunchedEffect captures a callback that might change, but you don't want to restart the effect. Without `rememberUpdatedState`, the effect would use the stale original callback or need to restart on every callback change. --- ## Sealed UiState Pattern ```kotlin sealed interface UiState { data object Loading : UiState data class Success(val data: T) : UiState data class Error(val message: String) : UiState } ``` Smart-cast safety: ```kotlin // BAD: smart cast can fail if uiState changes between check and usage if (uiState is UiState.Success) { Content((uiState as UiState.Success).data) // Unsafe cast } // GOOD: val capture for safe smart cast when (val state = uiState) { is UiState.Loading -> LoadingIndicator() is UiState.Success -> Content(state.data) // Safe smart cast via val is UiState.Error -> ErrorMessage(state.message) } ``` --- ## State Holder Class Pattern For complex screens with multiple interrelated state values, create a state holder: ```kotlin @Composable fun rememberSearchState( listState: LazyListState = rememberLazyListState(), coroutineScope: CoroutineScope = rememberCoroutineScope() ): SearchState = remember(listState, coroutineScope) { SearchState(listState, coroutineScope) } @Stable class SearchState( val listState: LazyListState, private val coroutineScope: CoroutineScope ) { var query by mutableStateOf("") private set val isScrolled: Boolean get() = listState.firstVisibleItemIndex > 0 fun updateQuery(newQuery: String) { query = newQuery } fun scrollToTop() { coroutineScope.launch { listState.animateScrollToItem(0) } } } ``` This pattern (used by `rememberScrollState`, `rememberDrawerState`, etc.) groups related state and logic into a single class, avoiding parameter bloat in composables. --- ## Production State Rules ### 1. mutableStateOf ONLY in composables, never in ViewModels ```kotlin // BAD: Compose state in ViewModel couples VM to Compose runtime class MyViewModel : ViewModel() { var name by mutableStateOf("") // Don't do this } // GOOD: StateFlow in ViewModel — framework-agnostic, testable class MyViewModel : ViewModel() { private val _name = MutableStateFlow("") val name = _name.asStateFlow() fun updateName(new: String) { _name.value = new } } ``` ### 2. SharedFlow for one-shot events, NOT Channel ```kotlin // GOOD: SharedFlow with buffer for one-shot events private val _events = MutableSharedFlow(extraBufferCapacity = 1) val events = _events.asSharedFlow() // Emit from ViewModel fun onAction() { _events.tryEmit(AppEvent.ShowSnackbar("Done")) } // Collect in composable LaunchedEffect(Unit) { viewModel.events.collect { event -> when (event) { is AppEvent.ShowSnackbar -> snackbarHostState.showSnackbar(event.message) is AppEvent.Navigate -> onNavigate(event.route) } } } ``` ### 3. rememberSaveable only at NavGraph level Use `rememberSaveable` for screen-level state (search query, tab selection) at the NavGraph entry point, not deep inside composable trees where it adds unnecessary persistence overhead. ### 4. snapshotFlow + distinctUntilChanged() for reactive scroll ```kotlin LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .distinctUntilChanged() .collect { index -> viewModel.onScrollPositionChanged(index) } } ``` ### 5. .stateIn() with .map() for derived flows ```kotlin val filteredItems = repository.items .map { items -> items.filter { it.isActive } } .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) ``` --- ## Compose Multiplatform Notes ### rememberSaveable and Bundle `rememberSaveable`, `Bundle`, and `@Parcelize` are **Android-only**. On CMP targets: ```kotlin // Android: @Parcelize works @Parcelize data class SearchParams(val query: String, val filters: List) : Parcelable // CMP: use @Serializable instead @Serializable data class SearchParams(val query: String, val filters: List) ``` For state persistence across configuration changes in CMP, use kotlinx.serialization-based custom `Saver` implementations. ### collectAsStateWithLifecycle `collectAsStateWithLifecycle()` is in `androidx.lifecycle:lifecycle-runtime-compose` -- it's Android-specific. ```kotlin // Android: lifecycle-aware, stops collecting when paused val state by viewModel.uiState.collectAsStateWithLifecycle() // CMP commonMain: basic collection, does NOT stop in background val state by viewModel.uiState.collectAsState() // CMP with multiplatform lifecycle (lifecycle-runtime-compose:2.10.0+): // collectAsStateWithLifecycle() available in commonMain ``` On CMP without the multiplatform lifecycle library, flows continue collecting when the app is backgrounded -- be aware of battery and performance implications. ================================================ FILE: .claude/skills/compose-expert/references/styles-experimental.md ================================================ # Compose Styles API (Experimental) > `@ExperimentalFoundationStyleApi` — `androidx.compose.foundation:foundation:1.11.0-alpha06` > > AOSP source: `compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/style/` ## What is the Styles API? A declarative, state-driven styling system for Compose. Instead of manually chaining modifiers and `animateXAsState` calls for every interaction state, you declare all visual states in a single `Style { }` block. The framework handles state detection, property interpolation, and animation automatically. ### Before vs After ```kotlin // BEFORE — 15+ lines of imperative state wiring val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val bgColor by animateColorAsState( if (isPressed) Color.DarkBlue else Color.Blue ) val scale by animateFloatAsState( if (isPressed) 0.95f else 1f ) Box( Modifier .graphicsLayer { scaleX = scale; scaleY = scale } .background(bgColor, RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp)) .clickable(interactionSource = interactionSource, indication = null) { } .padding(16.dp) ) // AFTER — Declarative. Done. val style = Style { background(Color.Blue) shape(RoundedCornerShape(16.dp)) contentPadding(16.dp) pressed(Style { animate(Style { background(Color.DarkBlue) scale(0.95f) }) }) } Box(Modifier.styleable(styleState = styleState, style = style)) ``` The mental shift: **stop telling Compose how to animate between states**. Declare what each state looks like — the framework interpolates. --- ## The Three Pieces ### 1. `Style { }` — Declare Visual States A `Style` is a `fun interface`. You use the builder DSL: ```kotlin val cardStyle = Style { // Base properties (always applied) background(Color(0xFFF5F5F5)) shape(RoundedCornerShape(12.dp)) contentPadding(16.dp) // State overrides — only applied when state is active selected(Style { animate(Style { background(Color.Blue.copy(alpha = 0.15f)) borderWidth(2.dp) borderColor(Color.Blue) }) }) disabled(Style { background(Color(0xFFE0E0E0)) contentColor(Color.Gray) // no animate = instant snap }) } ``` `animate(Style { })` wraps properties that should interpolate smoothly. Without `animate`, state changes snap immediately. ### 2. `MutableStyleState` — Drive State ```kotlin // For toggle states (checked, selected, enabled) — set explicitly: val styleState = remember { MutableStyleState(MutableInteractionSource()) } styleState.isChecked = isChecked styleState.isSelected = isSelected styleState.isEnabled = isEnabled // For interaction states (pressed, hovered, focused) — share interactionSource: val interactionSource = remember { MutableInteractionSource() } val styleState = remember { MutableStyleState(interactionSource) } // isPressed, isHovered, isFocused auto-track from shared interactionSource ``` ### 3. `Modifier.styleable()` — Apply to Any Composable ```kotlin Box( Modifier .styleable(styleState = styleState, style = cardStyle) .clickable(interactionSource = interactionSource, indication = null) { } ) ``` Background renders, shape clips, borders draw, transforms apply, text properties propagate to children via CompositionLocal. All animated. --- ## CRITICAL: Alpha06 Auto-Detection is Broken **`styleable(style = myStyle)` without an explicit `styleState` does NOT detect interaction states from sibling modifiers.** This is the single biggest trap in alpha06. ### What DOESN'T work: ```kotlin // Compiles. Renders base style. State changes are SILENT. Box( Modifier .styleable(style = myStyle) // no styleState! .toggleable(value = isChecked, ...) // style never sees this ) ``` ### What DOES work: **Pattern A — Toggle states (checked, selected, enabled):** ```kotlin val styleState = remember { MutableStyleState(MutableInteractionSource()) } styleState.isChecked = isChecked // YOU drive the state Box( Modifier .styleable(styleState = styleState, style = myStyle) .clickable { isChecked = !isChecked } ) ``` **Pattern B — Interaction states (pressed, hovered, focused):** ```kotlin val interactionSource = remember { MutableInteractionSource() } val styleState = remember { MutableStyleState(interactionSource) } Box( Modifier .styleable(styleState = styleState, style = myStyle) .clickable( interactionSource = interactionSource, // same instance! indication = null, ) { } ) ``` **Pattern C — Both (toggle button with press feedback):** ```kotlin val interactionSource = remember { MutableInteractionSource() } val styleState = remember { MutableStyleState(interactionSource) } styleState.isChecked = isChecked // explicit for toggle Box( Modifier .styleable(styleState = styleState, style = myStyle) .clickable( interactionSource = interactionSource, indication = null, ) { isChecked = !isChecked } ) ``` **Rule: always pass `styleState` to `styleable()`.** --- ## Text Property Propagation Gotcha `contentColor()`, `fontSize()`, `fontWeight()`, `letterSpacing()`, `textDecoration()`, and other text properties propagate to ALL child composables inside the styleable box via `CompositionLocal` (`LocalContentColor`, `LocalTextStyle`). ### Problem: ```kotlin // fontSize(28.sp) from the Style applies to BOTH texts! Box(Modifier.styleable(style = Style { fontSize(28.sp) })) { Text("Title") // 28sp Text("Subtitle") // also 28sp — overlap! } ``` ### Fix: Use a single `Text` inside styled boxes when style sets text properties. Move descriptions outside the styleable scope: ```kotlin Text("Description goes here") // outside the styled box Box(Modifier.styleable(style = gradientStyle)) { Text("Title Only") // only this gets styled } ``` --- ## Verified Properties (alpha06, tested on device) | Property | Works? | Notes | |----------|--------|-------| | `background(Color)` | Yes | Fills behind content | | `background(Brush)` | Yes | Gradient backgrounds | | `shape(Shape)` | Yes | Clips content + background | | `contentPadding(Dp)` | Yes | Inner padding | | `borderWidth(Dp) + borderColor(Color)` | Yes | Must set both | | `scale(Float)` | Yes | graphicsLayer transform | | `rotationZ(Float)` | Yes | graphicsLayer rotation | | `translationX/Y(Float)` | Yes | graphicsLayer offset | | `alpha(Float)` | Yes | Opacity | | `contentColor(Color)` | Yes | Propagates to child Text/Icon | | `contentBrush(Brush)` | Yes | Gradient text | | `fontSize(TextUnit)` | Yes | Propagates to children | | `fontWeight(FontWeight)` | Yes | Propagates to children | | `letterSpacing(TextUnit)` | Yes | Propagates to children | | `textDecoration(TextDecoration)` | Yes | Underline, strikethrough | | `animate(Style { })` | Yes | Smooth spring interpolation | | `dropShadow(Shadow)` | No | `Shadow` constructor is internal | --- ## Style Composition Styles compose with `.then()` — later styles override earlier ones per-property: ```kotlin val base = Style { background(Color.Blue) shape(RoundedCornerShape(12.dp)) contentPadding(16.dp) } val elevated = Style { borderWidth(2.dp) borderColor(Color.LightGray) scale(1.02f) } val dark = Style { background(Color(0xFF1E1E2E)) // overrides base's background contentColor(Color.White) } // Chained: val composed = base.then(elevated).then(dark) // Factory (equivalent): val composed = Style(base, elevated, dark) ``` --- ## Building Reusable Components The Styles API maps to Compose's component conventions. The `style` parameter becomes first-class, like `modifier`: ```kotlin // 1. Defaults object — theme-aware default style @OptIn(ExperimentalFoundationStyleApi::class) object StyledChipDefaults { @Composable fun style(): Style { val bg = MaterialTheme.colorScheme.secondaryContainer val fg = MaterialTheme.colorScheme.onSecondaryContainer return Style { background(bg) shape(RoundedCornerShape(8.dp)) contentPadding(horizontal = 16.dp, vertical = 8.dp) contentColor(fg) pressed(Style { animate(Style { scale(0.95f) }) }) } } } // 2. Component — style as parameter with default @Composable fun StyledChip( onClick: () -> Unit, modifier: Modifier = Modifier, style: Style = StyledChipDefaults.style(), content: @Composable () -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } val styleState = remember { MutableStyleState(interactionSource) } Box( modifier = modifier .styleable(styleState = styleState, style = style) .clickable( interactionSource = interactionSource, indication = null, onClick = onClick, ), contentAlignment = Alignment.Center, ) { content() } } // 3. Usage — default, custom, or composed StyledChip(onClick = {}) { Text("Default") } StyledChip( onClick = {}, style = Style { background(Color.Teal) shape(CircleShape) contentColor(Color.Black) }, ) { Text("Custom") } StyledChip( onClick = {}, style = StyledChipDefaults.style().then(Style { borderWidth(2.dp) borderColor(Color.Teal) }), ) { Text("Composed") } ``` --- ## Theme Integration `StyleScope` extends `CompositionLocalAccessorScope`, so Style blocks can read `MaterialTheme` values at resolution time: ```kotlin @Composable fun ThemedButton() { val primary = MaterialTheme.colorScheme.primary val onPrimary = MaterialTheme.colorScheme.onPrimary val surface = MaterialTheme.colorScheme.surface val style = Style { background(primary) contentColor(onPrimary) shape(RoundedCornerShape(12.dp)) contentPadding(16.dp) pressed(Style { animate(Style { background(surface) contentColor(primary) scale(0.95f) }) }) } // When theme changes (dark/light), style re-resolves automatically } ``` Capture theme colors in a `@Composable` scope, use in the Style builder. Theme switches update all styled elements instantly. --- ## Architecture: How It Works The API lives in 7 source files under `androidx.compose.foundation.style`: | File | Purpose | |------|---------| | `Style.kt` | `fun interface Style` + composition operators | | `StyleScope.kt` | ~50 property functions | | `StyleState.kt` | `StyleState` interface + `MutableStyleState` | | `StyleModifier.kt` | `Modifier.styleable()` implementation | | `StyleAnimations.kt` | `animate()` blocks | | `ResolvedStyle.kt` | Property resolution with bitset flagging | | `ExperimentalFoundationStyleApi.kt` | Opt-in annotation | ### Two-Node System `Modifier.styleable()` inserts two modifier nodes: - **`StyleOuterNode`** — Layout (padding, sizing), drawing (background, border, shape), transforms (scale, rotation, translation, alpha). Can invalidate at draw layer only when transform/draw properties change — no recomposition. - **`StyleInnerNode`** — Content padding and text style propagation. Sets `LocalContentColor`, `LocalTextStyle`, etc. so child `Text` and `Icon` composables pick up styled colors/fonts. ### Bitset-Based Property Tracking `ResolvedStyle` uses bitset flags for ~50 properties. On state change: 1. Only the delta between old and new resolved properties is computed 2. Drawing-only changes (background, border, alpha) → **draw-only invalidation** (skips layout + composition) 3. Layout changes (padding, sizing) → layout invalidation 4. Text changes (contentColor, fontSize) → composition invalidation (updates CompositionLocals) A press animation changing only `scale` and `background` never triggers recomposition. --- ## All StyleScope Properties ### Layout - `contentPadding(Dp)`, `contentPadding(horizontal, vertical)`, `contentPadding(start, top, end, bottom)` - `externalPadding(Dp)` and same variants - `width(Dp)`, `height(Dp)`, `size(Dp)`, `size(width, height)` - `minWidth/minHeight/maxWidth/maxHeight(Dp)` - `fillWidth()`, `fillHeight()`, `fillSize()` ### Drawing - `background(Color)`, `background(Brush)` - `foreground(Color)`, `foreground(Brush)` - `shape(Shape)` - `borderWidth(Dp)`, `borderColor(Color)`, `borderBrush(Brush)` - `border(width, color)`, `border(width, brush)` ### Transforms - `scale(Float)`, `scaleX(Float)`, `scaleY(Float)` - `rotationX(Float)`, `rotationY(Float)`, `rotationZ(Float)` - `translationX(Float)`, `translationY(Float)`, `translation(x, y)` - `alpha(Float)`, `clip(Boolean)`, `zIndex(Float)` - `transformOrigin(TransformOrigin)` ### Text & Content - `contentColor(Color)`, `contentBrush(Brush)` - `fontSize(TextUnit)`, `fontWeight(FontWeight)`, `fontStyle(FontStyle)` - `letterSpacing(TextUnit)`, `lineHeight(TextUnit)` - `textDecoration(TextDecoration)`, `fontFamily(FontFamily)` - `textAlign(TextAlign)`, `textDirection(TextDirection)` - `textStyle(TextStyle)`, `textIndent(TextIndent)` - `baselineShift(BaselineShift)`, `lineBreak(LineBreak)` - `hyphens(Hyphens)`, `fontSynthesis(FontSynthesis)` ### Shadows (internal constructor in alpha06) - `dropShadow(Shadow)`, `innerShadow(Shadow)` ### State Functions - `pressed(Style)`, `hovered(Style)`, `focused(Style)` - `selected(Style)`, `checked(Style)`, `disabled(Style)` ### Animation - `animate(Style)` — default spring - `animate(spec: AnimationSpec, Style)` — custom spec - `animate(toSpec, fromSpec, Style)` — asymmetric enter/exit ### Composition - `Style.then(other: Style)` — chain (later overrides) - `Style(style1, style2)` — merge factory - `Style(vararg styles)` — merge multiple --- ## Common Pitfalls 1. **Forgetting `styleState`** — the #1 bug. Style renders but never reacts to state. 2. **Not sharing `interactionSource`** — pressed/hovered/focused won't track without it. 3. **Multiple Text children in styled box** — all inherit fontSize/fontWeight/contentColor. 4. **Using `toggleable()` / `selectable()`** — they create their own interactionSource internally. Use `.clickable()` and set state explicitly on `MutableStyleState`. 5. **Missing `@OptIn(ExperimentalFoundationStyleApi::class)`** — required on all usages. 6. **Trying to use `dropShadow()`** — `Shadow` constructor is internal in alpha06, won't compile. 7. **No `indication = null` on clickable** — without it you get default ripple on top of your styled feedback. ================================================ FILE: .claude/skills/compose-expert/references/theming-material3.md ================================================ # Material 3 Theming Reference ## MaterialTheme Basics `MaterialTheme` is the root provider for design tokens in Compose Material 3. It establishes `colorScheme`, `typography`, and `shapes` across your app. ```kotlin @Composable fun MyApp() { MaterialTheme( colorScheme = lightColorScheme(), typography = Typography(), shapes = Shapes() ) { // All descendants access tokens via MaterialTheme Scaffold { Text("Uses MaterialTheme.typography.bodyLarge") } } } ``` **Source**: `androidx/compose/material3/MaterialTheme.kt` --- ## ColorScheme — Light and Dark A `ColorScheme` bundles 29+ semantic color tokens (primary, secondary, error, surface, etc.). ### Default Light/Dark Schemes ```kotlin // Light (default) val lightColors = lightColorScheme( primary = Color(0xFF6200EE), secondary = Color(0xFF03DAC6) ) // Dark val darkColors = darkColorScheme( primary = Color(0xFFBB86FC), secondary = Color(0xFF03DAC6) ) MaterialTheme(colorScheme = if (isDark) darkColors else lightColors) { ... } ``` ### Dynamic Color (Material You) Android 12+ supports extracting colors from wallpaper. Check `Build.VERSION.SDK_INT` before calling: ```kotlin val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val context = LocalContext.current if (isDark) { dynamicDarkColorScheme(context) } else { dynamicLightColorScheme(context) } } else { if (isDark) darkColorScheme() else lightColorScheme() } MaterialTheme(colorScheme = colorScheme) { ... } ``` This requires `android:READ_MEDIA_IMAGES` or context access. `dynamicColorScheme` APIs are in `androidx.compose.material3`. --- ## Typography — Custom Type Scales `Typography` defines `displayLarge`, `headlineSmall`, `bodyLarge`, `labelSmall`, etc. ### Using Google Fonts ```kotlin val Poppins = FontFamily( Font(R.font.poppins_regular, weight = FontWeight.Normal), Font(R.font.poppins_bold, weight = FontWeight.Bold), Font(R.font.poppins_italic, weight = FontWeight.Normal, style = FontStyle.Italic) ) val customTypography = Typography( displayLarge = TextStyle( fontFamily = Poppins, fontSize = 57.sp, lineHeight = 64.sp, letterSpacing = (-0.25).sp ), bodyMedium = TextStyle( fontFamily = Poppins, fontSize = 14.sp, lineHeight = 20.sp ) ) MaterialTheme(typography = customTypography) { ... } ``` All M3 type styles follow a 15-level scale. Partial overrides keep defaults for unspecified styles. --- ## Shapes — Corner Radius Customization `Shapes` defines `extraSmall`, `small`, `medium`, `large`, `extraLarge` corner radii. ```kotlin val customShapes = Shapes( extraSmall = RoundedCornerShape(4.dp), small = RoundedCornerShape(8.dp), medium = RoundedCornerShape(12.dp), large = RoundedCornerShape(16.dp), extraLarge = RoundedCornerShape(28.dp) ) MaterialTheme(shapes = customShapes) { ... } // Use in components Button( modifier = Modifier.clip(MaterialTheme.shapes.large) ) { ... } ``` Components automatically use theme shapes via `Surface` and `Card`. --- ## Dark Theme ### isSystemInDarkTheme() Check system dark mode setting: ```kotlin @Composable fun MyApp() { val isDark = isSystemInDarkTheme() MaterialTheme(colorScheme = if (isDark) darkColorScheme() else lightColorScheme()) { // Content } } ``` ### Manual Toggle with Persistence For user-selectable dark mode: ```kotlin val darkModeState = rememberSaveable { mutableStateOf(isSystemInDarkTheme()) } MaterialTheme(colorScheme = if (darkModeState.value) darkColorScheme() else lightColorScheme()) { Scaffold( floatingActionButton = { FloatingActionButton(onClick = { darkModeState.value = !darkModeState.value }) { Icon(Icons.Default.Settings, "Toggle theme") } } ) { // Content } } ``` Persist selection via DataStore or SharedPreferences. --- ## Component-Level Styling Use `MaterialTheme` tokens for colors, not hardcoded values: ```kotlin // DO Text( text = "Hello", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge ) // DON'T Text(text = "Hello", color = Color.Black, fontSize = 14.sp) ``` Common tokens: - `primary`, `secondary`, `tertiary` — accent colors - `surface`, `surfaceVariant` — container backgrounds - `onPrimary`, `onSurface`, `onError` — text/content on colored backgrounds - `error`, `errorContainer` — error states --- ## Surface vs Box **Surface**: styled container with elevation, background from `colorScheme.surface`, respects theme. ```kotlin Surface( color = MaterialTheme.colorScheme.surface, modifier = Modifier.fillMaxWidth() ) { // Has elevation, shadow Text("Themed container") } ``` **Box**: plain container, no theming assumptions. ```kotlin Box( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) ) { // Manual styling Text("Manual background") } ``` Use `Surface` for semantic containers (cards, dialogs). Use `Box` for layout grouping. --- ## Scaffold — Layout Integration `Scaffold` composes `topBar`, `floatingActionButton`, `snackbarHost`, and content with proper padding: ```kotlin Scaffold( topBar = { TopAppBar( title = { Text("My App") }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.primaryContainer ) ) }, floatingActionButton = { FloatingActionButton( onClick = { /* ... */ }, containerColor = MaterialTheme.colorScheme.tertiary ) { Icon(Icons.Default.Add, "Add item") } }, snackbarHost = { SnackbarHost(it) } ) { innerPadding -> LazyColumn(modifier = Modifier.padding(innerPadding)) { // Content respects safe area } } ``` `Scaffold` handles insets and spacing automatically. Don't nest `Scaffold` components. --- ## Extending Theme with CompositionLocal Add custom design tokens: ```kotlin data class AppColors( val brandPrimary: Color = Color(0xFF6200EE), val brandSecondary: Color = Color(0xFF03DAC6), val neutral: Color = Color.Gray ) val LocalAppColors = compositionLocalOf { AppColors() } @Composable fun AppTheme(content: @Composable () -> Unit) { val appColors = AppColors() CompositionLocalProvider(LocalAppColors provides appColors) { MaterialTheme { content() } } } // Use custom tokens @Composable fun MyButton() { Button( colors = ButtonDefaults.buttonColors( containerColor = LocalAppColors.current.brandPrimary ) ) { } } ``` --- ## Anti-Patterns ### Hardcoding Colors ```kotlin // DON'T Text("Hello", color = Color(0xFF000000)) // DO Text("Hello", color = MaterialTheme.colorScheme.onSurface) ``` ### Ignoring Theme in Reusable Components ```kotlin // DON'T fun MyCard(content: @Composable () -> Unit) { Box(modifier = Modifier.background(Color.White)) } // DO fun MyCard(content: @Composable () -> Unit) { Surface( color = MaterialTheme.colorScheme.surface, shape = MaterialTheme.shapes.medium ) { content() } } ``` ### Mixing Material 2 and Material 3 Don't import both `androidx.compose.material` and `androidx.compose.material3`. Choose M3 for new projects. ### Not Providing All Theme Parameters Partial `MaterialTheme` calls may leave descendants with defaults: ```kotlin // Unsafe if colorScheme varies by locale MaterialTheme(typography = customTypography) { ... } // Better MaterialTheme( colorScheme = currentColorScheme, typography = currentTypography, shapes = currentShapes ) { ... } ``` --- ## Resources - **Material 3 Tokens**: https://m3.material.io/ - **Compose Material3 Docs**: https://developer.android.com/develop/ui/compose/designsystems/material3 - **Dynamic Color**: Requires `androidx.compose.material3:material3` >= 1.1.0 and Android 12+ ================================================ FILE: .claude/skills/compose-expert/references/tv-compose.md ================================================ # Compose for TV (Android TV / Google TV) Reference for building Android TV apps using Jetpack Compose with `androidx.tv:tv-material` and `androidx.tv:tv-foundation`. This is the modern replacement for Leanback — do **not** use Leanback for new TV projects. Source: `tv/tv-material/`, `tv/tv-foundation/` in `androidx/androidx` (branch: `androidx-main`) --- ## 1. Setup & Dependencies ### Gradle ```kotlin dependencies { val composeBom = platform("androidx.compose:compose-bom:2026.03.00") implementation(composeBom) // General Compose implementation("androidx.activity:activity-compose:1.13.0") implementation("androidx.compose.ui:ui-tooling-preview") debugImplementation("androidx.compose.ui:ui-tooling") // Compose for TV — use INSTEAD of androidx.compose.material3:material3 implementation("androidx.tv:tv-material:1.1.0-rc01") // TV Foundation — only needed if using BringIntoViewSpec customization // Standard LazyRow/LazyColumn from compose.foundation work out-of-the-box since 1.7.0 implementation("androidx.tv:tv-foundation:1.0.0-rc01") // Optional: TVProvider for home screen channels implementation("androidx.tvprovider:tvprovider:1.1.0") } ``` ### Compatibility | Requirement | Value | |-------------|-------| | Min API level | 21 (Android 5.0) — library minimum; practical Google TV / Android TV device minimum is API 23–28 in production | | Compose BOM | 2026.03.00+ | | Kotlin | 2.0+ (KGP 2.0.0+ required for consumption) | | tv-material stable | 1.0.0 (first stable August 2024) | | tv-material latest | 1.1.0-rc01 | | tv-foundation latest | 1.0.0-rc01 | ### AndroidManifest.xml ```xml ``` --- ## 2. TV Material3 vs Mobile Material3 **Use `androidx.tv.material3` instead of `androidx.compose.material3` for TV apps.** | TV (`androidx.tv.*`) | Mobile (`androidx.compose.*`) | Notes | |----------------------|------------------------------|-------| | `androidx.tv:tv-material` | `androidx.compose.material3:material3` | TV variant has focus-aware indications | | `androidx.tv.material3.Surface` | `androidx.compose.material3.Surface` | TV Surface supports Border, Glow, Scale per state | | `androidx.tv.material3.Button` | `androidx.compose.material3.Button` | TV Button has focus scale/glow/border | | `androidx.tv.material3.Card` | `androidx.compose.material3.Card` | TV Card has 5+ variants for media | | `androidx.tv.material3.MaterialTheme` | `androidx.compose.material3.MaterialTheme` | Separate theming — don't mix | > **Never mix** mobile `MaterialTheme` with TV `MaterialTheme`. Each library defines its own > `MaterialTheme` object — using both causes inconsistent colors, typography, and shapes. --- ## 3. Component Catalog ### Surfaces (Building Blocks) `Surface` is the foundational TV composable — all interactive components build on it. TV Surface supports per-state customization of four visual indications: | Indication | Type | Purpose | |------------|------|---------| | **Scale** | `Float` | Enlarges element on focus (default: 1.0 → 1.1x) | | **Border** | `Border` | Draws a border around the element on focus | | **Glow** | `Glow` | Adds a diffused shadow/glow (API 28+ only) | | **Shape** | `Shape` | Changes shape on focus/press | Three Surface variants: ```kotlin // 1. Non-interactive (display only) Surface( modifier = Modifier.size(200.dp), colors = SurfaceDefaults.colors( containerColor = MaterialTheme.colorScheme.surface ), shape = RoundedCornerShape(8.dp) ) { Text("Static content") } // 2. Clickable (buttons, cards, etc.) Surface( onClick = { /* handle click */ }, modifier = Modifier.size(200.dp), scale = ClickableSurfaceDefaults.scale( focusedScale = 1.05f ), border = ClickableSurfaceDefaults.border( focusedBorder = Border( border = BorderStroke(2.dp, Color.White), shape = RoundedCornerShape(8.dp) ) ), glow = ClickableSurfaceDefaults.glow( focusedGlow = Glow( elevationColor = Color.White.copy(alpha = 0.5f), elevation = 8.dp ) ) ) { Text("Clickable") } // 3. Selectable (toggles, radio-like selection) var selected by remember { mutableStateOf(false) } Surface( selected = selected, onClick = { selected = !selected }, modifier = Modifier.size(200.dp), scale = SelectableSurfaceDefaults.scale( focusedScale = 1.05f, selectedScale = 1.02f ) ) { Text(if (selected) "Selected" else "Unselected") } ``` ### Cards TV provides five card variants for media content: | Component | Layout | Use Case | |-----------|--------|----------| | `Card` | Basic container with click | Simple content card | | `ClassicCard` | Image + title + subtitle + description | Standard media card | | `CompactCard` | Image with overlay text | Space-efficient card | | `WideClassicCard` | Horizontal image + text | Landscape media card | | `StandardCardContainer` | Image above + content below | Catalog grid items | | `WideCardContainer` | Image left + content right | List detail items | ```kotlin // Basic Card Card( onClick = { /* navigate to detail */ }, modifier = Modifier.size(width = 180.dp, height = 100.dp) ) { AsyncImage( model = movie.thumbnailUrl, contentDescription = movie.title, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } // ClassicCard — image + metadata slots ClassicCard( onClick = { /* navigate */ }, image = { AsyncImage( model = movie.posterUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.size(width = 150.dp, height = 200.dp) ) }, title = { Text(movie.title) }, subtitle = { Text(movie.year.toString()) }, description = { Text(movie.description, maxLines = 2) } ) // StandardCardContainer — layout wrapper (image top, content below) // Note: as of tv-material 1.0.0 stable, interactionSource is managed internally by the card. // The imageCard slot is a plain @Composable lambda with no interactionSource parameter. StandardCardContainer( imageCard = { Card( onClick = { /* navigate */ } ) { AsyncImage( model = movie.thumbnailUrl, contentDescription = movie.title, contentScale = ContentScale.Crop, modifier = Modifier.size(width = 180.dp, height = 100.dp) ) } }, title = { Text(movie.title, maxLines = 1) } ) ``` ### Carousel Displays featured content in an auto-scrolling rotator with D-pad navigation. ```kotlin @Composable fun FeaturedCarousel( featuredContent: List, modifier: Modifier = Modifier, ) { Carousel( itemCount = featuredContent.size, modifier = modifier .fillMaxWidth() .height(376.dp), // Optional: remember state to control or observe active item // carouselState = rememberCarouselState(), // autoScrollDurationMillis = CarouselDefaults.TimeToDisplayItemMillis (5000ms) ) { index -> // this: AnimatedContentScope — use Modifier.animateEnterExit() inside here val movie = featuredContent[index] Box(modifier = Modifier.fillMaxSize()) { AsyncImage( model = movie.backgroundImageUrl, contentDescription = movie.description, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) Column( modifier = Modifier .align(Alignment.BottomStart) .padding(32.dp) ) { Text( text = movie.title, style = MaterialTheme.typography.headlineLarge ) Text( text = movie.description, style = MaterialTheme.typography.bodyMedium, maxLines = 2 ) // Animate foreground content entry with AnimatedContentScope Button( onClick = { /* play */ }, modifier = Modifier .animateEnterExit( enter = fadeIn() + slideInHorizontally(), exit = fadeOut() + slideOutHorizontally() ) ) { Text("Watch Now") } } } } } ``` Key points: - `CarouselState` controls which item is shown; use `rememberCarouselState()` to persist - Auto-scroll pauses automatically when the user interacts via D-pad — there is no public API to pause/resume it programmatically (`ScrollPauseHandle` is internal) - Content inside the carousel lambda receives an `AnimatedContentScope` — use `Modifier.animateEnterExit()` for slide/fade effects - Custom slide transitions via `contentTransformStartToEnd` and `contentTransformEndToStart` parameters ### Buttons ```kotlin // Standard filled button Button(onClick = { /* action */ }) { Text("Play") } // Outlined button OutlinedButton(onClick = { /* action */ }) { Text("Add to List") } // Icon button IconButton(onClick = { /* action */ }) { Icon(Icons.Default.Search, contentDescription = "Search") } // Wide button (full-width with icon + text + subtitle) // Note: WideButton is available in tv-material 1.1.0-rc01+. Verify availability before use. WideButton( onClick = { /* action */ }, title = { Text("Continue Watching") }, subtitle = { Text("Episode 3 — 45 min remaining") }, icon = { Icon(Icons.Default.PlayArrow, null) } ) ``` ### Navigation: Drawer & Tabs Two patterns for top-level navigation: **Side navigation with `NavigationDrawer`:** ```kotlin @Composable fun TvApp() { val drawerState = rememberDrawerState(DrawerValue.Closed) val selectedItem = remember { mutableIntStateOf(0) } val items = listOf("Home", "Movies", "Shows", "Settings") NavigationDrawer( drawerState = drawerState, drawerContent = { items.forEachIndexed { index, label -> NavigationDrawerItem( selected = selectedItem.intValue == index, onClick = { selectedItem.intValue = index }, leadingContent = { Icon( imageVector = when (index) { 0 -> Icons.Default.Home 1 -> Icons.Default.Movie 2 -> Icons.Default.Tv else -> Icons.Default.Settings }, contentDescription = null ) } ) { Text(label) } } } ) { // Main content area when (selectedItem.intValue) { 0 -> HomeScreen() 1 -> MoviesScreen() 2 -> ShowsScreen() 3 -> SettingsScreen() } } } ``` - `NavigationDrawer` — always visible at screen edge, collapses when focus moves to content - `ModalNavigationDrawer` — overlays content with a scrim, use for secondary nav - `NavigationDrawerScope.doesNavigationDrawerHaveFocus` — check if drawer has focus to expand/collapse items **Top navigation with `TabRow`:** ```kotlin @Composable fun TopNavigation() { var selectedTab by remember { mutableIntStateOf(0) } val tabs = listOf("Home", "Movies", "Shows") Column { TabRow(selectedTabIndex = selectedTab) { tabs.forEachIndexed { index, title -> Tab( selected = selectedTab == index, onFocus = { selectedTab = index }, // TV tabs activate on focus ) { Text( text = title, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) } } } // Content below tabs when (selectedTab) { 0 -> HomeScreen() 1 -> MoviesScreen() 2 -> ShowsScreen() } } } ``` > **TV-specific behavior**: Tabs typically activate on focus (not click). Use the `onFocus` > callback to switch content as the user D-pads through tabs. ### Chips ```kotlin // Assist chip AssistChip(onClick = { /* action */ }) { Text("Genre: Action") } // Filter chip (toggleable) var selected by remember { mutableStateOf(false) } FilterChip( selected = selected, onClick = { selected = !selected } ) { Text("4K") } // Input chip (removable) InputChip( selected = true, onClick = { /* remove filter */ } ) { Text("English") } // Suggestion chip SuggestionChip(onClick = { /* apply */ }) { Text("Recommended") } ``` ### ListItem & DenseListItem ```kotlin // Standard list item ListItem( selected = false, onClick = { /* navigate to setting */ }, headlineContent = { Text("Audio Language") }, supportingContent = { Text("English") }, leadingContent = { Icon(Icons.Default.Language, contentDescription = null) }, trailingContent = { Icon(Icons.Default.ChevronRight, contentDescription = null) } ) // Dense list item — reduced height DenseListItem( selected = false, onClick = { /* action */ }, headlineContent = { Text("Subtitle Size") } ) ``` ### Form Controls ```kotlin // Checkbox var checked by remember { mutableStateOf(false) } Checkbox(checked = checked, onCheckedChange = { checked = it }) // RadioButton RadioButton(selected = true, onClick = { /* select */ }) // Switch var enabled by remember { mutableStateOf(true) } Switch(checked = enabled, onCheckedChange = { enabled = it }) ``` --- ## 4. Focus & D-pad Navigation TV UIs are focus-driven — there is no touch. Every interactive element must be focusable and navigable via the D-pad (up/down/left/right + select + back). ### Focus Indications TV Material provides four built-in focus indications — configured per component via `Defaults.*` objects: | Indication | API | When to Use | |------------|-----|-------------| | Scale | `scale(focusedScale = 1.05f)` | Most components — clear visual feedback | | Border | `border(focusedBorder = Border(...))` | Buttons, list items — precise outline | | Glow | `glow(focusedGlow = Glow(...))` | Cards, media tiles — elevated look | | Color | `colors(focusedContainerColor = ...)` | Chips, tabs — background color change | ### Focus Management with Modifiers ```kotlin // Request initial focus val focusRequester = remember { FocusRequester() } LazyRow { items(movies) { movie -> MovieCard( modifier = if (movie == movies.first()) Modifier.focusRequester(focusRequester) else Modifier ) } } LaunchedEffect(Unit) { focusRequester.requestFocus() } // Customize D-pad directional focus traversal // Use up/down/left/right — NOT enter/exit (those control FocusGroup enter/exit, not D-pad directions) Modifier.focusProperties { right = customFocusRequester // override where right D-pad goes left = FocusRequester.Cancel // block left navigation // up / down also available } // Focus restoration — remember which child had focus Modifier.focusRestorer() ``` ### D-pad Input Handling ```kotlin Modifier.onKeyEvent { keyEvent -> when { keyEvent.key == Key.DirectionCenter && keyEvent.type == KeyEventType.KeyUp -> { // Select/enter pressed true } keyEvent.key == Key.Back && keyEvent.type == KeyEventType.KeyUp -> { // Back button pressed true } else -> false } } // Long-press support is built into TV Surface components Surface( onClick = { /* regular click */ }, onLongClick = { /* long-press menu */ } ) { /* content */ } ``` ### Back Button ```kotlin // Use BackHandler from activity-compose BackHandler(enabled = isDetailVisible) { // Return to previous screen isDetailVisible = false } ``` > **Pattern**: Back button should always navigate to the previous destination. Pressing back > from the start destination should exit the app. Never gate exit with a confirmation dialog. --- ## 5. Lists & Scrolling ### Standard Lazy Layouts (Recommended) Since Compose Foundation 1.7.0, standard `LazyRow`/`LazyColumn`/`LazyGrid` support focus-driven scrolling out of the box. **The deprecated `TvLazyRow`/`TvLazyColumn` from `tv-foundation` should no longer be used.** ```kotlin @Composable fun CatalogBrowser( featuredContent: List, sections: List
, modifier: Modifier = Modifier, onMovieSelected: (Movie) -> Unit = {}, ) { LazyColumn( modifier = modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(16.dp) ) { // Featured carousel at top item { FeaturedCarousel(featuredContent) } // Content sections items(sections) { section -> Text( text = section.title, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.padding(start = 16.dp, bottom = 8.dp) ) LazyRow( horizontalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(horizontal = 16.dp) ) { items( items = section.movies, key = { it.id } ) { movie -> MovieCard( movie = movie, onClick = { onMovieSelected(movie) } ) } } } } } ``` ### Custom Scroll Positioning with BringIntoViewSpec Override default scroll behavior to keep focused items at a consistent position (e.g., 30% from the left edge instead of just scrolling to make the item visible): ```kotlin // Both the composable function AND the CompositionLocalProvider call inside require // @OptIn(ExperimentalFoundationApi::class) — annotate the whole function. @OptIn(ExperimentalFoundationApi::class) @Composable fun PositionFocusedItemInLazyLayout( parentFraction: Float = 0.3f, childFraction: Float = 0f, content: @Composable () -> Unit, ) { val bringIntoViewSpec = remember(parentFraction, childFraction) { object : BringIntoViewSpec { override fun calculateScrollDistance( offset: Float, size: Float, containerSize: Float ): Float { val targetForLeadingEdge = parentFraction * containerSize - (childFraction * size) val adjustedTarget = if (size <= containerSize && (containerSize - targetForLeadingEdge) < size) { containerSize - size } else { targetForLeadingEdge } return offset - adjustedTarget } } } CompositionLocalProvider( LocalBringIntoViewSpec provides bringIntoViewSpec, content = content, ) } // Usage: PositionFocusedItemInLazyLayout(parentFraction = 0.3f) { LazyColumn { items(sections) { section -> LazyRow { /* items */ } } } } ``` ### Migration from TV Foundation Lazy Layouts | Deprecated (`tv-foundation`) | Replacement (`compose.foundation`) | |-------------------------------|-------------------------------------| | `TvLazyRow` | `LazyRow` | | `TvLazyColumn` | `LazyColumn` | | `TvLazyHorizontalGrid` | `LazyHorizontalGrid` | | `TvLazyVerticalGrid` | `LazyVerticalGrid` | | `pivotOffsets` | `BringIntoViewSpec` via `LocalBringIntoViewSpec` | Requires `compose.foundation` 1.7.0+. Update imports and remove the `Tv` prefix. --- ## 6. Theming TV `MaterialTheme` mirrors the mobile Material3 theming system but is defined in `androidx.tv.material3`: ```kotlin @Composable fun TvAppTheme(content: @Composable () -> Unit) { // TV apps typically use dark themes for the living room // Import darkColorScheme from androidx.tv.material3 — NOT androidx.compose.material3 val colorScheme = darkColorScheme( primary = Color(0xFFBB86FC), onPrimary = Color.Black, secondary = Color(0xFF03DAC5), surface = Color(0xFF121212), onSurface = Color.White, background = Color(0xFF000000), onBackground = Color.White, ) val typography = Typography( // TV typography should be larger for 10-foot viewing headlineLarge = TextStyle( fontWeight = FontWeight.Bold, fontSize = 32.sp, lineHeight = 40.sp ), bodyLarge = TextStyle( fontSize = 18.sp, lineHeight = 24.sp ), ) MaterialTheme( colorScheme = colorScheme, typography = typography, shapes = Shapes(), // default TV shapes content = content ) } ``` Key differences from mobile theming: - Import `MaterialTheme` **and** `darkColorScheme`/`lightColorScheme` from `androidx.tv.material3`, not `androidx.compose.material3` - TV apps almost always use `darkColorScheme()` — living rooms are dark environments - Typography sizes should be larger for 10-foot viewing distance (16sp minimum body text) - `Shapes` from `androidx.tv.material3` — same concept, TV-specific defaults ### CompositionLocals | Local | Purpose | |-------|---------| | `LocalContentColor` | Content color for text/icons at current position | | `LocalTextStyle` | Default text style (set via `ProvideTextStyle`) | --- ## 7. Building an Immersive List `ImmersiveList` was removed in tv-material 1.0.0-beta01. Build it manually by changing the background based on the focused item: ```kotlin @Composable fun ImmersiveMovieRow( movies: List, modifier: Modifier = Modifier, ) { var focusedMovie by remember { mutableStateOf(movies.firstOrNull()) } Box(modifier = modifier.fillMaxWidth().height(400.dp)) { // Background — changes based on focused item AnimatedContent( targetState = focusedMovie, transitionSpec = { fadeIn(tween(500)) togetherWith fadeOut(tween(500)) }, label = "immersive-background" ) { movie -> movie?.let { AsyncImage( model = it.backgroundImageUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } } // Scrim gradient Box( modifier = Modifier .fillMaxSize() .background( Brush.verticalGradient( colors = listOf(Color.Transparent, Color.Black.copy(alpha = 0.8f)) ) ) ) // Foreground row Column( modifier = Modifier.align(Alignment.BottomStart).padding(32.dp) ) { focusedMovie?.let { movie -> Text(movie.title, style = MaterialTheme.typography.headlineLarge) Text(movie.description, maxLines = 2, style = MaterialTheme.typography.bodyMedium) } Spacer(Modifier.height(16.dp)) LazyRow(horizontalArrangement = Arrangement.spacedBy(12.dp)) { items(movies, key = { it.id }) { movie -> // Wrap Card in a Box — onFocusChanged on an interactive TV Surface // may not fire reliably. Apply it to a non-interactive wrapper instead. Box( modifier = Modifier .size(width = 160.dp, height = 90.dp) .onFocusChanged { state -> if (state.hasFocus) focusedMovie = movie } ) { Card( onClick = { /* navigate to detail */ }, modifier = Modifier.fillMaxSize() ) { AsyncImage( model = movie.thumbnailUrl, contentDescription = movie.title, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) } } } } } } ``` --- ## 8. Details Screen Pattern ```kotlin @Composable fun DetailsScreen( movie: Movie, modifier: Modifier = Modifier, onStartPlayback: (Movie) -> Unit = {}, ) { Box(modifier = modifier.fillMaxSize()) { AsyncImage( model = movie.backgroundImageUrl, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize() ) // Scrim for readability Box( modifier = Modifier .fillMaxSize() .background( Brush.horizontalGradient( colors = listOf(Color.Black.copy(alpha = 0.9f), Color.Transparent) ) ) ) Column( modifier = Modifier .padding(48.dp) .fillMaxHeight(), verticalArrangement = Arrangement.Center ) { Text(movie.title, style = MaterialTheme.typography.displaySmall) Spacer(Modifier.height(8.dp)) Text(movie.description, style = MaterialTheme.typography.bodyLarge, maxLines = 4) Spacer(Modifier.height(24.dp)) Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Button(onClick = { onStartPlayback(movie) }) { Text("Play") } OutlinedButton(onClick = { /* add to list */ }) { Text("My List") } } } } } ``` --- ## 9. TVProvider Integration `androidx.tvprovider` lets your app publish channels and programs to the Android TV home screen. This is not Compose UI — it's a content provider API used alongside your Compose app. ```kotlin // IMPORTANT: publishChannel / publishPreviewProgram perform I/O — call from a background // coroutine (Dispatchers.IO) or a WorkManager task, never from the main thread. // Example using a coroutine: viewModelScope.launch(Dispatchers.IO) { val channel = PreviewChannel.Builder() .setDisplayName("Continue Watching") .setAppLinkIntentUri(Uri.parse("myapp://home")) .build() val channelId = PreviewChannelHelper(context).publishChannel(channel) val program = PreviewProgram.Builder() .setChannelId(channelId) .setTitle("Movie Title") .setDescription("Episode 3") .setPosterArtUri(Uri.parse("https://example.com/poster.jpg")) .setIntentUri(Uri.parse("myapp://movie/123")) .setType(TvContractCompat.PreviewPrograms.TYPE_MOVIE) .build() PreviewChannelHelper(context).publishPreviewProgram(program) } ``` --- ## 10. Anti-Patterns & Gotchas ### Don't mix mobile and TV Material ```kotlin // WRONG — mixing material3 themes import androidx.compose.material3.MaterialTheme as MobileMaterialTheme import androidx.tv.material3.MaterialTheme as TvMaterialTheme // This WILL cause inconsistent colors, typography, and shapes. // Pick one: use tv.material3 for TV apps. ``` ### Don't use Leanback with Compose Leanback (`androidx.leanback`) is the legacy View-based TV framework. Do not mix it with Compose for TV. Use `androidx.tv:tv-material` exclusively for new projects. ### Glow disabled below API 28 Glow indication (`Glow(...)`) is silently disabled on devices running Android 8.1 (API 27) and below. Use Border + Scale as primary indications for broad compatibility. ### Focus traps Ensure every screen has a clear focus path. Common traps: - Empty `LazyRow`/`LazyColumn` with no items — focus search crashes or skips over them - Overlapping focusable elements — D-pad direction becomes unpredictable - Missing `focusRestorer()` — returning to a list loses the user's position ### TV lazy layouts are deprecated `TvLazyRow`, `TvLazyColumn`, `TvLazyVerticalGrid`, `TvLazyHorizontalGrid` from `tv-foundation` are deprecated since alpha11. Migrate to standard Compose Foundation lazy layouts (requires Foundation 1.7.0+). ### Don't show virtual back button TV uses the hardware back button on the remote. Never render a back button in your UI — it wastes screen real estate and confuses the navigation pattern. ### Typography too small Mobile-sized text (14sp body) is unreadable at TV viewing distances (10 feet). Use 16sp minimum for body text, 24sp+ for headings. ### Missing content descriptions TV apps must be accessible via TalkBack. Every interactive element needs a `contentDescription` — especially image-only cards and icon buttons. ### ExoPlayer rendering on Surface On lower API versions, `CompositingStrategy.Offscreen` on Surface composables can prevent ExoPlayer from rendering video. This was fixed in tv-material 1.0.1 by moving the compositing strategy from Surface to Text. --- ## 11. Sample Apps & Resources | Resource | URL | |----------|-----| | JetStream (full app sample) | `github.com/android/tv-samples/tree/main/JetStreamCompose` | | TV Material Catalog | `github.com/android/tv-samples/tree/main/TvMaterialCatalog` | | Compose for TV codelab | `developer.android.com/codelabs/compose-for-tv-introduction` | | TV design guides | `developer.android.com/design/ui/tv/guides/components` | | API reference | `developer.android.com/reference/kotlin/androidx/tv/material3/package-summary` | | ImmersiveList sample (manual) | `cs.android.com/androidx/.../tv/samples/.../ImmersiveListSamples.kt` | | TV samples repo | `github.com/android/tv-samples` | ================================================ FILE: .claude/skills/compose-expert/references/view-composition.md ================================================ # Jetpack Compose View Composition Reference ## Composable Function Naming Conventions Names communicate intent. Follow these patterns consistently. ### Nouns (UI Components) - PascalCase, describe *what* the composable displays - Used for UI widgets, screens, layout building blocks ```kotlin @Composable fun Button(...) // Displays a button @Composable fun UserCard(user: User) // Displays a user card @Composable fun LoginScreen() // Displays a login screen @Composable fun CheckboxWithLabel(...) // Displays a checkbox with label ``` ### Verbs (Side Effects / Effects) - PascalCase, describe *what action* happens - Used for composables that don't emit UI, only perform side effects ```kotlin @Composable fun LaunchedEffect(...) // Launches a coroutine @Composable fun DisposableEffect(...) // Manages resource lifecycle @Composable fun SideEffect(...) // Performs a side effect ``` ### Anti-Pattern: Ambiguous Names ```kotlin // ❌ Unclear if this is a UI component or effect @Composable fun HandleLogin(...) // ✅ Explicit @Composable fun LoginScreen(...) // Displays UI @Composable fun PerformLogin(...) // Side effect function (if truly a side effect) ``` ## The Slot Pattern Accept `@Composable` lambda parameters to create flexible, reusable containers. ### Basic Slot Pattern ```kotlin @Composable fun Card( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Box( modifier = modifier .background(Color.White) .padding(16.dp) ) { content() } } // Usage Card { Text("Hello") Image(...) } ``` ### Multiple Slots ```kotlin @Composable fun ListItem( modifier: Modifier = Modifier, icon: @Composable () -> Unit, title: @Composable () -> Unit, subtitle: @Composable (() -> Unit)? = null, trailing: @Composable (() -> Unit)? = null ) { Row(modifier = modifier.padding(16.dp)) { icon() Column(modifier = Modifier.weight(1f)) { title() subtitle?.invoke() } trailing?.invoke() } } // Usage ListItem( icon = { Icon(Icons.Default.Person, "") }, title = { Text("Alice") }, subtitle = { Text("Online") }, trailing = { Badge() } ) ``` **Key principle:** Slots accept `@Composable` lambdas, not pre-composed values. This ensures composition is deferred and scope-aware. ```kotlin // ❌ Wrong: passes composed value fun CustomLayout(content: String) { ... } // ✅ Correct: passes composition lambda fun CustomLayout(content: @Composable () -> Unit) { ... } ``` Source: Material 3 composables in `androidx.compose.material3` use slots extensively. ## Extracting Composables Know when to extract and when to keep composables together. ### Extract When - **Reused in multiple places:** DRY principle - **Single responsibility:** Composable handles one concern - **Easier to test:** Small, focused unit - **Performance:** Enables skipping and independent recomposition ```kotlin // ❌ Before: god composable @Composable fun UserProfile(user: User) { Column { // Header Box(modifier = Modifier.fillMaxWidth()) { Image(user.photo) Text(user.name, style = MaterialTheme.typography.headlineSmall) IconButton({ /* edit */ }) { Icon(Icons.Default.Edit, "") } } // Stats Row(modifier = Modifier.fillMaxWidth()) { StatItem(user.followers, "Followers") StatItem(user.following, "Following") StatItem(user.posts, "Posts") } // Bio Text(user.bio, style = MaterialTheme.typography.bodyMedium) } } // ✅ After: extracted composables @Composable fun UserProfile(user: User) { Column { UserProfileHeader(user) UserStats(user) UserBio(user.bio) } } @Composable private fun UserProfileHeader(user: User) { ... } @Composable private fun UserStats(user: User) { ... } @Composable private fun UserBio(bio: String) { ... } ``` ### Don't Extract When - **Single use:** Extraction adds indirection without benefit - **Tightly coupled logic:** Would require passing many parameters - **Too small:** Single `Text()` or `Icon()` doesn't need extraction ```kotlin // ❌ Over-extraction: trivial wrapper @Composable private fun UserName(name: String) { Text(name, style = MaterialTheme.typography.headlineSmall) } // ✅ Keep inline if only used once @Composable fun UserProfile(user: User) { Text(user.name, style = MaterialTheme.typography.headlineSmall) } ``` ## Stateful vs Stateless Composables Structure composables as a stateless layer with optional stateful wrapper. ### Pattern: Stateless + Wrapper ```kotlin // ✅ Stateless composable (reusable, testable) @Composable fun ToggleButton( isEnabled: Boolean, onToggle: (Boolean) -> Unit, text: String ) { Button( onClick = { onToggle(!isEnabled) }, colors = ButtonDefaults.buttonColors( containerColor = if (isEnabled) Color.Blue else Color.Gray ) ) { Text(text) } } // ✅ Stateful wrapper (manages state, uses stateless child) @Composable fun StatefulToggleButton(text: String = "Toggle") { var isEnabled by remember { mutableStateOf(false) } ToggleButton( isEnabled = isEnabled, onToggle = { isEnabled = it }, text = text ) } // Usage: choose based on need @Composable fun MyScreen() { // Use stateless when caller manages state var featureEnabled by remember { mutableStateOf(false) } ToggleButton(featureEnabled, { featureEnabled = it }, "Feature") // Use stateful wrapper for isolated state StatefulToggleButton("Local Toggle") } ``` **Advantage:** Caller can test and reuse `ToggleButton` without mocking state. `StatefulToggleButton` provides convenience for simple cases. ## Preview Annotations Use previews for rapid UI development and regression testing. ### @Preview Basic preview of a single composable. ```kotlin @Preview @Composable fun UserCardPreview() { UserCard(user = User(1, "Alice")) } // Multiple previews @Preview(name = "Light") @Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES) @Composable fun UserCardPreviews() { UserCard(user = User(1, "Alice")) } ``` ### @PreviewLightDark Automatically generates light and dark theme previews. ```kotlin @PreviewLightDark @Composable fun UserCardPreview() { MyTheme { UserCard(user = User(1, "Alice")) } } ``` ### @PreviewFontScale Shows how composable responds to different font sizes. ```kotlin @Preview(fontScale = 0.8f, name = "Small Fonts") @Preview(fontScale = 1f, name = "Normal Fonts") @Preview(fontScale = 1.2f, name = "Large Fonts") @Composable fun TextPreview() { Text("This is text") } ``` ### @PreviewScreenSizes Tests multiple screen dimensions. ```kotlin @Preview(device = Devices.PHONE, name = "Phone") @Preview(device = Devices.TABLET, name = "Tablet") @Preview(device = Devices.FOLDABLE, name = "Foldable") @Composable fun ResponsiveLayoutPreview() { ResponsiveLayout() } ``` Source: `androidx.compose.ui.tooling.preview` ## CompositionLocal Provide implicit parameters without threading them through the hierarchy. ### When to Use - **Theming:** Pass `Colors`, `Typography` implicitly - **Navigation:** Access from deep in composable tree - **Locale/Strings:** Avoid passing through every composable ```kotlin // ✅ Define at top level val LocalUser = staticCompositionLocalOf { null } @Composable fun App(user: User) { CompositionLocalProvider(LocalUser provides user) { MainContent() } } // ✅ Access deep in tree without passing through every composable @Composable fun UserGreeting() { val user = LocalUser.current Text("Hello, ${user?.name}") } ``` ### When NOT to Use - **Configuration params:** If only 1-2 levels deep, pass directly - **Frequently changing values:** Can cause unnecessary recomposition - **Dependencies:** Use dependency injection at ViewModel level ```kotlin // ❌ Over-use: should pass `title` directly val LocalTitle = staticCompositionLocalOf { "" } @Composable fun Parent() { CompositionLocalProvider(LocalTitle provides "My Title") { Child() } } // ✅ Just pass it @Composable fun Parent(title: String) { Child(title = title) } @Composable fun Child(title: String) { ... } ``` **Types (recomposition scope is the key difference):** - `staticCompositionLocalOf`: When the value changes, the **entire subtree** below the `CompositionLocalProvider` is invalidated and recomposed. Use for values that truly never change during composition (theme, spacing, DI-provided dependencies). - `compositionLocalOf`: When the value changes, only composables that **actually read** `.current` are invalidated. Use for values that may change during composition (user session, locale, scroll state). ## Composable Return Values **Never return values from composables.** Use callbacks instead. ```kotlin // ❌ Wrong: composables don't return values @Composable fun UserInput(): String { var text by remember { mutableStateOf("") } return text // Can't do this } // ✅ Correct: callback pattern @Composable fun UserInput(onUserInput: (String) -> Unit) { var text by remember { mutableStateOf("") } TextField( value = text, onValueChange = { text = it onUserInput(it) // Notify parent } ) } // Usage @Composable fun FormScreen() { UserInput(onUserInput = { input -> /* handle */ }) } ``` **Rationale:** Composables are executed during composition, which happens at unpredictable times and may be skipped or reordered. ## Screen-Level Composables Structure screens as a thin ViewModel integration layer above pure composables. ### Recommended Pattern ```kotlin // ✅ Screen composable: connects ViewModel @Composable fun UserDetailsScreen( viewModel: UserDetailsViewModel = hiltViewModel(), userId: String ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffect(userId) { viewModel.loadUser(userId) } UserDetailsContent( uiState = uiState, onRetry = { viewModel.loadUser(userId) } ) } // ✅ Content composable: pure (testable, reusable) @Composable private fun UserDetailsContent( uiState: UiState, onRetry: () -> Unit ) { when (uiState) { is UiState.Loading -> LoadingUI() is UiState.Success -> SuccessUI(uiState.user) is UiState.Error -> ErrorUI(uiState.message, onRetry) } } // ✅ Composable for preview/testing @Preview @Composable private fun UserDetailsContentPreview() { UserDetailsContent( uiState = UiState.Success(User(1, "Alice")), onRetry = {} ) } ``` **Benefits:** - Public screen composable integrates ViewModel - Private content composable is pure, testable, previewable - Clear separation: UI logic (public) vs rendering (private) **Anti-pattern:** Passing ViewModel to child composables. Keep it at screen level only. ```kotlin // ❌ Couples child to ViewModel @Composable fun UserCard(viewModel: UserViewModel) { val user by viewModel.user.collectAsStateWithLifecycle() Text(user.name) } // ✅ Pass only the data @Composable fun UserCard(user: User) { Text(user.name) } ``` ## Reusability Guidelines Design composables to be configurable without over-parameterization. ### Configuration via Parameters ```kotlin // ✅ Expose what varies, hide what doesn't @Composable fun Card( modifier: Modifier = Modifier, content: @Composable () -> Unit ) { // Internal: fixed styling, padding, etc. Box( modifier = modifier .background(Color.White) .padding(16.dp) .clip(RoundedCornerShape(8.dp)) ) { content() } } ``` ### Avoid Parameter Explosion ```kotlin // ❌ Too many parameters, hard to use @Composable fun Button( text: String, textColor: Color, backgroundColor: Color, cornerRadius: Dp, padding: PaddingValues, elevation: Dp, ... ) // ✅ Sensible defaults, style objects @Composable fun Button( text: String, modifier: Modifier = Modifier, style: ButtonStyle = ButtonStyle.Primary, onClick: () -> Unit ) { ... } // Or: use Material composables with built-in styles @Composable fun Button( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, ... ) { ... } // Material3 Button has reasonable defaults ``` ## Common Anti-Patterns ### God Composables ```kotlin // ❌ Does too much @Composable fun Dashboard() { // Header Box { /* 20 lines */ } // List LazyColumn { items(items) { /* 15 lines */ } } // Footer Box { /* 15 lines */ } // Dialogs, side effects, state management... } // ✅ Extract and delegate @Composable fun Dashboard() { Column { DashboardHeader() DashboardContent() DashboardFooter() } } ``` ### Deep Nesting ```kotlin // ❌ Hard to read and debug @Composable fun LoginScreen() { Box { Column { Row { Card { Box { Text { ... } } } } } } } // ✅ Intermediate variables and extraction @Composable fun LoginScreen() { val form = loginFormState() Column { LoginForm(form) LoginButton(form) } } ``` ### Passing ViewModel to Children ```kotlin // ❌ Violates composition boundaries @Composable fun ParentScreen(viewModel: ParentViewModel) { ChildCard(viewModel = viewModel) // Don't do this } // ✅ Extract data, pass to child @Composable fun ParentScreen(viewModel: ParentViewModel) { val data by viewModel.data.collectAsStateWithLifecycle() ChildCard(data = data) } ``` --- **Source references:** `androidx.compose.material3`, `androidx.compose.ui.tooling.preview`, `androidx.compose.runtime.CompositionLocal` ## Design-to-Composable Decomposition A systematic 5-step process for translating a visual design (Figma frame, screenshot, or spec) into a composable tree: **Step 1: Identify the root layout structure** - Full-screen Scaffold? (TopAppBar + content + bottom bar + FAB) - Scrollable content? (LazyColumn vs Column with verticalScroll) - Tabbed layout? (TabRow + HorizontalPager) - Dialog or bottom sheet? **Step 2: Decompose into visual sections (top-down)** - Identify major horizontal sections (header, content area, footer) - Within each section, identify horizontal groupings (icon + text rows, card grids) - This mirrors the DCGen divide-and-conquer approach: split horizontally first, then vertically **Step 3: For each section, identify the layout type** - Items stacked vertically with equal spacing -> `Column` with `Arrangement.spacedBy()` - Items side by side -> `Row` with weights or fixed sizes - Items overlapping -> `Box` with alignment modifiers - Grid of cards -> `LazyGrid` or `FlowRow` - Scrollable list of items -> `LazyColumn` **Step 4: Extract visual properties and map to theme** - Background colors -> `MaterialTheme.colorScheme.*` - Typography -> `MaterialTheme.typography.*` (headlineLarge, bodyMedium, etc.) - Spacing -> 4dp/8dp grid increments, use `Arrangement.spacedBy()` and `Modifier.padding()` - Corner radius -> `MaterialTheme.shapes.*` - Elevation -> `Card` or `Surface` with `tonalElevation` **Step 5: Identify interactive elements** - Buttons, text fields, toggles, checkboxes -> map to Material 3 components - Custom clickable areas -> `Modifier.clickable` with `role = Role.Button` - Add `contentDescription` for accessibility - Ensure 48dp minimum touch targets ## Screen Structure Patterns The standard screen pattern separates ViewModel integration from UI: ```kotlin @Composable fun ConversationScreen( viewModel: ConversationViewModel = hiltViewModel(), onNavigateToDetail: (String) -> Unit ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() ConversationContent( uiState = uiState, onAction = viewModel::onAction, onNavigateToDetail = onNavigateToDetail ) } @Composable private fun ConversationContent( uiState: ConversationUiState, onAction: (ConversationAction) -> Unit, onNavigateToDetail: (String) -> Unit ) { Scaffold( topBar = { TopAppBar(title = { Text("Conversations") }) }, floatingActionButton = { FloatingActionButton(onClick = { onAction(ConversationAction.Create) }) { Icon(Icons.Default.Add, contentDescription = "New conversation") } } ) { innerPadding -> // MUST use innerPadding -- ignoring it causes content overlap when (val state = uiState) { is ConversationUiState.Loading -> { Box(Modifier.fillMaxSize().padding(innerPadding), contentAlignment = Alignment.Center) { CircularProgressIndicator() } } is ConversationUiState.Success -> { LazyColumn(modifier = Modifier.padding(innerPadding)) { items(state.conversations, key = { it.id }) { conversation -> ConversationRow( conversation = conversation, onClick = { onNavigateToDetail(conversation.id) } ) } } } is ConversationUiState.Error -> { ErrorContent(state.message, modifier = Modifier.padding(innerPadding)) } } } } ``` Key pattern: ViewModel at screen level, pure content composable underneath. The content composable receives state + callbacks, never the ViewModel. This makes it previewable and testable. ## Composite Preview Annotations Define once, use everywhere: ```kotlin @Preview(name = "Light", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(name = "Large Font", fontScale = 1.5f) @Preview(name = "Small Device", device = "spec:width=320dp,height=640dp,dpi=320") @Preview(name = "Tablet", device = Devices.TABLET) @Preview(name = "Foldable", device = Devices.FOLDABLE) @Preview(name = "RTL", locale = "ar") annotation class ComponentPreviews ``` Apply to every extracted composable: ```kotlin @ComponentPreviews @Composable private fun ConversationRowPreview() { AppTheme { ConversationRow( conversation = previewConversation(), onClick = {} ) } } ``` For data-driven previews, use `PreviewParameterProvider`: ```kotlin class ConversationPreviewProvider : PreviewParameterProvider { override val values = sequenceOf( Conversation(id = "1", title = "Short title", unreadCount = 0), Conversation(id = "2", title = "Very long conversation title that might wrap", unreadCount = 99), Conversation(id = "3", title = "", unreadCount = 0), // Empty title edge case ) } @ComponentPreviews @Composable private fun ConversationRowPreview( @PreviewParameter(ConversationPreviewProvider::class) conversation: Conversation ) { AppTheme { ConversationRow(conversation = conversation, onClick = {}) } } ``` **CMP note:** In `commonMain`, use `@Preview` from `org.jetbrains.compose.ui.tooling.preview`. Device-specific previews (`Devices.TABLET`) are Android-only. ## Adaptive Layouts Use `WindowSizeClass` to adapt layouts for different screen sizes: ```kotlin @Composable fun AdaptiveScreen(windowSizeClass: WindowSizeClass) { when (windowSizeClass.widthSizeClass) { WindowWidthSizeClass.Compact -> { // Phone: single column SinglePaneLayout() } WindowWidthSizeClass.Medium -> { // Small tablet: two panes TwoPaneLayout() } WindowWidthSizeClass.Expanded -> { // Large tablet/desktop: list-detail ListDetailLayout() } } } ``` For navigation, use `NavigationSuiteScaffold` which automatically switches between bottom nav (compact), rail (medium), and drawer (expanded). ================================================ FILE: .claude/skills/edge-to-edge/SKILL.md ================================================ --- name: edge-to-edge description: |- Use this skill to migrate your Jetpack Compose app to add adaptive edge-to-edge support and troubleshoot common issues. Use this skill to fix UI components (like buttons or lists) that are obscured by or overlapping with the navigation bar or status bar, fix IME insets, and fix system bar legibility. license: Complete terms in LICENSE.txt metadata: author: Google LLC keywords: - android - compose - system bars - edge-to-edge - status bar - navigation bar --- ## Prerequisites - Project **MUST** use Android Jetpack Compose. - Project **MUST** target SDK 35 or later. If the SDK is lower than 35, increase the SDK to 35. ## Step 1: plan 1. Locate and analyze all Activity classes to detect which have existing edge-to-edge support. For every Activity without edge-to-edge, plan to make each Activity edge-to-edge. 2. In each Activity, Locate and analyze all lists and FAB components to detect which have existing edge-to-edge support. For every component without edge-to-edge support, plan to make each of these components edge-to-edge. 3. In each Activity, scan for `TextField`, `OutlinedTextField`, or `BasicTextField`. If found, then you **MUST** verify the IME doesn't hide the input field by following the IME section of this skill. ## Step 2: add edge-to-edge support 1. Add `enableEdgeToEdge` before `setContent` in `onCreate` in each Activity that does not already call `enableEdgeToEdge`. 2. Add `android:windowSoftInputMode="adjustResize"` in the AndroidManifest.xml for all Activities that use a soft keyboard. ## Step 3: apply insets - The app **MUST** apply system insets, or align content to rulers, so critical UI remains tappable. Choose only one method to avoid double padding: 1. **PREFERRED:** When available, use `Scaffold`s and pass `PaddingValues` to the content lambda. ```kotlin Scaffold { innerPadding -> // innerPadding accounts for system bars and any Scaffold components LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* Content */ } } ```
1. **PREFERRED:** When available, use the automatic inset handling or padding modifiers in material components. - Material 3 Components manages safe areas for its own components, including: - `TopAppBar` - `SmallTopAppBar` - `CenterAlignedTopAppBar` - `MediumTopAppBar` - `LargeTopAppBar` - `BottomAppBar` - `ModalDrawerSheet` - `DismissibleDrawerSheet` - `PermanentDrawerSheet` - `ModalBottomSheet` - `NavigationBar` - `NavigationRail` - For Material 2 Components, use the `windowInsets`parameter to apply insets manually for `BottomAppBar`, `TopAppBar` and `BottomNavigation`. **DO NOT** apply padding to the parent container; instead, pass insets directly to the App Bar component. Applying padding to the parent container prevents the App Bar background from drawing into the system bar area. For example, for `TopAppBar`, choose only one of the following options: 1. **PREFERRED:** `TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets)` 2. `TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))` 3. `TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))` 2. For components outside a Scaffold, use padding modifiers, such as `Modifier.safeDrawingPadding()` or `Modifier.windowInsetsPadding(WindowInsets.safeDrawing)`. ```kotlin Box( modifier = Modifier .fillMaxSize() .safeDrawingPadding() ) { Button( onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("Login") } } ```
3. For deeply nested components with excessive padding, use `WindowInsetsRulers` (e.g. `Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current)`). See the *IME* section for a code sample. 4. When you need an element (e.g. a custom header or decorative scrim) to equal the dimensions of a system bar, use inset size modifiers (e.g. `Modifier.windowInsetsTopHeight(WindowInsets.systemBars)`). See the *Lists* section for a code sample. ## Adaptive Scaffolds - `NavigationSuiteScaffold` manages safe areas for its own components, like the `NavigationRail` or `NavigationBar`. However, the adaptive scaffolds (e.g. `NavigationSuiteScaffold`, `ListDetailPaneScaffold`) don't propagate PaddingValues to their inner contents. You **MUST** apply insets to **individual** screens or components (e.g., list `contentPadding` or FAB padding) as described in *Step 3* . **DO NOT** apply `safeDrawingPadding` or similar modifiers to the `NavigationSuiteScaffold` parent. This clips and prevents an edge-to-edge screen. ## IME - For each Activity with a soft keyboard, check that `android:windowSoftInputMode="adjustResize"` is set in the AndroidManifest.xml. DO NOT use `SOFT_INPUT_ADJUST_RESIZE` because it is deprecated. Then, maintain focus on the input field. Choose one: - 1. **PREFERRED:** Add `Modifier.fitInside(WindowInsetsRulers.Ime.current)` to the content container. This is preferred over `imePadding()` because it reduces jank and extra padding caused by forgetting to consume insets upstream in the hierarchy. - 2. Add `imePadding` to the content container. The padding modifier **MUST** be placed before `Modifier.verticalScroll()`. Do NOT use `Modifier.imePadding()` if the parent already accounts for the IME with `contentWindowInsets` (e.g. `contentWindowInsets = WindowInsets.safeDrawing`). Doing so will cause double padding. ### IMEs with Scaffolds code patterns #### RIGHT RIGHT because `contentWindowInsets` contains IME insets, which are passed to the content lambda as `innerPadding`. ```kotlin // RIGHT Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .consumeWindowInsets(innerPadding) .verticalScroll(rememberScrollState()) ) { /* Content */ } } ```
*** ** * ** *** RIGHT because `fitInside` fits the content to the IME insets regardless of `contentWindowInsets`. ```kotlin // RIGHT Scaffold() { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .consumeWindowInsets(innerPadding) .fitInside(WindowInsetsRulers.Ime.current) .verticalScroll(rememberScrollState()) ) { /* Content */ } } ```
*** ** * ** *** RIGHT because the default `contentWindowInsets` does not contain IME insets, and `imePadding()` applies IME insets: ```kotlin // RIGHT Scaffold() { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .consumeWindowInsets(innerPadding) .imePadding() .verticalScroll(rememberScrollState()) ) { /* Content */ } } ```
#### WRONG WRONG because there will be excess padding when the IME opens. IME insets are applied twice, once with innerPadding, which contains IME insets from the passed `contentWindowInsets` values, and once with `imePadding`: ```kotlin // WRONG Scaffold( contentWindowInsets = WindowInsets.safeDrawing ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .imePadding() .verticalScroll(rememberScrollState()) ) { /* Content */ } } ```
*** ** * ** *** WRONG because the IME will cover up the content. Scaffold's default `contentWindowInsets` does NOT contain IME insets. ```kotlin // WRONG Scaffold() { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .verticalScroll(rememberScrollState()) ) { /* Content */ } } ```
### IMEs without Scaffolds code patterns #### RIGHT The following code samples WILL NOT cause excessive padding. ```kotlin // RIGHT Box( // Insets consumed modifier = Modifier.safeDrawingPadding() // or imePadding(), safeContentPadding(), safeGesturesPadding() ) { Column( modifier = Modifier.imePadding() ) { /* Content */ } } ```
*** ** * ** *** ```kotlin // RIGHT Box( // Insets consumed modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // or WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures ) { Column( modifier = Modifier.imePadding() ) { /* Content */ } } ```
*** ** * ** *** ```kotlin // RIGHT Box( // Insets not consumed, but irrelevant due to fitInside modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues() ) { Column( modifier = Modifier .fillMaxSize() .fitInside(WindowInsetsRulers.Ime.current) ) { /* Content */ } } ```
#### WRONG The following code sample WILL cause excessive padding because IME insets are applied twice: ```kotlin // WRONG Box( // Insets not consumed modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // or WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues() ) { Column( modifier = Modifier.imePadding() ) { /* Content */ } } ```
## Navigation Bar Contrast \& System Bar Icons - If the Activity uses `enableEdgeToEdge` from `WindowCompat`, you **MUST** set `isAppearanceLightNavigationBars` and `isAppearanceLightStatusBars` to the inverse of the device theme for apps that support light and dark theme so the system bar icons are legible. It's recommended to do this in your theme file. DO NOT do this if the Activities use `enableEdgeToEdge` from `ComponentActivity` because it handles the icon colors automatically. ```kotlin // Only use if calling `enableEdgeToEdge` from `WindowCompat`. // Apply to your theme file. @Composable fun MyTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as? Activity)?.window ?: return@SideEffect val controller = WindowCompat.getInsetsController(window, view) // Dark icons for Light Mode (!darkTheme), Light icons for Dark Mode controller.isAppearanceLightStatusBars = !darkTheme controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme(content = content) } ```
- If any screen uses a `Scaffold` or a `NavigationSuiteScaffold` with a bottom bar (e.g., `BottomAppBar`, `NavigationBar`), set `window.isNavigationBarContrastEnforced = false` in the corresponding Activity for SDK 29+. This prevents the system from adding a translucent background to the navigation bar, verifying your bottom bar colors extend to the bottom of the screen. ## Lists - Apply inset padding (like `Scaffold`'s `innerPadding`) to the `contentPadding` parameter of scrollable components (e.g. `LazyColumn`, `LazyRow`). DO NOT apply it as a `Modifier.padding()` to the list's parent container, as this clips the content and prevents it from scrolling behind the system bars. - Create a translucent composable covering the system bar so that the icons are still legible. ```kotlin class SystemBarProtectionSnippets : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // enableEdgeToEdge sets window.isNavigationBarContrastEnforced = true // which is used to add a translucent scrim to three-button navigation enableEdgeToEdge() setContent { MyTheme { // Main content MyContent() // After drawing main content, draw status bar protection StatusBarProtection() } } } } @Composable private fun StatusBarProtection( color: Color = MaterialTheme.colorScheme.surfaceContainer, ) { Spacer( modifier = Modifier .fillMaxWidth() .height( with(LocalDensity.current) { (WindowInsets.statusBars.getTop(this) * 1.2f).toDp() } ) .background( brush = Brush.verticalGradient( colors = listOf( color.copy(alpha = 1f), color.copy(alpha = 0.8f), Color.Transparent ) ) ) ) } ```
## Dialogs If both the following conditions are true, then the Dialog is full screen and must be made edge-to-edge: 1. The `DialogProperties` contains `usePlatformDefaultWidth = false`. 2. The Dialog calls `Modifier.fillMaxSize()`. To make a full screen Dialog edge-to-edge, set `decorFitsSystemWindows = false` in the `DialogProperties`. ```kotlin Dialog( onDismissRequest = { /* Handle dismiss */ }, properties = DialogProperties( // 1. Allows the dialog to span the full width of the screen usePlatformDefaultWidth = false, // 2. Allows the dialog to draw behind status and navigation bars decorFitsSystemWindows = false ) ) { /* Content */ } ```
## Checklist - \[ \] Does every `Activity` call `enableEdgeToEdge()`? - \[ \] Is `adjustResize` set in the `AndroidManifest.xml`? - \[ \] Does every `TextField`, `OutlinedTextField`, or `BasicTextField` have a parent with `imePadding()`, `fitInside`, `Modifier.safeDrawingPadding()`, `Modifier.safeContentPadding()`, `Modifier.safeGesturesPadding()`, or `contentWindowInsets` set to `WindowInsets.safeDrawing` or `WindowInsets.ime`? - \[\] Does the first and last list item draw away from the system bars by passing insets to `contentPadding`? - \[\] Do FABs draw above the navigation bars by either being inside a Scaffold or by applying `Modifier.safeDrawingPadding()`? - \[\] Does the project build? Run `./gradlew build` to be sure. ================================================ FILE: .claude/skills/r8-analyzer/SKILL.md ================================================ --- name: r8-analyzer description: |- Analyzes Android build files and R8 keep rules to identify redundancies, broad package-wide rules, and rules that subsume library consumer keep rules. Use when developers want to optimize their app's size, remove redundant or overly broad keep rules, or troubleshoot Proguard configurations. license: Complete terms in LICENSE.txt metadata: author: Google LLC keywords: - R8 - proguard - keep rules - app size - optimization --- ## Core workflow - \[ \] Step 1: Create a file called R8_Configuration_Analysis.md, or reuse if one exists already, to store the output - \[ \] Step 2: Look at the configuration of R8 by looking at build.gradle, build.gradle.kts, gradle.properties in the codebase using [references/CONFIGURATION.md](references/CONFIGURATION.md) as the reference. Inform the developer and add the analysis to the report file - \[ \] Step 3: If the AGP version is less than 9, suggest moving to AGP 9.0 version as AGP 9.0 includes [optimizations](references/android/topic/performance/app-optimization/enable-app-optimization.md). - \[ \] Step 4: Look at the proguard files in the codebase and evaluate each keep rule in the following specific order: a. **Libraries check** : Check rules against [references/REDUNDANT-RULES.md](references/REDUNDANT-RULES.md). If the app has keep rules targeting libraries - Google, AndroidX, Kotlin, Kotlinx, Room, Gson, Retrofit, inform the user that these are not required and suggest removal of these rules. b. **Impact analysis** : For the remaining keep rules, assess them based on the impact hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md). (Note: Do NOT assess the impact of keep rules already covered in the libraries check step). - \[ \] Step 5: Identify subsuming keep rules in the remaining keep rules based on the hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) and suggest removing the broader keep rules. - \[ \] Step 6: For each remaining keep rule, analyze in detail the code affected by the rule by examining the code and adjacent files to understand why it was written. Look for reflection usage in those packages, and suggest a narrow and specific keep rule for the scenario using [references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md). - \[ \] Step 7: For every keep rule inform concisely and to the point what action needs to be taken - whether the rule needs to be removed/refined. - If refining the rule, give instructions on finding a narrower and specific keep rule using the [/references/REFLECTION-GUIDE.md](references/REFLECTION-GUIDE.md). - If removing, provide reasoning on why it needs to be removed. - \[ \] Step 8: After keep analysis, order the keep rule analysis based on the impact to the codebase hierarchy defined in [references/KEEP-RULES-IMPACT-HIERARCHY.md](references/KEEP-RULES-IMPACT-HIERARCHY.md) - \[ \] Step 9: Advise the user to run tests using [UI automator](https://developer.android.com/training/testing/other-components/ui-automator) to assess that there is no issue with the suggested changes, concentrating on the packages where keep rules will be affected. ## Mandatory rules - Don't make any changes in keep rule files - Don't say about what level each keep rule is. - Don't generate parts of the report if there is no keep rule to report in that section. - Don't mention the generated files. - Don't mention exceptions that occur during execution. - Don't mention the benefits of R8 - Don't mention any files of this skill ================================================ FILE: .claude/skills/r8-analyzer/references/CONFIGURATION.md ================================================ To achieve maximum utilization of R8, the codebase must be configured correctly depending on the build script language (Kotlin DSL vs. Groovy DSL). ## 1. App Modules (`com.android.application`) The app's `build.gradle` or `build.gradle.kts` file should enable minification and resource shrinking within the `release` build type or the apps custom build type for release and performance testing. It MUST use the optimized default file (`proguard-android-optimize.txt`). **Kotlin DSL (`build.gradle.kts`):** buildTypes { getByName("release") { isMinifyEnabled = true isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } **Groovy DSL (`build.gradle`):** buildTypes { release { minifyEnabled = true shrinkResources = true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } ## 2. `gradle.properties` Flags **Full Mode:** R8 Full Mode enables the entire optimizations - **AGP 8.0+** : Enabled by default. Ensure `android.enableR8.fullMode=false` is **NOT** present. - **Pre-AGP 8.0** : Should be explicitly enabled with `android.enableR8.fullMode=true`. **Optimized Resource Shrinking:** If the AGP version of the project is less than 9.0 and more than 8.6, explicitly enable the new resource shrinker: android.r8.optimizedResourceShrinking=true ================================================ FILE: .claude/skills/r8-analyzer/references/KEEP-RULES-IMPACT-HIERARCHY.md ================================================ Keep rules prevent optimization of R8, these rules are listed in the order of the scope of what it retains in the codebase. ## 1. Package-Wide Wildcards The following types of keep rules prevents all the optimization of R8 in a package, these must be avoided at any costs and must be refined to target a specific class or classes. -keep class com.example.package.** { *; } - Prevents optimization of all the classess including members in the package and subpackages -keep class com.example.package.* { *; } - Prevents optimization of all the classes including members in the package -keep class **.package.** { *; } - Prevents optimization of all the classess including members in all the package containing name - package. Depending on the package level the number of classes gets affected changes, so if the package level is higher, more classes are affected. Suggest to refine the keep rule ## 2. Inversion operator Avoid using the inversion operator ! in keep rules because it will unintentionally prevent optimization in every class in your application. So if you have any keep rule with !operator, make sure you remove that with a narrow and specific keep rule -keep class !com.example.MyClass{*;} This keeps the entire app other than this class. Optimization are disabled for the entire class other than this class. ## 3. Keep Rules for both class and members Keep rules with -keep option and wildcard(`*`) inside braces forces R8 to retain specific classes and their members exactly as defined. These type of keep rules prevent any optimization in the entire class and keeps the entire class -keep class com.example.MyClass { *; } ## 4. Keepclassmembers Keep rules with -keepclassmembers and wildcard(`*`) inside braces option Forces R8 to retain the members that are defined. -keepclassmembers class com.example.MyClass { *; } ## 5. Modifiers with Keep Specification -Keeps the class and **all** members, but uses modifiers to allow specific optimizations (like obfuscation). Retains significant code (members) but allows some flexibility. -keep,allowobfuscation class com.example.MyClass { *; } -keep,allowshrinking class com.example.MyClass { *; } ### 6. Modifiers with specific method but no modifier Keeps the class and modifier but no optimizations are enabled -keep class com.example.MyClass { void myMethod(); } ## 7. Class-Name Only Preservation Keeps only the class name. R8 will remove all methods and fields if they are not used. -keep class com.example.MyClass ## 8. Modifiers without Member Specification Keeps the class entry point using modifiers, but implies no specific member retention logic in the rule itself -keep,allowobfuscation class com.example.MyClass -keep,allowshrinking class com.example.MyClass -keep,allowaccessmodification class com.example.MyClass ## 9. Conditional Keep Rules Only triggers if specific conditions are met (e.g., if class members exist). These are the most narrow and optimization-friendly rules. -keepclassmembers class com.example.MyClass { ; } -keepclasseswithmembers class * { native ; } ================================================ FILE: .claude/skills/r8-analyzer/references/REDUNDANT-RULES.md ================================================ This document outlines common "bad" or redundant keep rules for standard Android development and popular libraries. Modern toolchains and libraries include their own consumer keep rules embedded in their AAR/JAR files, making many manual configurations unnecessary or even harmful to code optimization. *** ** * ** *** ## Case: Global Keep Rules **Common Mistakes:** `proguard -dontshrink -dontobfuscate -dontoptimize` **The Fix:** These keep rules completely disable the core optimizations of R8 for the entire codebase. They must be removed from the codebase. *** ** * ** *** ## Case: Android Components Keep rules required for Android components like Activity, Fragment, ViewModel, Views, Services or Broadcast receivers are redundant. AAPT2 and R8 contain the logic to automatically keep components declared in the `AndroidManifest.xml` or referenced in XML layout files. **Common Mistakes:** `proguard -keep public class * extends android.app.Activity -keep public class * extends android.app.Service -keep public class * extends android.view.View -keepclassmembers class * extends android.app.Fragment { public void *(android.view.View); }` **The Fix:** Delete these manual rules. AAPT2 handles this automatically. *** ** * ** *** ## Case: Official Android and Kotlin Libraries Keep rules targeting official library packages like AndroidX, Kotlin, and Kotlinx are redundant as they are bundled within the libraries themselves. Manual rules are often broader than what is strictly needed. **Common Mistakes:** `proguard -keep class androidx.** { *; } -keep class kotlinx.** { *; } -keep class kotlin.** { *; }` **The Fix:** Delete these manual rules. Rely on the consumer keep rules packaged within these dependencies. *** ** * ** *** ## Case: Gson ### Overly Broad Data Model Rules The most common mistake is keeping entire packages of data models (POJOs/DTOs), keeping data models at all for deserialization is unnecessary. -keep class com.example.app.models.** { *; } -keep class com.example.app.package.models.* { *; } ### Redundant Interface \& Adapter Rules These rules added for TypeAdapter are unnecessary and are already covered by the library, and prevent R8 from effectively shrinking and optimizing custom adapters. R8 can determine if the adapter implementation are used. Keeping them globally prevents the removal of unused adapter implementations. -keep class * extends com.google.gson.TypeAdapter -keep class * implements com.google.gson.TypeAdapterFactory -keep class * implements com.google.gson.JsonSerializer -keep class * implements com.google.gson.JsonDeserializer ### Unnecessary TypeToken Rules There is no need to handle generic type erasure, Gson's own rules handle the necessary `TypeToken` preservation. -keep class com.google.gson.reflect.TypeToken { *; } -keep class * extends com.google.gson.reflect.TypeToken -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken ### Internal and Example Packages Keeping internal library logic prevents the compiler from stripping away dead code within the library. -keep class com.google.gson.internal.** { *; } -keep class com.google.gson.internal.reflect.** { *; } -keep class com.google.gson.internal.UnsafeAllocator { *; } -keep class com.google.gson.stream.** { *; } - **Keeps Unused Code:** Prevents R8 from removing models that are never actually used in the code. - **Prevents Method Stripping:** Keeps all getters, setters, `toString()`, `equals()`, and `hashCode()` methods, even if they are never called. - **Blocks Obfuscation:** Prevents the class names from being obfuscated, which is unnecessary for Gson if you use `@SerializedName`. **The Fix:** 1. Use `@SerializedName` on every field in your data classes uses so that the field is retained after R8 optimization 2. Modern Gson (**v2.11.0+** ) bundles its own rules ([View Gson's embedded ProGuard rules](https://github.com/google/gson/blob/main/gson/src/main/resources/META-INF/proguard/gson.pro)). The bundled keep rules retains the `@SerializedName` annotated fields. If you are on an older version, move towards Gson version 2.11 because it has the necessary keep rules and delete the keep rules that target the classes used for gson serialization and deserialization *** ** * ** *** ## Case: Retrofit Retrofit has shipped with its own consumer keep rules from 2.9.0 and higher, so any keep rules for the library or classes depending on Retrofit is detrimental to the optimization process. ### Blanket Library Preservation This is the most harmful Retrofit rule as it disables any shrinking for the entire library. -keep class retrofit2.** { *; } -keep class retrofit2.api.** { *; } -keep class com.package.example.retrofit.api.** { *; } ### Manual Annotation Keeps Retrofit's consumer rules automatically keep the interfaces annotated with `@GET`, `@POST`, `@DELETE`, `@PUT`, `@HEAD`, `@OPTIONS`, `@PATCH`, making these manual rules obsolete. `-keepclasseswithmembers class * { @retrofit2.http.* ; }` ### Redundant Network Response and Adapter Rules Network responses and third-party adapter wrappers (like RxJava) are often overly preserved by developers out of caution. -keep,allowobfuscation,allowshrinking class retrofit2.Response -keep class retrofit2.adapter.rxjava2.Result { *; } Fix: Verify you are using Retrofit 2.9.0 and higher. Retrofit from 2.9.0 bundles rules that detect its own HTTP annotations (@GET, @POST) ([View Retrofit's embedded ProGuard rules](https://github.com/square/retrofit/blob/master/retrofit/src/main/resources/META-INF/proguard/retrofit2.pro)). It will automatically keep the method signatures it needs to work. *** ** * ** *** ## Case: Kotlin Coroutines Kotlin Coroutines comes heavily optimized out of the box with embedded R8 rules (`kotlinx-coroutines-core` includes its own rules). ### Blanket Coroutine Library Rules Keeping everything under `kotlinx.coroutines` is extremely detrimental to app size, as coroutines contain a vast amount of internal APIs that aren't used. `-keepclassmembers class kotlinx.coroutines.** { *; }` ### Redundant Internal Continuations These low-level coroutine elements are preserved safely by the library's own consumer rules. Manually adding these prevents R8 from performing internal optimizations (such as removing unused continuations or inlining). -keepclassmembers class kotlin.coroutines.SafeContinuation { *; } -keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation ### Dispatcher and Exception Handler Rules Sometimes developers notice crashes related to Missing Classes on old Android versions and add these rules, but if you are using an up-to-date version of Coroutines, these are handled automatically or are not an issue. -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} -keepnames class kotlinx.coroutines.android.AndroidExceptionPreHandler {} -keepnames class kotlinx.coroutines.android.AndroidDispatcherFactory {} **Fix** Remove any broad `kotlinx` keep rules. Coroutines (**v1.7.0+** ) bundle the necessary keep rules ([View Coroutines' embedded ProGuard rules](https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/jvm/resources/META-INF/proguard/coroutines.pro)). *** ** * ** *** ## Case: Parcelable **Common Mistakes:** Legacy projects often contain `-keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *; }`. **The Fix:** 1. Add the `kotlin-parcelize` plugin. 2. **Use `@Parcelize`:** Replace manual `writeToParcel` logic with the `@Parcelize` annotation. 3. **Delete All Parcelable Rules:** The plugin automatically generates the required rules. 4. The default proguard file `proguard-android-optimize.txt` contains the keep rules for keeping all the parcelable classes 5. **Ideal Rule:** **None.** Delete all manual Parcelable keeps. *** ** * ** *** ## Case: Room Database **Common Mistakes:** Keeping DAO interfaces or the generated `_Impl` classes manually. -keep class * extends androidx.room.RoomDatabase -keep class *_*Impl { *; } **The Fix:** Room generates its own ProGuard rules for the code it creates. Manual rules are redundant and prevent R8 from optimizing the database access layers. - **Ideal Rule:** **None.** Delete all manual Room or DAO keeps. *** ** * ** *** ## Summary If you have updated your libraries to the versions mentioned, your `proguard-rules.pro` must not contain any keep rules for the libraries mentioned here. ================================================ FILE: .claude/skills/r8-analyzer/references/REFLECTION-GUIDE.md ================================================ A categorized summary of the keep rule examples, including the code patterns to look for (imports/usage) and the corresponding suggested rules. ### 1. Reflection: Classes Loaded by Name **Scenario:** A library or app loads a class dynamically using a string name - **Look for:** `Class.forName("...")`, `getDeclaredConstructor().newInstance()`, or interfaces used for dynamic loading. - **Example Code:** `kotlin val taskClass = Class.forName(className) val task = taskClass.getDeclaredConstructor().newInstance() as StartupTask` - **Suggested Keep Rule:** \`\`\`proguard -keep class \* implements com.example.library.StartupTask { (); } \`\`\` ### 2. Reflection: Classes Passed using `::class.java` **Scenario:** An app passes a class reference directly to a library function. - **Look for:** `::class.java` (Kotlin) or `.class` (Java) passed as an argument. - **Example Code:** `kotlin fun register(clazz: Class) { } // Usage: register(MyService::class.java)` - **Suggested Keep Rule:** \`\`\`proguard # Keep the class itself (R8 usually handles this, but explicit rules ensure stability) -keep class com.example.app.MyService { (); } \`\`\` ### 3. Annotation-Based Reflection (Methods/Classes) **Scenario:** Using custom annotations to mark methods or classes for reflective execution. **Look for:** Custom `@interface` definitions and `getDeclaredMethods()` filtered by annotation. **Example Code:** `kotlin annotation class ReflectiveExecutor // Logic: find methods annotated with @ReflectiveExecutor and invoke them` - **Suggested Keep Rule:** \`\`\`proguard # Keep the annotation itself -keep @interface com.example.library.ReflectiveExecutor # Keep members of any class annotated with this specific annotation -keepclassmembers class \* { @com.example.library.ReflectiveExecutor \*; } \`\`\` ### 4. Optional Dependencies (Soft Dependencies) **Scenario:** A core library checks if an optional module is present in the classpath. - **Look for:** `try-catch` blocks around `Class.forName()` used to toggle features. - **Example Code:** \`\`\`kotlin private const val VIDEO_TRACKER_CLASS = "com.example.analytics.video.VideoEventTracker" try { Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance() } catch (e: ClassNotFoundException) { /\* skip feature \*/ } \`\`\` - **Suggested Keep Rule:** `proguard # Preserve the optional class so the check doesn't fail due to shrinking -keep class com.example.analytics.video.VideoEventTracker { (); }` ### 5. Accessing Private Members **Scenario:** Using reflection to access internal fields or methods not exposed with public APIs. - **Look for:** `getDeclaredField("...")` or `getDeclaredMethod("...")` followed by `isAccessible = true`. - **Example Code:** `kotlin val secretField = instance::class.java.getDeclaredField("secretMessage") secretField.isAccessible = true` - **Suggested Keep Rule:** \`\`\`proguard # Specifically keep the private field/method by name and type -keepclassmembers class com.example.LibraryClass { private java.lang.String secretMessage; } \`\`\` ### 6. Parcelable (Manual Implementation) **Scenario:** Implementing `Parcelable` without using the `@Parcelize` annotation. - **Look for:** `implements Parcelable` and a static `CREATOR` field. - **Example Code:** `kotlin class MyData : Parcelable { // Manual implementation with CREATOR field }` - **Suggested Keep Rule:** *(Note: If using `import kotlinx.parcelize.Parcelize`, R8/ProGuard rules are generated automatically. If manual, use the following:)* `proguard -keepclassmembers class * implements android.os.Parcelable { static android.os.Parcelable$Creator CREATOR; }` ### 7. Enums and Obfuscation **Scenario:** App uses `Enum.valueOf("STRING_NAME")` indirectly (e.g.,using JSON deserialization) and the enum names get obfuscated. - **Look for:** Unnecessary generic Enum keep rules in ProGuard files. - **Example Code:** \`\`\`proguard # Unnecessary rule -keepclassmembers enum \* { \*; } \`\`\` - **Suggested Keep Rule:** \*(Note: The default `proguard-android-optimize.txt` already contains the optimal rules for Enums (keeping `values()` and `valueOf(String)`). Any additional manual rules for Enums are redundant.) # No manual rule needed. Use default proguard-android-optimize.txt. ================================================ FILE: .claude/skills/r8-analyzer/references/android/topic/performance/app-optimization/enable-app-optimization.md ================================================ For the best user experience, you should optimize your app to make it as small and fast as possible. Our app optimizer, called R8, streamlines your app by removing unused code and resources, rewriting code to optimize runtime performance, and more. To your users, this means: - Faster startup time - Reduced memory usage - Improved rendering and runtime performance - Fewer [ANRs](https://developer.android.com/topic/performance/anrs/keep-your-app-responsive) > [!IMPORTANT] > **Important:** You should always enable optimization for your app's release build; however, you probably don't want to enable it for tests or libraries. For more information about using R8 with tests, see [Test and troubleshoot the > optimization](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization). For more information about enabling R8 from libraries, see [Optimization for library authors](https://developer.android.com/topic/performance/app-optimization/library-optimization). ## R8 optimization overview R8 uses a multi-phase process to optimize your app for size and speed. Key operations include the following: - **Code shrinking (also known as tree shaking)** : R8 identifies and removes unreachable code from your application and its library dependencies. By analyzing the entry points of your app (such as `Activities` or `Services` defined in the manifest), R8 builds a graph of referenced code and removes anything that remains unreferenced. - **Logical optimizations**: R8 rewrites your code to improve execution efficiency and reduce overhead. Key techniques include: - **Method inlining**: R8 replaces a method call site with the actual body of the called method. This eliminates the overhead of a function call and lets R8 conduct further optimizations. - **Class merging**: R8 combines sets of classes and interfaces into a single class. This reduces the number of classes in the app, lowering memory pressure and improving startup speed. - **Obfuscation (also known as minification)** : To reduce the size of the DEX file, R8 shortens the names of classes, fields, and methods (for example, `com.example.MyActivity` could become `a.b.a`). Since 8.12.0 version of Android Gradle Plugin (AGP), R8 also optimizes resources as part of its optimization phases. For more information, see [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking). ## Enable optimization To enable app optimization, set `isMinifyEnabled = true` (for code optimization) and `isShrinkResources = true` (for resource optimization) in your [release build's](https://developer.android.com/studio/publish/preparing#turn-off-debugging) app-level build script as shown in the following code. We recommend that you always enable both settings. We also recommend enabling app optimization only in the final version of your app that you test before publishing---usually your release build---because the optimizations increase the build time of your project and can make debugging harder due to the way it modifies code. ### Kotlin ```kotlin android { buildTypes { release { // Enables code-related app optimization. isMinifyEnabled = true // Enables resource shrinking. isShrinkResources = true proguardFiles( // Default file with automatically generated optimization rules. getDefaultProguardFile("proguard-android-optimize.txt"), ... ) ... } } ... } ``` ### Groovy ```groovy android { buildTypes { release { // Enables code-related app optimization. minifyEnabled = true // Enables resource shrinking. shrinkResources = true // Default file with automatically generated optimization rules. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') ... } } } ``` ## Optimize resource shrinking for even smaller apps The 8.12.0 version of Android Gradle Plugin (AGP) introduces optimized resource shrinking, which aims to integrate resource and code optimization to create even smaller and faster apps. Before optimized resource shrinking, Android Asset Packaging Tool (AAPT2) generated keep rules that effectively treating resource shrinking separately from code, often retaining inaccessible code or resources that referenced each other. With optimized resource shrinking, resources are considered like a part of program code, forming the reference graph. When a collection of code or resources is not referenced, it is not protected by a keep rule, and can be removed. ### Enable optimized resource shrinking To turn on the new optimized resource shrinking pipeline for a version of AGP before 9.0.0, add the following to your project's `gradle.properties` file: android.r8.optimizedResourceShrinking=true If you are using AGP 9.0.0 or a newer version, you don't need to set `android.r8.optimizedResourceShrinking=true`. Optimized resource shrinking is automatically applied when `isShrinkResources = true` is enabled in your build configuration. ## Verify and configure R8 optimization settings To enable R8 to use its [full optimization capabilities](https://developer.android.com/topic/performance/app-optimization/full-mode), remove the following line from your project's `gradle.properties` file, if it exists: android.enableR8.fullMode=false # Remove this line from your codebase. Note that enabling app optimization makes stack traces difficult to understand, especially if R8 renames class or method names. To get stack traces that correctly correspond to your source code, see [Recover the original stack trace](https://developer.android.com/topic/performance/app-optimization/test-and-troubleshoot-the-optimization#recover-original-stack-trace). If R8 is enabled, you should also [create Startup Profiles](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations) for even better startup performance. If you enable app optimization and it causes errors, here are some strategies to fix them: - [Add keep rules](https://developer.android.com/topic/performance/app-optimization/add-keep-rules) to keep some code untouched. - [Adopt optimizations incrementally](https://developer.android.com/topic/performance/app-optimization/adopt-optimizations-incrementally). - Update your code to [use libraries that are better suited for optimization](https://developer.android.com/topic/performance/app-optimization/choose-libraries-wisely). > [!CAUTION] > **Caution:** Tools that replace or modify R8's output can negatively impact runtime performance. R8 is careful about including and testing many optimizations at the code level, in [DEX layout](https://developer.android.com/topic/performance/baselineprofiles/dex-layout-optimizations), and in correctly producing Baseline Profiles - other tools producing or modifying DEX files can break these optimizations, or otherwise regress performance. If you are interested in optimizing your build speed, see [Configure how R8 runs](https://developer.android.com/build/r8-execution-profiles) for information on how to configure R8 based on your environment. ## AGP and R8 version behavior changes The following table outlines the key features introduced in various versions of the Android Gradle Plugin (AGP) and the R8 compiler. | AGP version | Features introduced | |---|---| | 9.1 | **Classes repackaged by default:** R8 repackages classes (moving them to the unnamed package, at the top level) to compact DEX further, eliminating the need to specify `-repackageclasses` option. For information about how this works and how to opt out, see [global options](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). | | 9.0 | **Optimized resource shrinking:** Enabled by default (controlled using `android.r8.optimizedResourceShrinking`). [Optimized resource shrinking](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization#optimize-resource-shrinking) helps integrate resource shrinking with the code optimization pipeline, leading to smaller, faster apps. By optimizing both code and resource references simultaneously, it identifies and removes resources referenced exclusively from unused code. This is a significant improvement over the previous separate optimization processes. This is especially useful for apps that share substantial resources and code across different form factor verticals, with measured improvements of over 50% in app size. The resulting size reduction leads to smaller downloads, faster installations, and a better user experience with faster startup, improved rendering, and fewer ANRs. **Library rule filtering:** Support for global options (for example, `-dontobfuscate`) in library consumer rules has been dropped, and apps will filter them out. For more information, see [Add global options](https://developer.android.com/topic/performance/app-optimization/global-options). **Kotlin null checks:** Optimized by default (controlled using `-processkotlinnullchecks`). This version also introduced significant improvements in build speed. For more information, see [Global options for additional optimization](https://developer.android.com/topic/performance/app-optimization/global-options#global-options). **Optimize specific packages:** You can use `packageScope` to optimize specific packages. This is in experimental support. For more information, see [Optimize specified packages with `packageScope`](https://developer.android.com/topic/performance/app-optimization/optimize-specified-packages). **Optimized by default:** Support for `getDefaultProguardFile("proguard-android.txt")` has been dropped, because it includes `-dontoptimize`, which should be avoided. Instead, use `"proguard-android-optimize.txt"`. If you need to globally disable optimization in your app, [add the flag manually to a proguard file](https://developer.android.com/topic/performance/app-optimization/global-options#global-options-2). | | 8.12 | **Resource shrinking:** Initial support added (Off by default. Enable using `isShrinkResources`). Resource shrinking works in tandem with R8 to identify and remove unused resources effectively. **Logcat retracing:** Support for automatic retracing in the Android Studio [Logcat window](https://developer.android.com/studio/debug/logcat). | | 8.6 | **Improved retracing:** Includes filename and line number retracing by default for all `minSdk` levels (previously required `minSdk` 26+ in version 8.2). Updating R8 helps ensure that stack traces from obfuscated builds are readily and clearly readable. This version improves how line numbers and source files are mapped, making it easier for tools like the Android Studio Logcat to automatically retrace crashes to the original source code. | | 8.0 | **Full mode by default:** [R8 full mode](https://developer.android.com/topic/performance/app-optimization/full-mode) provides significantly more powerful optimization. It is enabled by default. You can opt out using `android.enableR8.fullMode=false`. | | 7.0 | **Full mode available:** Introduced as an opt-in feature using `android.enableR8.fullMode=true`. Full mode applies more powerful optimizations by making stricter assumptions about how your code uses reflection and other dynamic features. While it reduces app size and improves performance, it might require additional keep rules to prevent necessary code from being stripped. | ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org/ # This configuration is used by ktlint when spotless invokes it [*] # Most of the standard properties are supported indent_size=2 max_line_length=100 [*.{kt,kts}] ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma_on_call_site=true ktlint_function_naming_ignore_when_annotated_with=Composable,Test compose_allowed_composition_locals=LocalJetLimeStyle ================================================ FILE: .github/workflows/build.yml ================================================ name: Build on: push: branches: [ main ] pull_request: branches: [ main ] types: [ opened, synchronize, reopened, ready_for_review ] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: lint: name: Spotless check runs-on: macos-latest steps: - name: Check out code uses: actions/checkout@v6 - name: Set up JDK uses: actions/setup-java@v5.0.0 with: distribution: 'zulu' java-version: 21 - name: spotless run: ./gradlew spotlessCheck build: strategy: matrix: platform: [ android, ios, web-wasm, web-js, desktop ] include: - platform: android os: ubuntu-latest - platform: web-wasm os: ubuntu-latest - platform: web-js os: ubuntu-latest - platform: ios os: macos-latest - platform: desktop os: macos-latest runs-on: ${{ matrix.os }} steps: - name: Checkout code uses: actions/checkout@v6 - name: Java and Gradle set up if: matrix.platform != 'ios' uses: ./.github/workflows/setup/java-setup - name: iOS set up if: matrix.platform == 'ios' uses: ./.github/workflows/setup/ios-setup # Android - name: Grant execute permission for Android script if: matrix.platform == 'android' run: chmod +x ./scripts/build_android.sh - name: Build Android if: matrix.platform == 'android' run: ./scripts/build_android.sh # iOS - name: Grant execute permission for iOS script if: matrix.platform == 'ios' run: chmod +x ./scripts/build_ios.sh - name: Build iOS if: matrix.platform == 'ios' run: ./scripts/build_ios.sh # Web-WASM - name: Grant execute permission for Web WASM script if: matrix.platform == 'web-wasm' run: chmod +x ./scripts/build_web_wasm.sh - name: Build Web WASM if: matrix.platform == 'web-wasm' run: ./scripts/build_web_wasm.sh # Web-JS - name: Grant execute permission for Web JS script if: matrix.platform == 'web-js' run: chmod +x ./scripts/build_web_js.sh - name: Build Web JS if: matrix.platform == 'web-js' run: ./scripts/build_web_js.sh # MacOS - name: Grant execute permission for MacOS script if: matrix.platform == 'desktop' run: chmod +x ./scripts/build_macos.sh - name: Build MacOS if: matrix.platform == 'desktop' run: ./scripts/build_macos.sh ================================================ FILE: .github/workflows/publish.yml ================================================ name: Publish to Maven Central on: workflow_dispatch: inputs: version: description: 'Version to publish (e.g., 4.2.0)' required: true type: string jobs: publish: name: Release and Publish runs-on: macos-latest permissions: contents: write steps: - name: Check out code uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Java and Gradle uses: ./.github/workflows/setup/java-setup - name: Update version in project files shell: bash run: | VERSION="${{ github.event.inputs.version }}" # Update coordinates in jetlime/build.gradle.kts sed -i '' "s/coordinates(\"io.github.pushpalroy\", artifactId, \".*\")/coordinates(\"io.github.pushpalroy\", artifactId, \"$VERSION\")/g" jetlime/build.gradle.kts # Update cocoapods version in jetlime/build.gradle.kts sed -i '' "/cocoapods {/,/}/ s/version = \".*\"/version = \"$VERSION\"/" jetlime/build.gradle.kts # Update podspec version sed -i '' "s/spec.version = '.*'/spec.version = '$VERSION'/g" jetlime/jetlime.podspec # Update git tag script sed -i '' "s/TAG=\".*\"/TAG=\"$VERSION\"/g" scripts/add_git_tag.sh # Update README installation snippet sed -i '' "s/implementation(\"io.github.pushpalroy:jetlime:.*\")/implementation(\"io.github.pushpalroy:jetlime:$VERSION\")/g" README.md # Update commented-out maven testing snippet in sample app sed -i '' "s|// implementation(\"io.github.pushpalroy:jetlime:.*\")|// implementation(\"io.github.pushpalroy:jetlime:$VERSION\")|g" sample/composeApp/build.gradle.kts - name: Publish to Maven Central shell: bash env: ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} run: | ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache - name: Generate Dokka API docs shell: bash run: | ./scripts/run_dokka.sh - name: Commit version updates shell: bash run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" git add . git commit -m "Bump version to ${{ github.event.inputs.version }}" git push origin main - name: Create and push git tag shell: bash run: | VERSION="${{ github.event.inputs.version }}" git tag -a "$VERSION" -m "Release $VERSION" git push origin "$VERSION" - name: Create GitHub release shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ github.event.inputs.version }}" gh release create "$VERSION" \ --title "v$VERSION" \ --target main \ --generate-notes ================================================ FILE: .github/workflows/setup/ios-setup/action.yml ================================================ name: iOS set up description: Sets up Kotlin Native and Cocoapods runs: using: "composite" steps: - shell: bash run: ./gradlew :sample:composeApp:generateDummyFramework - name: Set up cocoapods uses: maxim-lobanov/setup-cocoapods@v1 with: version: latest - shell: bash name: Install Dependencies run: | cd sample/iosApp pod install --verbose ================================================ FILE: .github/workflows/setup/java-setup/action.yml ================================================ name: Java and Gradle Job Setup description: Sets up Java and Gradle runs: using: "composite" steps: # Setup java - name: Setup JDK 21 id: setup_jdk uses: actions/setup-java@v5.0.0 with: distribution: 'zulu' java-version: 21 # Grant execute permission for gradlew - name: Grant execute permission for gradlew id: grant_gradle_permission shell: bash run: chmod +x gradlew # Caching gradle packages # TODO: remove temporary workaround after fixed # temporarily work around https://github.com/actions/runner-images/issues/13341 # by disabling caching for macOS - if: ${{ runner.os != 'macOS' }} uses: actions/cache@v4 name: Cache Gradle for quicker builds id: caching_gradle with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} restore-keys: | ${{ runner.os }}-gradle- # Setup Gradle - name: Set up Gradle id: set_up_gradle uses: gradle/actions/setup-gradle@v4 ================================================ FILE: .gitignore ================================================ # Built application files *.apk *.aar *.ap_ *.aab # Files for the ART/Dalvik VM *.dex # Java class files *.class # Kotlin libraries .kotlin/ app/ distributions/ # Generated files bin/ gen/ out/ # Uncomment the following line in case you need and you don't have the release build type files in your app # release/ # Gradle files .gradle/ build/ # Local configuration file (sdk path, etc) local.properties keystore_release.properties # Proguard folder generated by Eclipse proguard/ # Log Files *.log # Android Studio Navigation editor temp files .navigation/ # Android Studio captures folder captures/ # IntelliJ *.iml .idea/ # Keystore files # Uncomment the following lines if you do not want to check your keystore files in. #*.jks #*.keystore # External native build folder generated in Android Studio 2.2 and later .externalNativeBuild .cxx/ # Google Services (e.g. APIs or Firebase) # google-services.json # Freeline freeline.py freeline/ freeline_project_description.json # fastlane fastlane/report.xml fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md # Version control vcs.xml # lint lint/intermediates/ lint/generated/ lint/outputs/ lint/tmp/ # lint/reports/ ================================================ FILE: CLAUDE.md ================================================ # CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Project Overview JetLime is a Kotlin Multiplatform (KMP) Compose Multiplatform library that renders customizable timeline UIs. Targets: Android, iOS (x64/arm64/simulator), Desktop (JVM), JS (IR), WasmJs. The published artifact is `io.github.pushpalroy:jetlime`. The repo has two Gradle modules: - `:jetlime` — the library (all platform source in `src/commonMain/kotlin/com/pushpal/jetlime/`; Android-specific manifest + instrumented tests in `src/androidMain` and `src/androidTest`). - `:sample:composeApp` — a sample app that consumes `:jetlime` via `implementation(project(":jetlime"))` and runs on all five targets (Android, iOS via CocoaPods, Desktop, Web-JS, Web-WASM). JDK 17 is required for builds; CI uses JDK 21 only for the Spotless lint job. Kotlin 2.3.20, Compose Multiplatform 1.10.3, `androidTarget` compileSdk 36 / minSdk 23. ## Common Commands Format / lint (required before PR — CI runs `spotlessCheck`): ``` ./gradlew spotlessApply # auto-fix ./gradlew spotlessCheck # verify ``` Spotless applies ktlint + `io.nlopez.compose.rules:ktlint` with a mandatory MIT license header from `spotless/copyright.kt`. `.editorconfig` enforces 2-space indent, `max_line_length=100`, trailing commas, and allow-lists `LocalJetLimeStyle` for the Compose ktlint rule (`compose_allowed_composition_locals`). Library tests (Compose UI tests — the real ones live in `jetlime/src/androidTest/`, not `src/test/`): ``` ./gradlew :jetlime:connectedAndroidTest # all instrumented tests (needs emulator/device) ./gradlew :jetlime:connectedAndroidTest --tests "com.pushpal.jetlime.JetLimeColumnTest.jetLimeColumn_displaysItems" ``` Sample app per-platform builds (wrap the right gradle tasks and copy outputs to `distributions/`): ``` ./scripts/build_android.sh # :sample:composeApp:assembleDebug ./scripts/build_ios.sh # xcodebuild on sample/iosApp/iosApp.xcworkspace ./scripts/build_macos.sh # :sample:composeApp:packageUberJarForCurrentOS ./scripts/build_web_js.sh # :sample:composeApp:jsBrowserDistribution ./scripts/build_web_wasm.sh # :sample:composeApp:wasmJsBrowserDistribution ``` API docs (Dokka V2 — output is synced into the root `docs/` directory that GitHub Pages serves): ``` ./scripts/run_dokka.sh # wraps :jetlime:syncDokkaToDocs --no-configuration-cache ``` Publishing (see `gradle.properties` for required credentials — `mavenCentralUsername`, `signing.*`): ``` ./gradlew publishToMavenLocal # test locally via ~/.m2 ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache ``` To test a local publish, uncomment the `maven` coordinate in `sample/composeApp/build.gradle.kts` and add `mavenLocal()` to `settings.gradle.kts`. Compose compiler metrics/reports: ``` ./gradlew assembleRelease -PcomposeCompilerReports=true # outputs under jetlime/build/compose_compiler/ ``` ## Architecture The library is tiny (~11 files in `commonMain`) and built around three layers: 1. **List containers** (`JetLimeList.kt`) — `JetLimeColumn` / `JetLimeRow` wrap `LazyColumn` / `LazyRow`. They install a `JetLimeStyle` via the `LocalJetLimeStyle` `CompositionLocal` and compute an `EventPosition` (`START` / `MIDDLE` / `END`) for each index via `EventPosition.dynamic(index, listSize)`. The arrangement (`VERTICAL` vs `HORIZONTAL`) is stamped on the style here and is how `JetLimeEvent` dispatches to `VerticalEvent` vs `HorizontalEvent`. 2. **Events** (`JetLimeEvent.kt`, `JetLimeExtendedEvent.kt`) — `JetLimeEvent` is a single-slot composable that uses `Modifier.drawBehind` to paint the connecting line(s) and the point circle/icon. The per-item `JetLimeEventStyle` carries both the event's `EventPosition` (used to decide whether to draw the up/down or left/right connector segments via `isNotStart()` / `isNotEnd()`) and a `PointPlacement` (`START` / `CENTER` / `END`) that governs where the point sits within the item box and how the connector is split into two segments that meet at the point. `JetLimeExtendedEvent` (vertical-only) adds a second slot (`additionalContent`) rendered on the opposite side of the line; it uses a custom `Layout` and `BoxWithConstraints` capped by `JetLimeEventDefaults.AdditionalContentMaxWidth`. 3. **Style + defaults** — `JetLimeStyle` (list-level: line brush, thickness, `pathEffect`, `contentDistance`, `itemSpacing`, alignment) and `JetLimeEventStyle` (per-event point visuals) are `@Immutable`. `JetLimeDefaults` / `JetLimeEventDefaults` expose the `columnStyle()` / `rowStyle()` / `eventStyle()` / `pointAnimation()` / `lineGradientBrush()` / `lineSolidBrush()` factory helpers — always extend these rather than constructing style classes directly (the constructors are `internal`). ### Key drawing invariants - Line segments are drawn with Compose `drawLine`, branching on `PointPlacement` and `EventPosition`. For `CENTER`/`END` placement, the code draws two separate segments (start→point and point→end) and skips the relevant half at the first/last item. For `START` placement (default), a single segment extends from the point past the item box, with a `pointStartFactor = 1.1f` overdraw so adjacent items' lines visually join. - **RTL mirroring** is handled explicitly in `HorizontalEvent` and in `JetLimeExtendedEvent`. In horizontal RTL, the `xOffset` is flipped as `size.width - logicalXOffset` and segment start/end Xs swap sides. Extended vertical uses `LocalLayoutDirection` + `absolutePadding` so physical LEFT/RIGHT alignment is preserved regardless of layout direction. Any change to line/point drawing must keep both LTR and RTL visually correct — see the RTL tests in `JetLimeColumnTest` / `JetLimeRowTest`. - `VerticalAlignment.LEFT/RIGHT` and `HorizontalAlignment.TOP/BOTTOM` are *physical* sides (via `absolutePadding`), not start/end-relative. ## Release Housekeeping The library version appears in several places that must be kept in sync on release: - `jetlime/build.gradle.kts` — `mavenPublishing.coordinates(..., "X.Y.Z")` and `cocoapods { version = "X.Y.Z" }` - `jetlime/jetlime.podspec` - `scripts/add_git_tag.sh` — `TAG="X.Y.Z"` - `README.md` installation snippet After publishing, `scripts/add_git_tag.sh` creates and pushes the `X.Y.Z` git tag on `main`. ================================================ FILE: CONTRIBUTING.md ================================================ ## How to contribute We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. ## Preparing a pull request for review Ensure your change is properly formatted by running: ```gradle ./gradlew spotlessApply ``` Please correct any failures before requesting a review. ## Code reviews All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests. ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2022 Pushpal Roy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: PUBLISHING.md ================================================ # Publishing to Maven Central JetLime is published to Maven Central via a manual GitHub Actions workflow (`publish.yml`). Publishing is triggered on demand — there is no automatic release on push. --- ## One-time Setup ### 1. GPG Signing Key If you do not already have a GPG key, generate one: ```bash gpg --full-generate-key # Choose RSA, 4096 bits, no expiry, enter your name/email/passphrase ``` Publish your public key to a keyserver so Maven Central can verify it: ```bash gpg --list-keys --keyid-format=short # note the 8-char key ID, e.g. 1A2B3C4D gpg --keyserver keyserver.ubuntu.com --send-keys 1A2B3C4D ``` ### 2. Export the Signing Key for CI Vanniktech Maven Publish 0.36.0 requires the key body only — no armor headers, no newlines: ```bash gpg --armor --export-secret-keys 1A2B3C4D \ | tail -n +2 \ | grep -v "^-----END" \ | grep -v "^=" \ | tr -d '\n' ``` Copy the single-line output — this is your `SIGNING_IN_MEMORY_KEY` secret value. ### 3. Maven Central Credentials Log in to [central.sonatype.com](https://central.sonatype.com), click your avatar in the top-right corner, and choose **Generate User Token**. This gives you a username and password scoped for publishing. ### 4. Add GitHub Secrets Go to **GitHub → Repository → Settings → Secrets and variables → Actions → New repository secret** and add the following five secrets: | Secret name | Value | |---|---| | `MAVEN_CENTRAL_USERNAME` | Token username from Sonatype | | `MAVEN_CENTRAL_PASSWORD` | Token password from Sonatype | | `SIGNING_KEY_ID` | Short 8-char GPG key ID (e.g. `1A2B3C4D`) | | `SIGNING_IN_MEMORY_KEY` | Single-line output from the export command above | | `SIGNING_PASSWORD` | Passphrase used when creating the GPG key | `GITHUB_TOKEN` is provided automatically by GitHub Actions — do not add it manually. ### 5. Branch Protection (if enabled) The workflow commits the version bump directly to `main`. If branch protection requires pull requests, add `github-actions[bot]` as a bypass actor: **Settings → Branches → edit rule → Allow specified actors to bypass required pull requests** --- ## Releasing a New Version ### Step 1 — Update the version The workflow updates all version references automatically. The files it touches are: - `jetlime/build.gradle.kts` — `coordinates(...)` and `cocoapods { version }` - `jetlime/jetlime.podspec` — `spec.version` - `scripts/add_git_tag.sh` — `TAG=` - `README.md` — installation snippet ### Step 2 — Trigger the workflow 1. Go to **GitHub → Actions → Publish to Maven Central** 2. Click **Run workflow** 3. Enter the new version (e.g. `4.3.0`) and click **Run workflow** ### What the workflow does | Step | Action | |---|---| | Checkout | Fetches `main` with full history | | Setup | Installs JDK 21 and Gradle | | Update versions | Runs `sed` across all version-bearing files | | Publish | Runs `publishAndReleaseToMavenCentral` — signs, uploads, and auto-releases | | Commit | Commits the version bump files and pushes to `main` | | Tag | Creates and pushes an annotated git tag (e.g. `4.3.0`) | ### Step 3 — Verify Once the workflow completes (~5–10 min): - The new version appears on [central.sonatype.com](https://central.sonatype.com) under `io.github.pushpalroy:jetlime` - A git tag for the version is visible in the repository - `main` has a new commit: `Bump version to X.Y.Z` It can take up to 30 minutes for the artifact to appear in Maven search indexes. --- ## Testing a Publish Locally To verify signing and upload work before triggering CI: ```bash # Publish to your local ~/.m2 (no signing required) ./gradlew publishToMavenLocal # Full signed publish to Maven Central (requires credentials in gradle.properties or env) ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache ``` To consume the locally published artifact in the sample app, uncomment the `mavenLocal()` block in `settings.gradle.kts` and the `maven` coordinate in `sample/composeApp/build.gradle.kts`. ================================================ FILE: README.md ================================================ # JetLime 🍋 > A simple yet highly customizable UI library to show a timeline view in Compose Multiplatform. [![Jetbrains Compose](https://img.shields.io/badge/Jetbrains%20Compose-1.10.3-blue?style=for-the-badge&logo=appveyor)](https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-compatibility-and-versioning.html#jetpack-compose-artifacts-used) ![Kotlin](https://img.shields.io/badge/Kotlin-2.3.20-blue.svg?color=blue&style=for-the-badge) [![Maven Central](https://img.shields.io/maven-central/v/io.github.pushpalroy/jetlime?style=for-the-badge&logo=appveyor)](https://search.maven.org/artifact/io.github.pushpalroy/jetlime) ![Stars](https://img.shields.io/github/stars/pushpalroy/jetlime?color=yellowgreen&style=for-the-badge) ![Forks](https://img.shields.io/github/forks/pushpalroy/jetlime?color=yellowgreen&style=for-the-badge) ![Pull Request](https://img.shields.io/github/issues-pr/pushpalroy/jetlime?color=yellowgreen&style=for-the-badge) ![Watchers](https://img.shields.io/github/watchers/pushpalroy/jetlime?color=yellowgreen&style=for-the-badge) ![Issues](https://img.shields.io/github/issues/pushpalroy/jetlime?color=orange&style=for-the-badge) [![License](https://img.shields.io/github/license/pushpalroy/jetlime?color=blue&style=for-the-badge&logo=appveyor)](https://github.com/pushpalroy/jetlime/blob/master/LICENSE) ![Sample Build CI](https://img.shields.io/github/actions/workflow/status/pushpalroy/jetlime/build.yml?style=for-the-badge&label=Sample%20Build) ![badge][badge-android] ![badge][badge-ios] ![badge][badge-jvm] ![badge][badge-web] | Basic | Dashed | Dynamic | |:---------------------------------------:|------------------------------------------|:-----------------------------------------:| | | | | | Custom | Extended | | | | | | ### Supported Platform Samples | Android | iOS | Desktop | Web | |:------------------------------------------------:|----------------------------------------------|:-----------------------------------------------:|---------------------------------------------| | | | | | ## ✨ Highlights - Compose Multiplatform timelines: Android, iOS, Desktop (JVM), Web (JS & WASM) - Vertical and horizontal layouts (JetLimeColumn / JetLimeRow) - Flexible point placement: START, CENTER, END with continuous line joins - RTL layout support for JetLimeRow and JetLimeExtendedEvent (mirrors timelines and keeps content visible in right-to-left layouts) - Dashed/gradient/solid lines via Brush + PathEffect - Extended events with dual content slots (left/right), icons, and animations - Small, focused API with sensible defaults (JetLimeDefaults) ## 📦 Installation In `build.gradle` of shared module, include the following dependency ```gradle dependencies { implementation("io.github.pushpalroy:jetlime:4.3.0") } ``` ## 📖 Usage ### 📍 Add items in a Vertical Timeline Use the [JetLimeColumn](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-column.html) ```kotlin val items = remember { mutableListOf(Item1, Item2, Item3) } // Any type of items JetLimeColumn( modifier = Modifier.padding(16.dp), itemsList = ItemsList(items), key = { _, item -> item.id }, ) { index, item, position -> JetLimeEvent( style = JetLimeEventDefaults.eventStyle( position = position ), ) { // Content here } } ``` ### 📍 Add items in a Horizontal Timeline Use the [JetLimeRow](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-row.html) ```kotlin val items = remember { mutableListOf(Item1, Item2, Item3) } // Any type of items JetLimeRow( modifier = Modifier.padding(16.dp), itemsList = ItemsList(items), key = { _, item -> item.id }, ) { index, item, position -> JetLimeEvent( style = JetLimeEventDefaults.eventStyle( position = position ), ) { // Content here } } ``` Pass the `key` to define factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. This key will be used by a LazyColumn or LazyRow internally. If we want to to add items dynamically from a data source, we should use `mutableStateListOf`, so that our list can be observed as a state: ```kotlin val items = remember { mutableStateListOf() } ``` ### 🧩 Extended Events (Vertical Timeline) Use the [JetLimeExtendedEvent](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-extended-event.html) with a [JetLimeColumn](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-column.html) Using this we can pass an additional content to draw on the left side of the timeline. ```kotlin val items = remember { mutableListOf(Item1, Item2, Item3) } // Any type of items JetLimeColumn( modifier = Modifier.padding(16.dp), itemsList = ItemsList(items), key = { _, item -> item.id }, style = JetLimeDefaults.columnStyle(contentDistance = 24.dp), ) { index, item, position -> JetLimeExtendedEvent( style = JetLimeEventDefaults.eventStyle( position = position ), additionalContent = { // Additional content here } ) { // Content here } } ``` ### 🎛️ Customize `JetLimeColumn` Style Use the [JetLimeDefaults.columnStyle()](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-defaults/column-style.html) ```kotlin JetLimeColumn( style = JetLimeDefaults.columnStyle( contentDistance = 32.dp, itemSpacing = 16.dp, lineThickness = 2.dp, lineBrush = JetLimeDefaults.lineSolidBrush(color = Color(0xFF2196F3)), lineVerticalAlignment = RIGHT, ), ) { // Code to add events } ``` ### 🎛️ Customize `JetLimeRow` Style Use the [JetLimeDefaults.rowStyle()](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-defaults/row-style.html) ```kotlin JetLimeRow( style = JetLimeDefaults.rowStyle( contentDistance = 32.dp, itemSpacing = 16.dp, lineThickness = 2.dp, lineBrush = JetLimeDefaults.lineSolidBrush(color = Color(0xFF2196F3)), lineHorizontalAlignment = BOTTOM, ), ) { // Code to add events } ``` ### 🎛️ Customize `JetLimeEvent` Style Use the [JetLimeEventDefaults.eventStyle()](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/event-style.html) ```kotlin JetLimeEvent( style = JetLimeEventDefaults.eventStyle( position = position, pointColor = Color(0xFF2889D6), pointFillColor = Color(0xFFD5F2FF), pointRadius = 14.dp, pointAnimation = JetLimeEventDefaults.pointAnimation(), pointType = EventPointType.filled(0.8f), pointStrokeWidth = 2.dp, pointStrokeColor = MaterialTheme.colorScheme.onBackground, ), ) { // Code to add event content } ``` --- ### ⚙️ JetLimeColumn and JetLimeRow Properties #### 🧭 Alignment The timeline line and point circles can be set to either side. For a `JetLimeColumn` the alignment can be set to [LEFT](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-vertical-alignment/index.html#825393495%2FClasslikes%2F-1761194290) or [RIGHT](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-vertical-alignment/index.html#861885460%2FClasslikes%2F-1761194290) ```kotlin lineVerticalAlignment = LEFT or RIGHT // Default is LEFT ``` For a `JetLimeRow` the alignment can be set to [TOP](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-horizontal-alignment/index.html#769734623%2FClasslikes%2F-1761194290) or [BOTTOM](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-horizontal-alignment/index.html#-1737811223%2FClasslikes%2F-1761194290) ```kotlin lineHorizontalAlignment = TOP or BOTTOM // Default is TOP ``` #### 🎨 Line Style The line can be drawn by passing a `Brush` object to [lineBrush](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-style/line-brush.html) in a `columnStyle` or `rowStyle`. Default values can also be used from [JetLimeDefaults](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-defaults/index.html) and colors can be modified for quick setup: ```kotlin lineBrush = JetLimeDefaults.lineGradientBrush() or lineBrush = JetLimeDefaults.solidBrush() ``` A dashed/dotted line can also be drawn using the `pathEffect` property by passing a `PathEffect` to a `columnStyle` or `rowStyle`. ```kotlin style = JetLimeDefaults.columnStyle( pathEffect = PathEffect.dashPathEffect( intervals = floatArrayOf(30f, 30f), phase = 0f, ) ) ``` #### ↔️ Content Distance The [contentDistance](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-style/content-distance.html) in `Dp` specifies how far the timeline line should be from the timeline content. #### ↕️ Item Spacing The [itemSpacing](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-style/item-spacing.html) in `Dp` specifies the gap between the event items. #### 📏 Line Thickness The [lineThickness](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-style/line-thickness.html) in `Dp` the thickness of the timeline line. --- ### 🌍 RTL Layout Support JetLime supports right-to-left (RTL) layouts out of the box using Compose’s `LayoutDirection.Rtl`. - **Horizontal timelines (`JetLimeRow` + `JetLimeEvent`)** - The timeline direction is mirrored in RTL. - Start and end items are correctly connected. - Points and lines stay aligned without clipping, and the last item’s line joins cleanly. - **Extended vertical events (`JetLimeExtendedEvent` inside `JetLimeColumn`)** - Additional content remains fully visible on the side nearest the logical start. - Main content remains fully visible on the opposite side. - The timeline line and point stay between additional and main content without overlapping them. To preview RTL behavior in your app, wrap your content with a `CompositionLocalProvider`: ```kotlin CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Rtl) { JetLimeColumn( itemsList = ItemsList(items), key = { _, item -> item.id }, ) { index, item, position -> JetLimeExtendedEvent( style = JetLimeEventDefaults.eventStyle(position = position), additionalContent = { /* Additional content */ }, ) { // Main content } } } ``` | Basic (RTL) | Dynamic (RTL) | Extended (RTL) | |:-----------------------------------------:|---------------------------------------------|:--------------------------------------------:| | | | | ### ⚙️ JetLimeEvent Properties #### 📍 Position We always need to pass the position to the [eventStyle](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/event-style.html) that will be received in the JetLimeColumn lambda. This is needed so that JetLimeColumn can calculate the position of an event in the list at any time. Based on the calculation it will assign either of the three [EventPosition](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-event-position/index.html): `START`, `MIDDLE` or `END`. This classification is needed to render correct lines for start and end items. ```kotlin JetLimeColumn( itemsList = ItemsList(items), key = { _, item -> item.id }, ) { index, item, position -> JetLimeEvent( style = JetLimeEventDefaults.eventStyle( position = position ), ) { // Content here } } ``` #### 📌 Point Placement The `pointPlacement` of type [PointPlacement](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-point-placement/index.html) controls where the point renders within the item: - `START` – near the start edge (top for vertical, left for horizontal). Default. - `CENTER` – centered within the item. - `END` – near the end edge (bottom for vertical, right for horizontal). Examples: ```kotlin // All items CENTER JetLimeEventDefaults.eventStyle(position = position, pointPlacement = PointPlacement.CENTER) // All items END JetLimeEventDefaults.eventStyle(position = position, pointPlacement = PointPlacement.END) // Mixed: second-to-last CENTER, rest START JetLimeEventDefaults.eventStyle( position = position, pointPlacement = if (index == items.size - 2) PointPlacement.CENTER else PointPlacement.START, ) ``` Notes: - Lines connect continuously across START/CENTER/END, and stop cleanly at the last item’s point. - Works for both JetLimeColumn (vertical) and JetLimeRow (horizontal). #### 🟡 Point Type The `pointType` of type [EventPointType](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-event-point-type/index.html) specifies the style of the point circle. It can be any of the three types: `EMPTY`, `FILLED` or `CUSTOM`. For using `EMPTY` ```kotlin pointType = EventPointType.EMPTY ``` For using `FILLED`, the [filled()](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-event-point-type/-companion/index.html#-1342152058%2FFunctions%2F-1761194290) function has to be used which takes an optional `fillPercent` ```kotlin pointType = EventPointType.filled(0.8f) ``` For using `CUSTOM`, the [custom()](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-event-point-type/-companion/index.html#-2135258840%2FFunctions%2F-1761194290) function has to be used which takes an `icon` of `Painter`. This can be used to use a custom icon instead of the default types defined. An optional `tint` can also be applied on the icon. ```kotlin pointType = EventPointType.custom(icon = painterResource(id = R.drawable.icon_check), tint = Color.Green) ``` #### 🔁 Point Animation The `pointAnimation` of type [EventPointAnimation](https://pushpalroy.github.io/JetLime/jetlime/com.pushpal.jetlime/-event-point-animation/index.html) specifies the animation of the point circle. To enable the default animation ```kotlin pointAnimation = JetLimeEventDefaults.pointAnimation() ``` To use a custom animation `initialValue`, `targetValue` and `animationSpec` can be passed to `pointAnimation()`. `animationSpec` should be of the type `InfiniteRepeatableSpec`. #### 🎨 Point Color The `pointColor` is the color of the event point circle background. #### 🎨 Point Fill Color The `pointFillColor` is the fill color of the event point circle which is drawn over the `pointColor`. #### 📐 Point Radius The `pointRadius` in `Dp` is the radius of the point circle. #### 🖊️ Point Stroke Width The `pointStrokeWidth` in `Dp` is the width of the circle border. #### 🖍️ Point Stroke Color The `pointStrokeColor` is the color of the circle border. ## 📚 Documentation The full API documentation is available here: [JetLime Documentation](https://pushpalroy.github.io/JetLime/index.html) ## 💡 Inspiration - [Timeline-View by Vipul Asri](https://github.com/vipulasri/Timeline-View) - [This amazing blog by Vita Sokolova on Timeline component with Jetpack Compose](https://proandroiddev.com/a-step-by-step-guide-to-building-a-timeline-component-with-jetpack-compose-358a596847cb) ## 🤝 Contribution Would love to receive contributions! Read [contribution guidelines](CONTRIBUTING.md) for more information regarding contribution. ## 💬 Discuss? Have any questions, doubts or want to present your opinions, views? You're always welcome. You can [start discussions](https://github.com/pushpalroy/jetlime/discussions). ## 📝 License ``` MIT License Copyright (c) 2024 Pushpal Roy Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` [badge-android]: http://img.shields.io/badge/Android-grey?style=for-the-badge&logo=appveyor [badge-ios]: http://img.shields.io/badge/iOS-grey?style=for-the-badge&logo=appveyor [badge-jvm]: http://img.shields.io/badge/JVM-grey?style=for-the-badge&logo=appveyor [badge-web]: http://img.shields.io/badge/Web-grey?style=for-the-badge&logo=appveyor ================================================ FILE: build.gradle.kts ================================================ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.jetbrains.compose) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlin.multiplatform) apply false alias(libs.plugins.nexus.vanniktech.publish) apply false alias(libs.plugins.dokka) apply false alias(libs.plugins.spotless) apply false alias(libs.plugins.kotlin.cocoapods) apply false alias(libs.plugins.compose.compiler.report.generator) apply false } // Compose Compiler Metrics // Run ./gradlew assembleRelease -PcomposeCompilerReports=true to generate reports // https://github.com/androidx/androidx/blob/androidx-main/compose/compiler/design/compiler-metrics.md#enabling-metrics subprojects { apply(plugin = "com.diffplug.spotless") configure { kotlin { target("**/*.kt") targetExclude("${layout.buildDirectory.get()}/**/*.kt") targetExclude("bin/**/*.kt") ktlint() .setEditorConfigPath("$rootDir/.editorconfig") .editorConfigOverride( mapOf( "indent_size" to "2", "continuation_indent_size" to "2", ), ) .customRuleSets( listOf( "io.nlopez.compose.rules:ktlint:0.3.11", ), ) licenseHeaderFile(rootProject.file("spotless/copyright.kt")) trimTrailingWhitespace() endWithNewline() } kotlinGradle { target("*.gradle.kts") ktlint() } } } ================================================ FILE: docs/index.html ================================================ jetlime

jetlime

JetLime Module Documentation

JetLime is a Kotlin Multiplatform timeline UI library for Compose.

Overview

JetLime provides composables for vertical and horizontal timelines, customizable points (START, CENTER, END), and extended events with dual content areas.

Refer to the project README for installation and quick start examples.

Packages

Link copied to clipboard
common

Contains composables, styles, timeline rendering, and helper utilities.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-animation/animation-spec.html ================================================ animationSpec

animationSpec

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-animation/equals.html ================================================ equals

equals

open operator override fun equals(other: Any?): Boolean

Compares this EventPointAnimation with another object for equality. The result is true if and only if the argument is not null, is an EventPointAnimation object, and has the same values for initialValue, targetValue, and animationSpec.

Return

true if the given object represents an EventPointAnimation equivalent to this instance, false otherwise.

Parameters

other

The object to compare this EventPointAnimation against.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-animation/hash-code.html ================================================ hashCode

hashCode

open override fun hashCode(): Int

Returns a hash code value for the object, consistent with the definition of equality for the class. This method is supported for the benefit of hash tables such as those provided by HashMap.

Return

A hash code value for this object.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-animation/index.html ================================================ EventPointAnimation

EventPointAnimation

Represents an animation configuration for an event point in a UI component. This animation defines how an event point (circle) should animate, including its initial value, target value, and the specification of the animation.

Properties

Link copied to clipboard

The specification of the animation, including duration, easing, and repeat behavior.

Link copied to clipboard

The starting value of the animation.

Link copied to clipboard

The ending value of the animation.

Functions

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean

Compares this EventPointAnimation with another object for equality. The result is true if and only if the argument is not null, is an EventPointAnimation object, and has the same values for initialValue, targetValue, and animationSpec.

Link copied to clipboard
open override fun hashCode(): Int

Returns a hash code value for the object, consistent with the definition of equality for the class. This method is supported for the benefit of hash tables such as those provided by HashMap.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-animation/initial-value.html ================================================ initialValue

initialValue

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-animation/target-value.html ================================================ targetValue

targetValue

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/-companion/-default.html ================================================ Default

Default

The default value of EventPointType

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/-companion/-e-m-p-t-y.html ================================================ EMPTY

EMPTY

Represents an empty event point type.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/-companion/custom.html ================================================ custom

custom

fun custom(icon: Painter, tint: Color? = null): EventPointType

Creates a custom event point type with a specified Painter icon.

Return

A new instance of EventPointType with the custom icon.

Parameters

icon

The Painter icon for the custom event point type.

tint

The optional tint Color of the custom icon.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/-companion/filled.html ================================================ filled

filled

fun filled(fillPercent: Float = 0.5f): EventPointType

Creates a filled event point type with a specified fill percentage.

Return

A new instance of EventPointType with the filled icon.

Parameters

fillPercent

The percentage of fill for the event point.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/-companion/index.html ================================================ Companion

Companion

object Companion

Properties

Link copied to clipboard

The default value of EventPointType

Link copied to clipboard

Represents an empty event point type.

Functions

Link copied to clipboard
fun custom(icon: Painter, tint: Color? = null): EventPointType

Creates a custom event point type with a specified Painter icon.

Link copied to clipboard
fun filled(fillPercent: Float = 0.5f): EventPointType

Creates a filled event point type with a specified fill percentage.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/equals.html ================================================ equals

equals

open operator override fun equals(other: Any?): Boolean

Checks if this instance is equal to another object. Two instances of EventPointType are considered equal if they have the same type and icon.

Return

true if the other object is an instance of EventPointType and has the same type and icon, false otherwise.

Parameters

other

The object to compare this instance with.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/fill-percent.html ================================================ fillPercent

fillPercent

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/hash-code.html ================================================ hashCode

hashCode

open override fun hashCode(): Int

Returns a hash code value for the object, which is consistent with the definition of equality for the class. This supports the use in hash tables, like those provided by HashMap.

Return

A hash code value for this object.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/icon.html ================================================ icon

icon

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/index.html ================================================ EventPointType

EventPointType

Represents a type of event point in a UI component with an optional icon. This class is used to define different types of event points such as empty, filled, or custom types with specific icons.

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard

An optional percentage value for the FILLED type

Link copied to clipboard

An optional Painter icon associated with the event point type.

Link copied to clipboard
val tint: Color?

An optional tint Color overlay for the icon.

Link copied to clipboard

The name of the event point type.

Functions

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean

Checks if this instance is equal to another object. Two instances of EventPointType are considered equal if they have the same type and icon.

Link copied to clipboard
open override fun hashCode(): Int

Returns a hash code value for the object, which is consistent with the definition of equality for the class. This supports the use in hash tables, like those provided by HashMap.

Link copied to clipboard

A helper function to check if the current EventPointType is CUSTOM

Link copied to clipboard

A helper function to check if the current EventPointType is EMPTY or FILLED

Link copied to clipboard

A helper function to check if the current EventPointType is FILLED

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/is-custom.html ================================================ isCustom

isCustom

A helper function to check if the current EventPointType is CUSTOM

Return

true if the current EventPointType is custom, false otherwise.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/is-empty-or-filled.html ================================================ isEmptyOrFilled

isEmptyOrFilled

A helper function to check if the current EventPointType is EMPTY or FILLED

Return

true if the current EventPointType is empty or filled, false otherwise.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/is-filled.html ================================================ isFilled

isFilled

A helper function to check if the current EventPointType is FILLED

Return

true if the current EventPointType is filled, false otherwise.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/tint.html ================================================ tint

tint

val tint: Color?
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-point-type/type.html ================================================ type

type

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/-companion/dynamic.html ================================================ dynamic

dynamic

fun dynamic(index: Int, listSize: Int): EventPosition

Determines the event position dynamically based on the index and the size of the list.

Return

EventPosition corresponding to the index in the list.

Parameters

index

The index of the item in the list.

listSize

The total size of the list.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/-companion/index.html ================================================ Companion

Companion

object Companion

Functions

Link copied to clipboard
fun dynamic(index: Int, listSize: Int): EventPosition

Determines the event position dynamically based on the index and the size of the list.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/equals.html ================================================ equals

equals

open operator override fun equals(other: Any?): Boolean

Checks if this instance is equal to another object. Two instances of EventPosition are considered equal if they have the same name.

Return

true if the other object is an instance of EventPosition and has the same name, false otherwise.

Parameters

other

The object to compare this instance with.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/hash-code.html ================================================ hashCode

hashCode

open override fun hashCode(): Int

Returns a hash code value for the object, which is consistent with the definition of equality for the class. This supports the use in hash tables, like those provided by HashMap.

Return

A hash code value for this object.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/index.html ================================================ EventPosition

EventPosition

Represents a position of an event within a sequence, such as the start, middle, or end. This class encapsulates the logic for determining the position based on the index in a list.

Types

Link copied to clipboard
object Companion

Properties

Link copied to clipboard

The name of the event position.

Functions

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean

Checks if this instance is equal to another object. Two instances of EventPosition are considered equal if they have the same name.

Link copied to clipboard
open override fun hashCode(): Int

Returns a hash code value for the object, which is consistent with the definition of equality for the class. This supports the use in hash tables, like those provided by HashMap.

Link copied to clipboard

A helper function to check if the current position is not the end position. This can be useful for determining layout or drawing logic based on the position of an event.

Link copied to clipboard

Helper to check if current position is not the start.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/is-not-end.html ================================================ isNotEnd

isNotEnd

A helper function to check if the current position is not the end position. This can be useful for determining layout or drawing logic based on the position of an event.

Return

true if the current position is not the end, false otherwise.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/is-not-start.html ================================================ isNotStart

isNotStart

Helper to check if current position is not the start.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-event-position/name.html ================================================ name

name

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-horizontal-alignment/-b-o-t-t-o-m/index.html ================================================ BOTTOM

BOTTOM

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-horizontal-alignment/-t-o-p/index.html ================================================ TOP

TOP

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-horizontal-alignment/entries.html ================================================ entries

entries

Returns a representation of an immutable list of all enum entries, in the order they're declared.

This method may be used to iterate over the enum entries.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-horizontal-alignment/index.html ================================================ HorizontalAlignment

HorizontalAlignment

Enum representing the alignment of the timeline line and points for JetLimeRow.

Entries

Link copied to clipboard
Link copied to clipboard

Properties

Link copied to clipboard

Returns a representation of an immutable list of all enum entries, in the order they're declared.

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int

Functions

Link copied to clipboard

Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)

Link copied to clipboard

Returns an array containing the constants of this enum type, in the order they're declared.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-horizontal-alignment/value-of.html ================================================ valueOf

valueOf

Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)

Throws

if this enum type has no constant with the specified name

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-horizontal-alignment/values.html ================================================ values

values

Returns an array containing the constants of this enum type, in the order they're declared.

This method may be used to iterate over the constants.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-items-list/-items-list.html ================================================ ItemsList

ItemsList

constructor(items: List<T>)

Type Parameters

T

The type of elements in the list.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-items-list/equals.html ================================================ equals

equals

open operator override fun equals(other: Any?): Boolean

Compares this ItemsList object with another object for equality. The comparison checks whether the other object is also an ItemsList and contains the same items in the same order.

Return

true if the other object is an ItemsList with the same items, false otherwise.

Parameters

other

The object to compare with this ItemsList.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-items-list/hash-code.html ================================================ hashCode

hashCode

open override fun hashCode(): Int

Generates a hash code for this ItemsList. The hash code is generated based on the items in the list.

Return

The hash code value for this ItemsList.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-items-list/index.html ================================================ ItemsList

ItemsList

class ItemsList<T>(val items: List<T>)

An immutable list class that holds a list of items of type T.

This class provides an immutable wrapper around a standard list, ensuring that the contents cannot be modified after creation. It overrides equals and hashCode methods to provide proper equality checks and hash code generation based on the list contents.

Type Parameters

T

The type of elements in the list.

Constructors

Link copied to clipboard
constructor(items: List<T>)

Properties

Link copied to clipboard
val items: List<T>

The list of items contained in this ItemsList.

Functions

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean

Compares this ItemsList object with another object for equality. The comparison checks whether the other object is also an ItemsList and contains the same items in the same order.

Link copied to clipboard
open override fun hashCode(): Int

Generates a hash code for this ItemsList. The hash code is generated based on the items in the list.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-items-list/items.html ================================================ items

items

val items: List<T>
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-column.html ================================================ JetLimeColumn

JetLimeColumn

fun <T> JetLimeColumn(itemsList: ItemsList<T>, modifier: Modifier = Modifier, style: JetLimeStyle = JetLimeDefaults.columnStyle(), listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), key: (index: Int, item: T) -> Any? = null, itemContent: @Composable (index: Int, T, EventPosition) -> Unit)

A composable function that creates a vertical timeline interface with a list of items.

This function sets up a LazyColumn layout for displaying items in a vertical timeline format. It allows for customization of its appearance and behavior through various parameters.

Example usage:

val items = remember { getItemsList() }

JetLimeColumn(
itemsList = ItemsList(items),
key = { _, item -> item.id },
style = JetLimeDefaults.columnStyle(),
) { index, item, position ->
JetLimeEvent(
style = JetLimeEventDefaults.eventStyle(position = position)
) {
ComposableContent(item = item)
}
}

Parameters

itemsList

A list of items to be displayed in the JetLimeColumn.

modifier

A modifier to be applied to the LazyColumn.

style

The JetLime style configuration. Defaults to a predefined column style.

listState

The state object to be used for the LazyColumn.

contentPadding

The padding to apply to the content inside the LazyColumn.

key

A factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. Type of the key should be saveable via Bundle on Android. If null is passed the position in the list will represent the key.

itemContent

A composable lambda that takes an index, an item of type T, and an EventPosition to build each item's content.

Type Parameters

T

The type of items in the items list.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-defaults/column-style.html ================================================ columnStyle

columnStyle

fun columnStyle(contentDistance: Dp = ContentDistance, itemSpacing: Dp = ItemSpacing, lineThickness: Dp = LineThickness, lineBrush: Brush = lineSolidBrush(), pathEffect: PathEffect? = null, lineVerticalAlignment: VerticalAlignment = LEFT): JetLimeStyle

Creates a column style configuration for JetLimeColumn.

Return

A JetLimeStyle instance configured for column arrangement.

Parameters

contentDistance

The distance of content from the JetLime component's start.

itemSpacing

The spacing between items in the JetLime component.

lineThickness

The thickness of the line in the JetLime component.

lineBrush

The brush used for the line in the JetLime component.

pathEffect

the effect applied to the geometry of the timeline to obtain a dashed pattern.

lineVerticalAlignment

The vertical alignment of the line: LEFT or RIGHT

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-defaults/index.html ================================================ JetLimeDefaults

JetLimeDefaults

Provides default values and utility functions for JetLimeColumn or JetLimeRow styling.

This object contains default values and composable functions that create different types of brushes and a default JetLimeStyle. It acts as a utility provider for the JetLimeColumn or JetLimeRow component styling, allowing for consistent default styling across the application.

Functions

Link copied to clipboard
fun columnStyle(contentDistance: Dp = ContentDistance, itemSpacing: Dp = ItemSpacing, lineThickness: Dp = LineThickness, lineBrush: Brush = lineSolidBrush(), pathEffect: PathEffect? = null, lineVerticalAlignment: VerticalAlignment = LEFT): JetLimeStyle

Creates a column style configuration for JetLimeColumn.

Link copied to clipboard
fun lineGradientBrush(colors: ImmutableList<Color> = persistentListOf( MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.tertiary, ), start: Offset = Offset.Zero, end: Offset = Offset.Infinite, tileMode: TileMode = TileMode.Clamp): Brush

Creates a linear gradient brush for lines in JetLimeColumn or JetLimeRow components.

Link copied to clipboard
fun lineSolidBrush(color: Color = MaterialTheme.colorScheme.primary): Brush

Creates a solid color brush for lines in JetLimeColumn or JetLimeRow components.

Link copied to clipboard
fun rowStyle(contentDistance: Dp = ContentDistance, itemSpacing: Dp = ItemSpacing, lineThickness: Dp = LineThickness, lineBrush: Brush = lineSolidBrush(), pathEffect: PathEffect? = null, lineHorizontalAlignment: HorizontalAlignment = TOP): JetLimeStyle

Creates a row style configuration for JetLimeRow.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-defaults/line-gradient-brush.html ================================================ lineGradientBrush

lineGradientBrush

fun lineGradientBrush(colors: ImmutableList<Color> = persistentListOf( MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.secondary, MaterialTheme.colorScheme.tertiary, ), start: Offset = Offset.Zero, end: Offset = Offset.Infinite, tileMode: TileMode = TileMode.Clamp): Brush

Creates a linear gradient brush for lines in JetLimeColumn or JetLimeRow components.

Return

A Brush object representing a linear gradient.

Parameters

colors

The colors to be used in the gradient. Defaults to primary, secondary, and tertiary colors from MaterialTheme's color scheme.

start

The start offset for the gradient.

end

The end offset for the gradient.

tileMode

The tile mode for the gradient.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-defaults/line-solid-brush.html ================================================ lineSolidBrush

lineSolidBrush

fun lineSolidBrush(color: Color = MaterialTheme.colorScheme.primary): Brush

Creates a solid color brush for lines in JetLimeColumn or JetLimeRow components.

Return

A Brush object representing a solid color.

Parameters

color

The color to be used for the brush. Defaults to the primary color from MaterialTheme's color scheme.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-defaults/row-style.html ================================================ rowStyle

rowStyle

fun rowStyle(contentDistance: Dp = ContentDistance, itemSpacing: Dp = ItemSpacing, lineThickness: Dp = LineThickness, lineBrush: Brush = lineSolidBrush(), pathEffect: PathEffect? = null, lineHorizontalAlignment: HorizontalAlignment = TOP): JetLimeStyle

Creates a row style configuration for JetLimeRow.

Return

A JetLimeStyle instance configured for row arrangement.

Parameters

contentDistance

The distance of content from the JetLime component's start.

itemSpacing

The spacing between items in the JetLime component.

lineThickness

The thickness of the line in the JetLime component.

lineBrush

The brush used for the line in the JetLime component.

pathEffect

the effect applied to the geometry of the timeline to obtain a dashed pattern.

lineHorizontalAlignment

The horizontal alignment of the line: TOP or BOTTOM

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/event-style.html ================================================ eventStyle

eventStyle

fun eventStyle(position: EventPosition, pointPlacement: PointPlacement = PointPlacement.START, pointType: EventPointType = PointType, pointColor: Color = MaterialTheme.colorScheme.onPrimary, pointFillColor: Color = MaterialTheme.colorScheme.primary, pointRadius: Dp = PointRadius, pointAnimation: EventPointAnimation? = null, pointStrokeWidth: Dp = PointStrokeWidth, pointStrokeColor: Color = MaterialTheme.colorScheme.primary): JetLimeEventStyle

Creates a default JetLimeEventStyle object with specified parameters.

Return

A JetLimeEventStyle object configured with the given parameters.

Parameters

position

The position of the event relative to the timeline.

pointPlacement

Controls where the point renders within the item. One of PointPlacement.START, PointPlacement.CENTER, or PointPlacement.END. Defaults to START.

pointType

The type of point used in the event. Defaults to a filled point.

pointColor

The color of the point. Defaults to the 'onPrimary' color from MaterialTheme's color scheme.

pointFillColor

The fill color of the point. Defaults to the primary color from MaterialTheme's color scheme.

pointRadius

The radius of the point. Defaults to PointRadius.

pointAnimation

The animation for the point, if any.

pointStrokeWidth

The stroke width of the point. Defaults to PointStrokeWidth.

pointStrokeColor

The stroke color of the point. Defaults to the primary color from MaterialTheme's color scheme.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/index.html ================================================ JetLimeEventDefaults

JetLimeEventDefaults

Provides default values and utility functions for JetLimeEvent styling.

This object contains default values and composable functions for creating event styles and point animations in JetLimeColumn or JetLimeRow components. It offers a convenient way to access standard styling options and animations for JetLime events.

Functions

Link copied to clipboard
fun eventStyle(position: EventPosition, pointPlacement: PointPlacement = PointPlacement.START, pointType: EventPointType = PointType, pointColor: Color = MaterialTheme.colorScheme.onPrimary, pointFillColor: Color = MaterialTheme.colorScheme.primary, pointRadius: Dp = PointRadius, pointAnimation: EventPointAnimation? = null, pointStrokeWidth: Dp = PointStrokeWidth, pointStrokeColor: Color = MaterialTheme.colorScheme.primary): JetLimeEventStyle

Creates a default JetLimeEventStyle object with specified parameters.

Link copied to clipboard
fun pointAnimation(initialValue: Float = 1.0f, targetValue: Float = 1.2f, animationSpec: InfiniteRepeatableSpec<Float> = PointAnimation): EventPointAnimation

Creates an EventPointAnimation object to define animations for event points.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/point-animation.html ================================================ pointAnimation

pointAnimation

fun pointAnimation(initialValue: Float = 1.0f, targetValue: Float = 1.2f, animationSpec: InfiniteRepeatableSpec<Float> = PointAnimation): EventPointAnimation

Creates an EventPointAnimation object to define animations for event points.

Return

An EventPointAnimation object configured with the given parameters.

Parameters

initialValue

The initial value of the animation. Defaults to 1.0f.

targetValue

The target value of the animation. Defaults to 1.2f.

animationSpec

The specification for the animation. Defaults to PointAnimation.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/equals.html ================================================ equals

equals

open operator override fun equals(other: Any?): Boolean

Checks if this instance is equal to another object. Two instances of JetLimeEventStyle are considered equal if they have the same values for all properties.

Return

true if the other object is an instance of JetLimeEventStyle and has the same property values, false otherwise.

Parameters

other

The object to compare this instance with.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/hash-code.html ================================================ hashCode

hashCode

open override fun hashCode(): Int

Returns a hash code value for the object, consistent with the definition of equality for the class. This supports the use in hash tables, like those provided by HashMap.

Return

A hash code value for this object.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/index.html ================================================ JetLimeEventStyle

JetLimeEventStyle

Represents the style configuration for an event in a JetLimeEvent UI component. This class encapsulates various styling properties such as position, point type, colors, radius, animation, and stroke attributes for an event point.

Properties

Link copied to clipboard

Optional animation for the event point.

Link copied to clipboard

The color of the event point.

Link copied to clipboard

The fill color of the event point.

Link copied to clipboard

The placement of the point relative to the event content (START, CENTER, or END).

Link copied to clipboard

The radius of the event point.

Link copied to clipboard

The stroke color of the event point.

Link copied to clipboard

The stroke width of the event point.

Link copied to clipboard

The type of the event point: Empty, Filled or Custom.

Link copied to clipboard

The position of the event in the UI component.

Functions

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean

Checks if this instance is equal to another object. Two instances of JetLimeEventStyle are considered equal if they have the same values for all properties.

Link copied to clipboard
open override fun hashCode(): Int

Returns a hash code value for the object, consistent with the definition of equality for the class. This supports the use in hash tables, like those provided by HashMap.

Link copied to clipboard

Sets the placement of the point relative to the event content.

Link copied to clipboard

Sets the position of the JetLimeEvent.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-animation.html ================================================ pointAnimation

pointAnimation

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-color.html ================================================ pointColor

pointColor

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-fill-color.html ================================================ pointFillColor

pointFillColor

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-placement.html ================================================ pointPlacement

pointPlacement

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-radius.html ================================================ pointRadius

pointRadius

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-stroke-color.html ================================================ pointStrokeColor

pointStrokeColor

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-stroke-width.html ================================================ pointStrokeWidth

pointStrokeWidth

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-type.html ================================================ pointType

pointType

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/position.html ================================================ position

position

The position of the event in the UI component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/set-point-placement.html ================================================ setPointPlacement

setPointPlacement

Sets the placement of the point relative to the event content.

Return

A JetLimeEventStyle instance with the updated point placement.

Parameters

pointPlacement

The PointPlacement to use for drawing the point.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event-style/set-position.html ================================================ setPosition

setPosition

Sets the position of the JetLimeEvent.

This function allows for changing the position of a JetLime event. It modifies the current instance of JetLimeEventStyle, setting its position property to the specified EventPosition.

Return

A JetLimeEventStyle instance with the updated position.

Parameters

position

The EventPosition to set for the JetLime event.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-event.html ================================================ JetLimeEvent

JetLimeEvent

fun JetLimeEvent(modifier: Modifier = Modifier, style: JetLimeEventStyle = JetLimeEventDefaults.eventStyle(EventPosition.END), content: @Composable () -> Unit)

Composable function for creating a JetLimeColumn or JetLimeRow event.

Example usage:

val items = remember { getItemsList() }

JetLimeColumn(
itemsList = ItemsList(items),
key = { _, item -> item.id },
style = JetLimeDefaults.columnStyle(),
) { index, item, position ->
JetLimeEvent(
style = JetLimeEventDefaults.eventStyle(position = position)
) {
ComposableContent(item = item)
}
}

Parameters

modifier

The modifier to be applied to the event.

style

The style of the JetLimeColumn or JetLimeRow event, defaulting to JetLimeEventDefaults.eventStyle.

content

The composable content inside the event.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-extended-event.html ================================================ JetLimeExtendedEvent

JetLimeExtendedEvent

fun JetLimeExtendedEvent(modifier: Modifier = Modifier, style: JetLimeEventStyle = JetLimeEventDefaults.eventStyle(EventPosition.END), additionalContent: @Composable BoxScope.() -> Unit = { }, additionalContentMaxWidth: Dp = AdditionalContentMaxWidth, content: @Composable () -> Unit)

Should only be used with a JetLimeColumn for a vertical arrangement of events.

Composable function for creating a JetLimeColumn event which has 2 slots for content. The main content will be drawn on the right side of the timeline and the additional content will be drawn on the left side of the timeline. The additional content is optional, and has a maximum width constraint defined by the JetLimeEventDefaults.AdditionalContentMaxWidth.

Example usage:

val items = remember { getItemsList() }

JetLimeColumn(
itemsList = ItemsList(items),
key = { _, item -> item.id },
style = JetLimeDefaults.columnStyle(),
) { index, item, position ->
JetLimeExtendedEvent(
style = JetLimeEventDefaults.eventStyle(position = position),
additionalContent = { ComposableAdditionalContent(item.icon) }
) {
ComposableMainContent(item = item.content)
}
}

Parameters

modifier

The modifier to be applied to the event.

style

The style of the JetLimeColumn event, defaulting to JetLimeEventDefaults.eventStyle.

additionalContent

The optional additional content of the event, placed on the left side of timeline.

additionalContentMaxWidth

The maximum width allowed for additionalContent

content

The main content of the event, placed on the right side of timeline.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-row.html ================================================ JetLimeRow

JetLimeRow

fun <T> JetLimeRow(itemsList: ItemsList<T>, modifier: Modifier = Modifier, style: JetLimeStyle = JetLimeDefaults.rowStyle(), listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), key: (index: Int, item: T) -> Any? = null, itemContent: @Composable (index: Int, T, EventPosition) -> Unit)

A composable function that creates a horizontal timeline interface with a list of items.

This function sets up a LazyRow layout for displaying items in a horizontal timeline format. It allows for customization of its appearance and behavior through various parameters.

Example usage:

val items = remember { getItemsList() }

JetLimeRow(
itemsList = ItemsList(items),
key = { _, item -> item.id },
style = JetLimeDefaults.rowStyle(),
) { index, item, position ->
JetLimeEvent(
style = JetLimeEventDefaults.eventStyle(position = position)
) {
ComposableContent(item = item)
}
}

Parameters

itemsList

A list of items to be displayed in the JetLimeRow.

modifier

A modifier to be applied to the LazyRow.

style

The JetLime style configuration. Defaults to a predefined row style.

listState

The state object to be used for the LazyRow.

contentPadding

The padding to apply to the content inside the LazyRow.

key

A factory of stable and unique keys representing the item. Using the same key for multiple items in the list is not allowed. Type of the key should be saveable via Bundle on Android. If null is passed the position in the list will represent the key.

itemContent

A composable lambda that takes an index, an item of type T, and an EventPosition to build each item's content.

Type Parameters

T

The type of items in the items list.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/content-distance.html ================================================ contentDistance

contentDistance

Parameters

contentDistance

The distance of content from the start of the JetLime component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/equals.html ================================================ equals

equals

open operator override fun equals(other: Any?): Boolean

Checks if this JetLimeStyle is equal to another object.

Equality is determined based on the equality of content distance, item spacing, line thickness, line brush, and both horizontal and vertical alignment properties.

Return

true if the specified object is equal to this JetLimeStyle, false otherwise.

Parameters

other

The object to compare with this instance.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/hash-code.html ================================================ hashCode

hashCode

open override fun hashCode(): Int

Generates a hash code for this JetLimeStyle.

The hash code is a combination of content distance, item spacing, line thickness, line brush, and alignment properties.

Return

The hash code value for this JetLimeStyle.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/index.html ================================================ JetLimeStyle

JetLimeStyle

Represents the styling configuration for JetLimeColumn and JetLimeRow components.

This class encapsulates various properties that define the appearance and layout of JetLimeColumn and JetLimeRow components, such as content distance, item spacing, line thickness, and alignment properties. It provides a fluent API for modifying these properties.

Parameters

contentDistance

The distance of content from the start of the JetLime component.

itemSpacing

The spacing between items in the JetLime component.

lineThickness

The thickness of the line in the JetLime component.

lineBrush

The brush used for the line in the JetLime component.

pathEffect

the effect applied to the geometry of the timeline to obtain a dashed pattern.

lineHorizontalAlignment

The horizontal alignment of the line in the JetLime component.

lineVerticalAlignment

The vertical alignment of the line in the JetLime component.

Properties

Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard
Link copied to clipboard

Functions

Link copied to clipboard
open operator override fun equals(other: Any?): Boolean

Checks if this JetLimeStyle is equal to another object.

Link copied to clipboard
open override fun hashCode(): Int

Generates a hash code for this JetLimeStyle.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/item-spacing.html ================================================ itemSpacing

itemSpacing

Parameters

itemSpacing

The spacing between items in the JetLime component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/line-brush.html ================================================ lineBrush

lineBrush

Parameters

lineBrush

The brush used for the line in the JetLime component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/line-horizontal-alignment.html ================================================ lineHorizontalAlignment

lineHorizontalAlignment

Parameters

lineHorizontalAlignment

The horizontal alignment of the line in the JetLime component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/line-thickness.html ================================================ lineThickness

lineThickness

Parameters

lineThickness

The thickness of the line in the JetLime component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/line-vertical-alignment.html ================================================ lineVerticalAlignment

lineVerticalAlignment

Parameters

lineVerticalAlignment

The vertical alignment of the line in the JetLime component.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-jet-lime-style/path-effect.html ================================================ pathEffect

pathEffect

Parameters

pathEffect

the effect applied to the geometry of the timeline to obtain a dashed pattern.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-local-jet-lime-style.html ================================================ LocalJetLimeStyle

LocalJetLimeStyle

A CompositionLocal providing the current JetLimeStyle.

This is used to provide a default or overridden style configuration down the composition tree. Accessing this without a provider will result in an error, ensuring that the style is always defined when used within a composable context.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/-c-e-n-t-e-r/index.html ================================================ CENTER

CENTER

Point drawn centered relative to the event content box.

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/-e-n-d/index.html ================================================ END

END

Point drawn at the end edge (bottom for vertical, right for horizontal).

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/-s-t-a-r-t/index.html ================================================ START

START

Point drawn at the start edge (existing default behaviour).

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/entries.html ================================================ entries

entries

Returns a representation of an immutable list of all enum entries, in the order they're declared.

This method may be used to iterate over the enum entries.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/index.html ================================================ PointPlacement

PointPlacement

Defines the placement of the timeline point relative to the event content.

Entries

Link copied to clipboard

Point drawn at the start edge (existing default behaviour).

Link copied to clipboard

Point drawn centered relative to the event content box.

Link copied to clipboard

Point drawn at the end edge (bottom for vertical, right for horizontal).

Properties

Link copied to clipboard

Returns a representation of an immutable list of all enum entries, in the order they're declared.

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int

Functions

Link copied to clipboard

Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)

Link copied to clipboard

Returns an array containing the constants of this enum type, in the order they're declared.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/value-of.html ================================================ valueOf

valueOf

Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)

Throws

if this enum type has no constant with the specified name

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-point-placement/values.html ================================================ values

values

Returns an array containing the constants of this enum type, in the order they're declared.

This method may be used to iterate over the constants.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-vertical-alignment/-l-e-f-t/index.html ================================================ LEFT

LEFT

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-vertical-alignment/-r-i-g-h-t/index.html ================================================ RIGHT

RIGHT

Properties

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int
================================================ FILE: docs/jetlime/com.pushpal.jetlime/-vertical-alignment/entries.html ================================================ entries

entries

Returns a representation of an immutable list of all enum entries, in the order they're declared.

This method may be used to iterate over the enum entries.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-vertical-alignment/index.html ================================================ VerticalAlignment

VerticalAlignment

Enum representing the alignment of the timeline line and points for JetLimeColumn.

Entries

Link copied to clipboard
Link copied to clipboard

Properties

Link copied to clipboard

Returns a representation of an immutable list of all enum entries, in the order they're declared.

Link copied to clipboard
expect val name: String
Link copied to clipboard
expect val ordinal: Int

Functions

Link copied to clipboard

Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)

Link copied to clipboard

Returns an array containing the constants of this enum type, in the order they're declared.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-vertical-alignment/value-of.html ================================================ valueOf

valueOf

Returns the enum constant of this type with the specified name. The string must match exactly an identifier used to declare an enum constant in this type. (Extraneous whitespace characters are not permitted.)

Throws

if this enum type has no constant with the specified name

================================================ FILE: docs/jetlime/com.pushpal.jetlime/-vertical-alignment/values.html ================================================ values

values

Returns an array containing the constants of this enum type, in the order they're declared.

This method may be used to iterate over the constants.

================================================ FILE: docs/jetlime/com.pushpal.jetlime/index.html ================================================ com.pushpal.jetlime

Package-level declarations

JetLime Core Package

Contains composables, styles, timeline rendering, and helper utilities.

Main Types

  • JetLimeEvent, JetLimeExtendedEvent

  • JetLimeColumn, JetLimeRow

  • JetLimeEventStyle, JetLimeStyle

  • EventPosition, PointPlacement

See README for detailed usage samples.

Types

Link copied to clipboard

Represents an animation configuration for an event point in a UI component. This animation defines how an event point (circle) should animate, including its initial value, target value, and the specification of the animation.

Link copied to clipboard

Represents a type of event point in a UI component with an optional icon. This class is used to define different types of event points such as empty, filled, or custom types with specific icons.

Link copied to clipboard

Represents a position of an event within a sequence, such as the start, middle, or end. This class encapsulates the logic for determining the position based on the index in a list.

Link copied to clipboard

Enum representing the alignment of the timeline line and points for JetLimeRow.

Link copied to clipboard
class ItemsList<T>(val items: List<T>)

An immutable list class that holds a list of items of type T.

Link copied to clipboard

Provides default values and utility functions for JetLimeColumn or JetLimeRow styling.

Link copied to clipboard

Provides default values and utility functions for JetLimeEvent styling.

Link copied to clipboard

Represents the style configuration for an event in a JetLimeEvent UI component. This class encapsulates various styling properties such as position, point type, colors, radius, animation, and stroke attributes for an event point.

Link copied to clipboard

Represents the styling configuration for JetLimeColumn and JetLimeRow components.

Link copied to clipboard

Defines the placement of the timeline point relative to the event content.

Link copied to clipboard

Enum representing the alignment of the timeline line and points for JetLimeColumn.

Properties

Link copied to clipboard

A CompositionLocal providing the current JetLimeStyle.

Functions

Link copied to clipboard
fun <T> JetLimeColumn(itemsList: ItemsList<T>, modifier: Modifier = Modifier, style: JetLimeStyle = JetLimeDefaults.columnStyle(), listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), key: (index: Int, item: T) -> Any? = null, itemContent: @Composable (index: Int, T, EventPosition) -> Unit)

A composable function that creates a vertical timeline interface with a list of items.

Link copied to clipboard
fun JetLimeEvent(modifier: Modifier = Modifier, style: JetLimeEventStyle = JetLimeEventDefaults.eventStyle(EventPosition.END), content: @Composable () -> Unit)

Composable function for creating a JetLimeColumn or JetLimeRow event.

Link copied to clipboard
fun JetLimeExtendedEvent(modifier: Modifier = Modifier, style: JetLimeEventStyle = JetLimeEventDefaults.eventStyle(EventPosition.END), additionalContent: @Composable BoxScope.() -> Unit = { }, additionalContentMaxWidth: Dp = AdditionalContentMaxWidth, content: @Composable () -> Unit)

Should only be used with a JetLimeColumn for a vertical arrangement of events.

Link copied to clipboard
fun <T> JetLimeRow(itemsList: ItemsList<T>, modifier: Modifier = Modifier, style: JetLimeStyle = JetLimeDefaults.rowStyle(), listState: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(0.dp), key: (index: Int, item: T) -> Any? = null, itemContent: @Composable (index: Int, T, EventPosition) -> Unit)

A composable function that creates a horizontal timeline interface with a list of items.

================================================ FILE: docs/jetlime/package-list ================================================ $dokka.format:html-v1 $dokka.linkExtension:html $dokka.location:com.pushpal.jetlime////PointingToDeclaration/jetlime/com.pushpal.jetlime/index.html $dokka.location:com.pushpal.jetlime//JetLimeColumn/#com.pushpal.jetlime.ItemsList[TypeParam(bounds=[kotlin.Any?])]#androidx.compose.ui.Modifier#com.pushpal.jetlime.JetLimeStyle#androidx.compose.foundation.lazy.LazyListState#androidx.compose.foundation.layout.PaddingValues#kotlin.Function2[kotlin.Int,TypeParam(bounds=[kotlin.Any?]),kotlin.Any]?#kotlin.Function3[kotlin.Int,TypeParam(bounds=[kotlin.Any?]),com.pushpal.jetlime.EventPosition,kotlin.Unit]/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-column.html $dokka.location:com.pushpal.jetlime//JetLimeEvent/#androidx.compose.ui.Modifier#com.pushpal.jetlime.JetLimeEventStyle#kotlin.Function0[kotlin.Unit]/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event.html $dokka.location:com.pushpal.jetlime//JetLimeExtendedEvent/#androidx.compose.ui.Modifier#com.pushpal.jetlime.JetLimeEventStyle#kotlin.Function1[androidx.compose.foundation.layout.BoxScope,kotlin.Unit]#androidx.compose.ui.unit.Dp#kotlin.Function0[kotlin.Unit]/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-extended-event.html $dokka.location:com.pushpal.jetlime//JetLimeRow/#com.pushpal.jetlime.ItemsList[TypeParam(bounds=[kotlin.Any?])]#androidx.compose.ui.Modifier#com.pushpal.jetlime.JetLimeStyle#androidx.compose.foundation.lazy.LazyListState#androidx.compose.foundation.layout.PaddingValues#kotlin.Function2[kotlin.Int,TypeParam(bounds=[kotlin.Any?]),kotlin.Any]?#kotlin.Function3[kotlin.Int,TypeParam(bounds=[kotlin.Any?]),com.pushpal.jetlime.EventPosition,kotlin.Unit]/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-row.html $dokka.location:com.pushpal.jetlime//LocalJetLimeStyle/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-local-jet-lime-style.html $dokka.location:com.pushpal.jetlime/EventPointAnimation///PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-animation/index.html $dokka.location:com.pushpal.jetlime/EventPointAnimation/animationSpec/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-animation/animation-spec.html $dokka.location:com.pushpal.jetlime/EventPointAnimation/equals/#kotlin.Any?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-animation/equals.html $dokka.location:com.pushpal.jetlime/EventPointAnimation/hashCode/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-animation/hash-code.html $dokka.location:com.pushpal.jetlime/EventPointAnimation/initialValue/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-animation/initial-value.html $dokka.location:com.pushpal.jetlime/EventPointAnimation/targetValue/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-animation/target-value.html $dokka.location:com.pushpal.jetlime/EventPointType.Companion///PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/-companion/index.html $dokka.location:com.pushpal.jetlime/EventPointType.Companion/Default/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/-companion/-default.html $dokka.location:com.pushpal.jetlime/EventPointType.Companion/EMPTY/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/-companion/-e-m-p-t-y.html $dokka.location:com.pushpal.jetlime/EventPointType.Companion/custom/#androidx.compose.ui.graphics.painter.Painter#androidx.compose.ui.graphics.Color?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/-companion/custom.html $dokka.location:com.pushpal.jetlime/EventPointType.Companion/filled/#kotlin.Float/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/-companion/filled.html $dokka.location:com.pushpal.jetlime/EventPointType///PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/index.html $dokka.location:com.pushpal.jetlime/EventPointType/equals/#kotlin.Any?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/equals.html $dokka.location:com.pushpal.jetlime/EventPointType/fillPercent/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/fill-percent.html $dokka.location:com.pushpal.jetlime/EventPointType/hashCode/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/hash-code.html $dokka.location:com.pushpal.jetlime/EventPointType/icon/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/icon.html $dokka.location:com.pushpal.jetlime/EventPointType/isCustom/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/is-custom.html $dokka.location:com.pushpal.jetlime/EventPointType/isEmptyOrFilled/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/is-empty-or-filled.html $dokka.location:com.pushpal.jetlime/EventPointType/isFilled/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/is-filled.html $dokka.location:com.pushpal.jetlime/EventPointType/tint/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/tint.html $dokka.location:com.pushpal.jetlime/EventPointType/type/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-point-type/type.html $dokka.location:com.pushpal.jetlime/EventPosition.Companion///PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/-companion/index.html $dokka.location:com.pushpal.jetlime/EventPosition.Companion/dynamic/#kotlin.Int#kotlin.Int/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/-companion/dynamic.html $dokka.location:com.pushpal.jetlime/EventPosition///PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/index.html $dokka.location:com.pushpal.jetlime/EventPosition/equals/#kotlin.Any?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/equals.html $dokka.location:com.pushpal.jetlime/EventPosition/hashCode/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/hash-code.html $dokka.location:com.pushpal.jetlime/EventPosition/isNotEnd/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/is-not-end.html $dokka.location:com.pushpal.jetlime/EventPosition/isNotStart/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/is-not-start.html $dokka.location:com.pushpal.jetlime/EventPosition/name/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-event-position/name.html $dokka.location:com.pushpal.jetlime/HorizontalAlignment.BOTTOM///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-horizontal-alignment/-b-o-t-t-o-m/index.html $dokka.location:com.pushpal.jetlime/HorizontalAlignment.TOP///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-horizontal-alignment/-t-o-p/index.html $dokka.location:com.pushpal.jetlime/HorizontalAlignment///PointingToDeclaration/jetlime/com.pushpal.jetlime/-horizontal-alignment/index.html $dokka.location:com.pushpal.jetlime/HorizontalAlignment/entries/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-horizontal-alignment/entries.html $dokka.location:com.pushpal.jetlime/HorizontalAlignment/valueOf/#kotlin.String/PointingToDeclaration/jetlime/com.pushpal.jetlime/-horizontal-alignment/value-of.html $dokka.location:com.pushpal.jetlime/HorizontalAlignment/values/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-horizontal-alignment/values.html $dokka.location:com.pushpal.jetlime/ItemsList///PointingToDeclaration/jetlime/com.pushpal.jetlime/-items-list/index.html $dokka.location:com.pushpal.jetlime/ItemsList/ItemsList/#kotlin.collections.List[TypeParam(bounds=[kotlin.Any?])]/PointingToDeclaration/jetlime/com.pushpal.jetlime/-items-list/-items-list.html $dokka.location:com.pushpal.jetlime/ItemsList/equals/#kotlin.Any?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-items-list/equals.html $dokka.location:com.pushpal.jetlime/ItemsList/hashCode/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-items-list/hash-code.html $dokka.location:com.pushpal.jetlime/ItemsList/items/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-items-list/items.html $dokka.location:com.pushpal.jetlime/JetLimeDefaults///PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-defaults/index.html $dokka.location:com.pushpal.jetlime/JetLimeDefaults/columnStyle/#androidx.compose.ui.unit.Dp#androidx.compose.ui.unit.Dp#androidx.compose.ui.unit.Dp#androidx.compose.ui.graphics.Brush#androidx.compose.ui.graphics.PathEffect?#com.pushpal.jetlime.VerticalAlignment/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-defaults/column-style.html $dokka.location:com.pushpal.jetlime/JetLimeDefaults/lineGradientBrush/#kotlinx.collections.immutable.ImmutableList[androidx.compose.ui.graphics.Color]#androidx.compose.ui.geometry.Offset#androidx.compose.ui.geometry.Offset#androidx.compose.ui.graphics.TileMode/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-defaults/line-gradient-brush.html $dokka.location:com.pushpal.jetlime/JetLimeDefaults/lineSolidBrush/#androidx.compose.ui.graphics.Color/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-defaults/line-solid-brush.html $dokka.location:com.pushpal.jetlime/JetLimeDefaults/rowStyle/#androidx.compose.ui.unit.Dp#androidx.compose.ui.unit.Dp#androidx.compose.ui.unit.Dp#androidx.compose.ui.graphics.Brush#androidx.compose.ui.graphics.PathEffect?#com.pushpal.jetlime.HorizontalAlignment/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-defaults/row-style.html $dokka.location:com.pushpal.jetlime/JetLimeEventDefaults///PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/index.html $dokka.location:com.pushpal.jetlime/JetLimeEventDefaults/eventStyle/#com.pushpal.jetlime.EventPosition#com.pushpal.jetlime.PointPlacement#com.pushpal.jetlime.EventPointType#androidx.compose.ui.graphics.Color#androidx.compose.ui.graphics.Color#androidx.compose.ui.unit.Dp#com.pushpal.jetlime.EventPointAnimation?#androidx.compose.ui.unit.Dp#androidx.compose.ui.graphics.Color/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/event-style.html $dokka.location:com.pushpal.jetlime/JetLimeEventDefaults/pointAnimation/#kotlin.Float#kotlin.Float#androidx.compose.animation.core.InfiniteRepeatableSpec[kotlin.Float]/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-defaults/point-animation.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle///PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/index.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/equals/#kotlin.Any?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/equals.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/hashCode/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/hash-code.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointAnimation/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-animation.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointColor/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-color.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointFillColor/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-fill-color.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointPlacement/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-placement.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointRadius/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-radius.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointStrokeColor/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-stroke-color.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointStrokeWidth/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-stroke-width.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/pointType/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/point-type.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/position/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/position.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/setPointPlacement/#com.pushpal.jetlime.PointPlacement/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/set-point-placement.html $dokka.location:com.pushpal.jetlime/JetLimeEventStyle/setPosition/#com.pushpal.jetlime.EventPosition/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-event-style/set-position.html $dokka.location:com.pushpal.jetlime/JetLimeStyle///PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/index.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/contentDistance/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/content-distance.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/equals/#kotlin.Any?/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/equals.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/hashCode/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/hash-code.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/itemSpacing/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/item-spacing.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/lineBrush/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/line-brush.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/lineHorizontalAlignment/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/line-horizontal-alignment.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/lineThickness/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/line-thickness.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/lineVerticalAlignment/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/line-vertical-alignment.html $dokka.location:com.pushpal.jetlime/JetLimeStyle/pathEffect/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-jet-lime-style/path-effect.html $dokka.location:com.pushpal.jetlime/PointPlacement.CENTER///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-point-placement/-c-e-n-t-e-r/index.html $dokka.location:com.pushpal.jetlime/PointPlacement.END///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-point-placement/-e-n-d/index.html $dokka.location:com.pushpal.jetlime/PointPlacement.START///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-point-placement/-s-t-a-r-t/index.html $dokka.location:com.pushpal.jetlime/PointPlacement///PointingToDeclaration/jetlime/com.pushpal.jetlime/-point-placement/index.html $dokka.location:com.pushpal.jetlime/PointPlacement/entries/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-point-placement/entries.html $dokka.location:com.pushpal.jetlime/PointPlacement/valueOf/#kotlin.String/PointingToDeclaration/jetlime/com.pushpal.jetlime/-point-placement/value-of.html $dokka.location:com.pushpal.jetlime/PointPlacement/values/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-point-placement/values.html $dokka.location:com.pushpal.jetlime/VerticalAlignment.LEFT///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-vertical-alignment/-l-e-f-t/index.html $dokka.location:com.pushpal.jetlime/VerticalAlignment.RIGHT///PointingToDeclaration/{"org.jetbrains.dokka.links.EnumEntryDRIExtra":{"key":"org.jetbrains.dokka.links.EnumEntryDRIExtra"}}jetlime/com.pushpal.jetlime/-vertical-alignment/-r-i-g-h-t/index.html $dokka.location:com.pushpal.jetlime/VerticalAlignment///PointingToDeclaration/jetlime/com.pushpal.jetlime/-vertical-alignment/index.html $dokka.location:com.pushpal.jetlime/VerticalAlignment/entries/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-vertical-alignment/entries.html $dokka.location:com.pushpal.jetlime/VerticalAlignment/valueOf/#kotlin.String/PointingToDeclaration/jetlime/com.pushpal.jetlime/-vertical-alignment/value-of.html $dokka.location:com.pushpal.jetlime/VerticalAlignment/values/#/PointingToDeclaration/jetlime/com.pushpal.jetlime/-vertical-alignment/values.html com.pushpal.jetlime ================================================ FILE: docs/navigation.html ================================================ ================================================ FILE: docs/scripts/main.js ================================================ (()=>{var e={1817:e=>{e.exports=''},4811:e=>{e.exports=''},5742:e=>{e.exports=''},7112:e=>{e.exports=''},8420:e=>{e.exports=''},7004:e=>{e.exports=''},7222:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>s});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(5280),c=a()(o());c.i(l.A),c.push([e.id,'.avatar_d716 {\n display: inline-block;\n -o-object-fit: cover;\n object-fit: cover;\n -o-object-position: center;\n object-position: center;\n\n /* This is a "graceful degradation" fallback, while the real value is controlled by JS */\n\n border-radius: var(--ring-border-radius);\n}\n\n.subavatar_b10d {\n position: absolute;\n top: 15px;\n left: 27px;\n\n border: 1px var(--ring-content-background-color) solid;\n}\n\n.empty_a151 {\n display: inline-block;\n\n box-sizing: border-box;\n\n border: 1px solid var(--ring-borders-color);\n}\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/avatar/avatar.css"],names:[],mappings:"AAEA;EACE,qBAAqB;EACrB,oBAAiB;KAAjB,iBAAiB;EACjB,0BAAuB;KAAvB,uBAAuB;;EAEvB,wFAAwF;;EAExF,wCAAwC;AAC1C;;AAEA;EACE,kBAAkB;EAClB,SAAS;EACT,UAAU;;EAEV,sDAAsD;AACxD;;AAEA;EACE,qBAAqB;;EAErB,sBAAsB;;EAEtB,2CAA2C;AAC7C",sourcesContent:['@import "../global/variables.css";\n\n.avatar {\n display: inline-block;\n object-fit: cover;\n object-position: center;\n\n /* This is a "graceful degradation" fallback, while the real value is controlled by JS */\n\n border-radius: var(--ring-border-radius);\n}\n\n.subavatar {\n position: absolute;\n top: 15px;\n left: 27px;\n\n border: 1px var(--ring-content-background-color) solid;\n}\n\n.empty {\n display: inline-block;\n\n box-sizing: border-box;\n\n border: 1px solid var(--ring-borders-color);\n}\n'],sourceRoot:""}]),c.locals={avatar:"avatar_d716",subavatar:"subavatar_b10d",empty:"empty_a151"};const s=c},9892:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,'.heightS_b28d {\n --ring-button-height: 24px;\n --ring-button-font-size: var(--ring-font-size-smaller);\n}\n\n.heightM_dfd3 {\n --ring-button-height: 28px;\n --ring-button-font-size: var(--ring-font-size);\n}\n\n.heightL_a4d3 {\n --ring-button-height: 32px;\n --ring-button-font-size: var(--ring-font-size);\n}\n\n.button_aba4 {\n position: relative;\n\n display: inline-block;\n\n box-sizing: border-box;\n height: var(--ring-button-height);\n margin: 0;\n padding: 0 16px;\n\n cursor: pointer;\n transition: color var(--ring-ease), background-color var(--ring-ease), box-shadow var(--ring-ease);\n text-decoration: none;\n\n color: var(--ring-text-color);\n\n border: 0;\n border-radius: var(--ring-border-radius);\n outline: 0;\n background-color: var(--ring-content-background-color);\n box-shadow: inset 0 0 0 1px var(--ring-borders-color);\n\n font-family: var(--ring-font-family);\n font-size: var(--ring-button-font-size);\n\n line-height: var(--ring-button-height);\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.button_aba4:hover {\n transition: none;\n\n box-shadow: inset 0 0 0 1px var(--ring-border-hover-color);\n }}\n\n.button_aba4:active {\n transition: none;\n\n background-color: var(--ring-selected-background-color);\n box-shadow: inset 0 0 0 1px var(--ring-border-hover-color);\n }\n\n.button_aba4:focus-visible {\n transition: none;\n\n box-shadow: inset 0 0 0 1px var(--ring-border-hover-color), 0 0 0 1px var(--ring-border-hover-color);\n }\n\n.button_aba4.active_bbe6 {\n transition: none;\n\n background-color: var(--ring-hover-background-color);\n box-shadow: inset 0 0 0 1px var(--ring-main-color);\n }\n\n.button_aba4.active_bbe6:focus-visible {\n box-shadow: inset 0 0 0 2px var(--ring-main-color), 0 0 0 1px var(--ring-border-hover-color);\n }\n\n.button_aba4[disabled] {\n pointer-events: none;\n\n background-color: var(--ring-disabled-background-color);\n box-shadow: inset 0 0 0 1px var(--ring-border-disabled-color);\n }\n\n.button_aba4.active_bbe6[disabled] {\n background-color: var(--ring-disabled-selected-background-color);\n box-shadow: inset 0 0 0 1px var(--ring-border-selected-disabled-color);\n }\n\n.button_aba4[disabled],\n .button_aba4.withIcon_ef77[disabled] {\n color: var(--ring-disabled-color);\n }\n\n.button_aba4[disabled] .icon_e878 {\n color: var(--ring-icon-disabled-color);\n }\n\n.button_aba4::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n outline: 0;\n }\n\n.withIcon_ef77 {\n color: var(--ring-secondary-color);\n}\n\n.primary_ddae {\n color: var(--ring-white-text-color);\n background-color: var(--ring-main-color);\n box-shadow: none;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.primary_ddae:hover {\n transition: none;\n\n background-color: var(--ring-main-hover-color);\n box-shadow: none;\n }}\n\n.primary_ddae.withIcon_ef77,\n .primary_ddae.withIcon_ef77:active,\n .primary_ddae.withIcon_ef77.active_bbe6 {\n color: var(--ring-action-link-color);\n }\n\n.primary_ddae:focus-visible,\n .primary_ddae:active,\n .primary_ddae.active_bbe6 {\n background-color: var(--ring-button-primary-background-color);\n }\n\n.primary_ddae:active,\n .primary_ddae.active_bbe6 {\n box-shadow: inset 0 0 0 1px var(--ring-button-primary-border-color);\n }\n\n.primary_ddae[disabled] {\n background-color: var(--ring-disabled-background-color);\n box-shadow: inset 0 0 0 1px var(--ring-border-disabled-color);\n }\n\n.primary_ddae.loader_cbfc[disabled] {\n color: var(--ring-white-text-color);\n }\n\n.primary_ddae .loaderBackground_d9f5 {\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n\n border-radius: var(--ring-border-radius);\n }\n\n.primary_ddae .loaderBackground_d9f5::before {\n background-image:\n linear-gradient(\n to right,\n var(--ring-main-color),\n var(--ring-button-loader-background) 40%,\n var(--ring-main-color) 80%\n );\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.danger_bcea:hover {\n transition: none;\n }}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.danger_bcea:hover {\n box-shadow: inset 0 0 0 1px var(--ring-button-danger-hover-color);\n }}\n\n.danger_bcea,\n .danger_bcea.withIcon_ef77,\n .danger_bcea.withIcon_ef77:active,\n .danger_bcea.withIcon_ef77.active_bbe6,\n .danger_bcea.text_fc2a,\n .danger_bcea.text_fc2a:active,\n .danger_bcea.text_fc2a.active_bbe6 {\n color: var(--ring-error-color);\n }\n\n.danger_bcea:active,\n .danger_bcea.active_bbe6 {\n background-color: var(--ring-button-danger-active-color);\n }\n\n.danger_bcea:active,\n .danger_bcea.active_bbe6,\n .danger_bcea:focus-visible {\n box-shadow: inset 0 0 0 1px var(--ring-button-danger-hover-color);\n }\n\n.danger_bcea:focus-visible {\n transition: none;\n }\n\n.text_fc2a.text_fc2a,\n.withIcon_ef77.withIcon_ef77 {\n background-color: transparent;\n box-shadow: none;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.text_fc2a.text_fc2a:hover, .withIcon_ef77.withIcon_ef77:hover {\n transition: none;\n }}\n\n.text_fc2a.text_fc2a:active,\n .withIcon_ef77.withIcon_ef77:active,\n .text_fc2a.text_fc2a.active_bbe6,\n .withIcon_ef77.withIcon_ef77.active_bbe6 {\n background-color: transparent;\n box-shadow: none;\n }\n\n.text_fc2a.text_fc2a:focus-visible, .withIcon_ef77.withIcon_ef77:focus-visible {\n box-shadow: inset 0 0 0 2px var(--ring-border-hover-color);\n }\n\n.loader_cbfc.text_fc2a > .content_b2b8 {\n animation-name: text-loading_d1b4;\n animation-duration: 1200ms;\n animation-iteration-count: infinite;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.text_fc2a.text_fc2a:hover {\n background-color: transparent;\n box-shadow: none;\n}}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.withIcon_ef77:hover:not(:focus-visible) {\n background-color: transparent;\n box-shadow: none;\n}}\n\n.text_fc2a {\n color: var(--ring-action-link-color);\n}\n\n.inline_b4a2 {\n display: inline-block;\n\n margin: 0;\n padding: 0;\n\n font-size: var(--ring-font-size);\n}\n\n.withIcon_ef77 {\n padding: 0 8px;\n}\n\n.text_fc2a:active,\n .text_fc2a.active_bbe6 {\n color: var(--ring-link-hover-color);\n }\n\n.withIcon_ef77:active,\n .withIcon_ef77.active_bbe6 {\n color: var(--ring-action-link-color);\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.withIcon_ef77:hover {\n color: var(--ring-link-hover-color);\n}}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.text_fc2a:hover {\n color: var(--ring-link-hover-color);\n}}\n\n.icon_e878 {\n color: inherit;\n\n line-height: normal;\n}\n\n.icon_e878:not(:last-child) {\n margin-right: 4px;\n }\n\n.withNormalIcon_aaca .icon_e878 {\n transition: color var(--ring-ease);\n\n color: var(--ring-icon-color);\n}\n\n.withNormalIcon_aaca:active,\n.withNormalIcon_aaca.active_bbe6 {\n color: var(--ring-main-color);\n}\n\n.withNormalIcon_aaca:active .icon_e878, .withNormalIcon_aaca.active_bbe6 .icon_e878 {\n transition: none;\n\n color: inherit;\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.withNormalIcon_aaca:hover .icon_e878,\n.withDangerIcon_e3ca:hover .icon_e878 {\n transition: none;\n\n color: inherit;\n}}\n\n.withDangerIcon_e3ca .icon_e878,\n.withDangerIcon_e3ca:active .icon_e878 {\n color: var(--ring-icon-error-color);\n}\n\n.loader_cbfc {\n position: relative;\n z-index: 0;\n\n pointer-events: none;\n\n background-color: transparent;\n}\n\n.loaderBackground_d9f5 {\n position: absolute;\n z-index: -1;\n top: 1px;\n right: 1px;\n bottom: 1px;\n left: 1px;\n\n overflow: hidden;\n\n border-radius: var(--ring-border-radius-small);\n}\n\n.loaderBackground_d9f5::before {\n display: block;\n\n width: calc(100% + 64px);\n height: 100%;\n\n content: "";\n animation: progress_ed8f 1s linear infinite;\n\n background-image:\n linear-gradient(\n to right,\n var(--ring-content-background-color),\n var(--ring-selected-background-color) 40%,\n var(--ring-content-background-color) 80%\n );\n\n background-repeat: repeat;\n background-size: 64px;\n }\n\n.delayed_d562 .content_b2b8::after {\n content: "…";\n}\n\n.short_a07a {\n width: 32px;\n padding: 0;\n}\n\n.dropdownIcon_e982 {\n margin-right: -2px;\n\n margin-left: 2px;\n\n transition: color var(--ring-ease);\n\n color: var(--ring-icon-secondary-color);\n\n line-height: normal;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.button_aba4:hover .dropdownIcon_e982 {\n transition: none;\n\n color: var(--ring-main-color);\n}}\n\n@keyframes progress_ed8f {\n from {\n transform: translateX(-64px);\n }\n\n to {\n transform: translateX(0);\n }\n}\n\n@keyframes text-loading_d1b4 {\n 50% {\n opacity: 0.5;\n }\n}\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/button/button.css",""],names:[],mappings:"AAOA;EACE,0BAAoC;EACpC,sDAAsD;AACxD;;AAEA;EACE,0BAAsC;EACtC,8CAA8C;AAChD;;AAEA;EACE,0BAAoC;EACpC,8CAA8C;AAChD;;AAEA;EACE,kBAAkB;;EAElB,qBAAqB;;EAErB,sBAAsB;EACtB,iCAAc;EACd,SAAS;EACT,eAAyB;;EAEzB,eAAe;EACf,kGAAkG;EAClG,qBAAqB;;EAErB,6BAA6B;;EAE7B,SAAS;EACT,wCAAwC;EACxC,UAAU;EACV,sDAAsD;EACtD,qDAAmD;;EAEnD,oCAAoC;EACpC,uCAAuC;;EAEvC,sCAAmB;AA2DrB;;AC1GA,wGAAA;IAAA,iBAAA;;IAAA,2DAAA;GAAA,CAAA;;ADuDE;IACE,gBAAgB;;IAEhB,uDAAuD;IACvD,0DAAwD;EAC1D;;AAEA;IACE,gBAAgB;;IAEhB,oGAAkG;EACpG;;AAEA;IACE,gBAAgB;;IAEhB,oDAAoD;IACpD,kDAAgD;EAClD;;AAEA;IACE,4FAA4F;EAC9F;;AAEA;IACE,oBAAoB;;IAEpB,uDAAuD;IACvD,6DAA2D;EAC7D;;AAEA;IACE,gEAAgE;IAChE,sEAAoE;EACtE;;AAEA;;IAEE,iCAAiC;EACnC;;AAEA;IACE,sCAAsC;EACxC;;AAEA;IACE,UAAU;;IAEV,SAAS;IACT,UAAU;EACZ;;AAGF;EACE,kCAAkC;AACpC;;AAEA;EACE,mCAAmC;EACnC,wCAAwC;EACxC,gBAAgB;AAqDlB;;ACxKA,wGAAA;IAAA,iBAAA;;IAAA,+CAAA;IAAA,iBAAA;GAAA,CAAA;;AD4HE;;;IAGE,oCAAoC;EACtC;;AAEA;;;IAGE,6DAA6D;EAC/D;;AAEA;;IAEE,mEAAiE;EACnE;;AAEA;IACE,uDAAuD;IACvD,6DAA2D;EAC7D;;AAEA;IACE,mCAAmC;EACrC;;AAEA;IACE,MAAM;IACN,QAAQ;IACR,SAAS;IACT,OAAO;;IAEP,wCAAwC;EAW1C;;AATE;MACE;;;;;;SAMG;IACL;;ACtKJ,wGAAA;IAAA,iBAAA;GAAA,CAAA;;AAAA,wGAAA;IAAA,kEAAA;GAAA,CAAA;;AD2KE;;;;;;;IAOE,8BAA8B;EAChC;;AAEA;;IAEE,wDAAwD;EAC1D;;AAEA;;;IAIE,iEAA+D;EACjE;;AAEA;IAEE,gBAAgB;EAClB;;AAGF;;EAEE,6BAA6B;EAC7B,gBAAgB;AAelB;;ACzNA,wGAAA;IAAA,iBAAA;GAAA,CAAA;;ADgNE;;;;IAEE,6BAA6B;IAC7B,gBAAgB;EAClB;;AAEA;IACE,0DAA0D;EAC5D;;AAGF;EACE,iCAA4B;EAC5B,0BAA0B;EAC1B,mCAAmC;AACrC;;AC/NA,wGAAA;EAAA,8BAAA;EAAA,iBAAA;CAAA,CAAA;;AAAA,wGAAA;EAAA,8BAAA;EAAA,iBAAA;CAAA,CAAA;;AD2OA;EACE,oCAAoC;AACtC;;AAEA;EACE,qBAAqB;;EAErB,SAAS;EACT,UAAU;;EAEV,gCAAgC;AAClC;;AAEA;EACE,cAAe;AACjB;;AAGE;;IAEE,mCAAmC;EACrC;;AAIA;;IAEE,oCAAoC;EACtC;;ACvQF,wGAAA;EAAA,oCAAA;CAAA,CAAA;;AAAA,wGAAA;EAAA,oCAAA;CAAA,CAAA;;ADkRA;EACE,cAAc;;EAEd,mBAAmB;AAKrB;;AAHE;IACE,iBAA8B;EAChC;;AAGF;EACE,kCAAkC;;EAElC,6BAA6B;AAC/B;;AAEA;;EAEE,6BAA6B;AAO/B;;AALE;IACE,gBAAgB;;IAEhB,cAAc;EAChB;;AC1SF,wGAAA;;EAAA,iBAAA;;EAAA,eAAA;CAAA,CAAA;;ADoTA;;EAEE,mCAAmC;AACrC;;AAEA;EACE,kBAAkB;EAClB,UAAU;;EAEV,oBAAoB;;EAEpB,6BAA6B;AAC/B;;AAEA;EACE,kBAAkB;EAClB,WAAW;EACX,QAAQ;EACR,UAAU;EACV,WAAW;EACX,SAAS;;EAET,gBAAgB;;EAEhB,8CAA8C;AAsBhD;;AApBE;IACE,cAAc;;IAEd,wBAA+B;IAC/B,YAAY;;IAEZ,WAAW;IACX,2CAAsC;;IAEtC;;;;;;OAMG;;IAEH,yBAAyB;IACzB,qBAA4B;EAC9B;;AAGF;EACE,YAAY;AACd;;AAEA;EACE,WAAqB;EACrB,UAAU;AACZ;;AAEA;EACE,kBAAkB;;EAElB,gBAAgB;;EAEhB,kCAAkC;;EAElC,uCAAuC;;EAEvC,mBAAmB;AACrB;;ACvXA,wGAAA;EAAA,iBAAA;;EAAA,8BAAA;CAAA,CAAA;;AD+XA;EACE;IACE,4BAA4C;EAC9C;;EAEA;IACE,wBAAwB;EAC1B;AACF;;AAEA;EACE;IACE,YAAY;EACd;AACF",sourcesContent:['@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n@value button-shadow: inset 0 0 0 1px;\n@value height: var(--ring-button-height);\n@value loaderWidth: calc(unit * 8);\n\n.heightS {\n --ring-button-height: calc(unit * 3);\n --ring-button-font-size: var(--ring-font-size-smaller);\n}\n\n.heightM {\n --ring-button-height: calc(unit * 3.5);\n --ring-button-font-size: var(--ring-font-size);\n}\n\n.heightL {\n --ring-button-height: calc(unit * 4);\n --ring-button-font-size: var(--ring-font-size);\n}\n\n.button {\n position: relative;\n\n display: inline-block;\n\n box-sizing: border-box;\n height: height;\n margin: 0;\n padding: 0 calc(unit * 2);\n\n cursor: pointer;\n transition: color var(--ring-ease), background-color var(--ring-ease), box-shadow var(--ring-ease);\n text-decoration: none;\n\n color: var(--ring-text-color);\n\n border: 0;\n border-radius: var(--ring-border-radius);\n outline: 0;\n background-color: var(--ring-content-background-color);\n box-shadow: button-shadow var(--ring-borders-color);\n\n font-family: var(--ring-font-family);\n font-size: var(--ring-button-font-size);\n\n line-height: height;\n\n &:hover {\n transition: none;\n\n box-shadow: button-shadow var(--ring-border-hover-color);\n }\n\n &:active {\n transition: none;\n\n background-color: var(--ring-selected-background-color);\n box-shadow: button-shadow var(--ring-border-hover-color);\n }\n\n &:focus-visible {\n transition: none;\n\n box-shadow: button-shadow var(--ring-border-hover-color), 0 0 0 1px var(--ring-border-hover-color);\n }\n\n &.active {\n transition: none;\n\n background-color: var(--ring-hover-background-color);\n box-shadow: button-shadow var(--ring-main-color);\n }\n\n &:focus-visible.active {\n box-shadow: inset 0 0 0 2px var(--ring-main-color), 0 0 0 1px var(--ring-border-hover-color);\n }\n\n &[disabled] {\n pointer-events: none;\n\n background-color: var(--ring-disabled-background-color);\n box-shadow: button-shadow var(--ring-border-disabled-color);\n }\n\n &[disabled].active {\n background-color: var(--ring-disabled-selected-background-color);\n box-shadow: button-shadow var(--ring-border-selected-disabled-color);\n }\n\n &[disabled],\n &[disabled].withIcon {\n color: var(--ring-disabled-color);\n }\n\n &[disabled] .icon {\n color: var(--ring-icon-disabled-color);\n }\n\n &::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n outline: 0;\n }\n}\n\n.withIcon {\n color: var(--ring-secondary-color);\n}\n\n.primary {\n color: var(--ring-white-text-color);\n background-color: var(--ring-main-color);\n box-shadow: none;\n\n &:hover {\n transition: none;\n\n background-color: var(--ring-main-hover-color);\n box-shadow: none;\n }\n\n &.withIcon,\n &.withIcon:active,\n &.withIcon.active {\n color: var(--ring-action-link-color);\n }\n\n &:focus-visible,\n &:active,\n &.active {\n background-color: var(--ring-button-primary-background-color);\n }\n\n &:active,\n &.active {\n box-shadow: button-shadow var(--ring-button-primary-border-color);\n }\n\n &[disabled] {\n background-color: var(--ring-disabled-background-color);\n box-shadow: button-shadow var(--ring-border-disabled-color);\n }\n\n &[disabled].loader {\n color: var(--ring-white-text-color);\n }\n\n & .loaderBackground {\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n\n border-radius: var(--ring-border-radius);\n\n &::before {\n background-image:\n linear-gradient(\n to right,\n var(--ring-main-color),\n var(--ring-button-loader-background) 40%,\n var(--ring-main-color) 80%\n );\n }\n }\n}\n\n.danger {\n &,\n &.withIcon,\n &.withIcon:active,\n &.withIcon.active,\n &.text,\n &.text:active,\n &.text.active {\n color: var(--ring-error-color);\n }\n\n &:active,\n &.active {\n background-color: var(--ring-button-danger-active-color);\n }\n\n &:active,\n &.active,\n &:focus-visible,\n &:hover {\n box-shadow: button-shadow var(--ring-button-danger-hover-color);\n }\n\n &:focus-visible,\n &:hover {\n transition: none;\n }\n}\n\n.text.text,\n.withIcon.withIcon {\n background-color: transparent;\n box-shadow: none;\n\n &:hover {\n transition: none;\n }\n\n &:active,\n &.active {\n background-color: transparent;\n box-shadow: none;\n }\n\n &:focus-visible {\n box-shadow: inset 0 0 0 2px var(--ring-border-hover-color);\n }\n}\n\n.loader.text > .content {\n animation-name: text-loading;\n animation-duration: 1200ms;\n animation-iteration-count: infinite;\n}\n\n.text.text:hover {\n background-color: transparent;\n box-shadow: none;\n}\n\n.withIcon:hover:not(:focus-visible) {\n background-color: transparent;\n box-shadow: none;\n}\n\n.text {\n color: var(--ring-action-link-color);\n}\n\n.inline {\n display: inline-block;\n\n margin: 0;\n padding: 0;\n\n font-size: var(--ring-font-size);\n}\n\n.withIcon {\n padding: 0 unit;\n}\n\n.text {\n &:active,\n &.active {\n color: var(--ring-link-hover-color);\n }\n}\n\n.withIcon {\n &:active,\n &.active {\n color: var(--ring-action-link-color);\n }\n}\n\n.withIcon:hover {\n color: var(--ring-link-hover-color);\n}\n\n.text:hover {\n color: var(--ring-link-hover-color);\n}\n\n.icon {\n color: inherit;\n\n line-height: normal;\n\n &:not(:last-child) {\n margin-right: calc(unit * 0.5);\n }\n}\n\n.withNormalIcon .icon {\n transition: color var(--ring-ease);\n\n color: var(--ring-icon-color);\n}\n\n.withNormalIcon:active,\n.withNormalIcon.active {\n color: var(--ring-main-color);\n\n & .icon {\n transition: none;\n\n color: inherit;\n }\n}\n\n.withNormalIcon:hover .icon,\n.withDangerIcon:hover .icon {\n transition: none;\n\n color: inherit;\n}\n\n.withDangerIcon .icon,\n.withDangerIcon:active .icon {\n color: var(--ring-icon-error-color);\n}\n\n.loader {\n position: relative;\n z-index: 0;\n\n pointer-events: none;\n\n background-color: transparent;\n}\n\n.loaderBackground {\n position: absolute;\n z-index: -1;\n top: 1px;\n right: 1px;\n bottom: 1px;\n left: 1px;\n\n overflow: hidden;\n\n border-radius: var(--ring-border-radius-small);\n\n &::before {\n display: block;\n\n width: calc(100% + loaderWidth);\n height: 100%;\n\n content: "";\n animation: progress 1s linear infinite;\n\n background-image:\n linear-gradient(\n to right,\n var(--ring-content-background-color),\n var(--ring-selected-background-color) 40%,\n var(--ring-content-background-color) 80%\n );\n\n background-repeat: repeat;\n background-size: loaderWidth;\n }\n}\n\n.delayed .content::after {\n content: "…";\n}\n\n.short {\n width: calc(unit * 4);\n padding: 0;\n}\n\n.dropdownIcon {\n margin-right: -2px;\n\n margin-left: 2px;\n\n transition: color var(--ring-ease);\n\n color: var(--ring-icon-secondary-color);\n\n line-height: normal;\n}\n\n.button:hover .dropdownIcon {\n transition: none;\n\n color: var(--ring-main-color);\n}\n\n@keyframes progress {\n from {\n transform: translateX(calc(0 - loaderWidth));\n }\n\n to {\n transform: translateX(0);\n }\n}\n\n@keyframes text-loading {\n 50% {\n opacity: 0.5;\n }\n}\n',null],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,"button-shadow":"inset 0 0 0 1px",height:"var(--ring-button-height)",loaderWidth:"64px",heightS:"heightS_b28d",heightM:"heightM_dfd3",heightL:"heightL_a4d3",button:"button_aba4",active:"active_bbe6",withIcon:"withIcon_ef77",icon:"icon_e878",primary:"primary_ddae",loader:"loader_cbfc",loaderBackground:"loaderBackground_d9f5",danger:"danger_bcea",text:"text_fc2a",content:"content_b2b8","text-loading":"text-loading_d1b4",inline:"inline_b4a2",withNormalIcon:"withNormalIcon_aaca",withDangerIcon:"withDangerIcon_e3ca",progress:"progress_ed8f",delayed:"delayed_d562",short:"short_a07a",dropdownIcon:"dropdownIcon_e982"};const u=s},1866:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,".checkbox_dccf {\n position: relative;\n\n display: inline-block;\n\n text-align: left;\n\n color: var(--ring-text-color);\n outline: none;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.checkbox_dccf:hover .cell_edda {\n transition: background-color var(--ring-ease);\n\n border-color: var(--ring-border-hover-color);\n }}\n\n.cell_edda {\n position: relative;\n top: -2px;\n\n display: inline-block;\n\n box-sizing: border-box;\n width: 14px;\n height: 14px;\n\n -webkit-user-select: none;\n\n -moz-user-select: none;\n\n user-select: none;\n transition: border-color var(--ring-ease), background-color var(--ring-ease), box-shadow var(--ring-ease);\n vertical-align: middle;\n pointer-events: none;\n\n border: 1px solid var(--ring-borders-color);\n border-radius: var(--ring-border-radius-small);\n background-color: var(--ring-content-background-color);\n}\n\n.icon_b476.icon_b476 {\n position: absolute;\n\n top: -1px;\n left: -1px;\n\n width: 16px;\n height: 16px;\n\n opacity: 0;\n color: var(--ring-white-text-color);\n}\n\n.icon_b476.icon_b476 svg {\n position: absolute;\n top: 0;\n left: 0;\n }\n\n.check_a219 {\n}\n\n.minus_de65 {\n}\n\n.input_a330 {\n position: absolute;\n top: 0;\n left: 0;\n\n width: 100%;\n height: 100%;\n margin: 0;\n\n cursor: pointer;\n\n opacity: 0;\n\n /* stylelint-disable-next-line selector-max-specificity */\n}\n\n.input_a330:checked + .cell_edda,\n .input_a330:indeterminate + .cell_edda {\n border-color: transparent;\n background-color: var(--ring-main-color);\n }\n\n/* stylelint-disable-next-line selector-max-specificity */\n\n.input_a330:checked + .cell_edda .check_a219 {\n opacity: 1;\n }\n\n.input_a330:focus-visible + .cell_edda,\n .input_a330.focus_eaa3 + .cell_edda {\n transition: background-color var(--ring-ease);\n\n border-color: var(--ring-border-hover-color);\n box-shadow: 0 0 0 1px var(--ring-border-hover-color);\n }\n\n/* stylelint-disable-next-line selector-max-specificity */\n\n.input_a330:indeterminate + .cell_edda .minus_de65 {\n opacity: 1;\n }\n\n.input_a330[disabled] {\n pointer-events: none;\n }\n\n/* stylelint-disable-next-line selector-max-specificity */\n\n.input_a330[disabled][disabled] + .cell_edda {\n border-color: var(--ring-border-disabled-color);\n background-color: var(--ring-disabled-background-color);\n }\n\n/* stylelint-disable-next-line selector-max-specificity */\n\n.input_a330[disabled]:checked + .cell_edda,\n .input_a330[disabled]:indeterminate + .cell_edda {\n border-color: var(--ring-border-selected-disabled-color);\n }\n\n/* stylelint-disable-next-line selector-max-specificity */\n\n.input_a330[disabled]:checked + .cell_edda .check_a219,\n .input_a330[disabled]:indeterminate + .cell_edda .minus_de65 {\n color: var(--ring-icon-disabled-color);\n }\n\n/* stylelint-disable-next-line selector-max-specificity */\n\n.input_a330:indeterminate:indeterminate + .cell_edda .check_a219 {\n transition: none;\n\n opacity: 0;\n }\n\n.input_a330[disabled] ~ .label_dcc7 {\n color: var(--ring-disabled-color);\n }\n\n.label_dcc7 {\n margin-left: 8px;\n\n line-height: normal;\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/checkbox/checkbox.css",""],names:[],mappings:"AAKA;EACE,kBAAkB;;EAElB,qBAAqB;;EAErB,gBAAgB;;EAEhB,6BAA6B;EAC7B,aAAa;AAOf;;ACpBA,wGAAA;IAAA,8CAAA;;IAAA,6CAAA;GAAA,CAAA;;ADsBA;EACE,kBAAkB;EAClB,SAAS;;EAET,qBAAqB;;EAErB,sBAAsB;EACtB,WAAmB;EACnB,YAAoB;;EAEpB,yBAAiB;;KAAjB,sBAAiB;;UAAjB,iBAAiB;EACjB,yGAAyG;EACzG,sBAAsB;EACtB,oBAAoB;;EAEpB,2CAA2C;EAC3C,8CAA8C;EAC9C,sDAAsD;AACxD;;AAEA;EACE,kBAAkB;;EAElB,SAAS;EACT,UAAU;;EAEV,WAAqB;EACrB,YAAsB;;EAEtB,UAAU;EACV,mCAAmC;AAOrC;;AALE;IACE,kBAAkB;IAClB,MAAM;IACN,OAAO;EACT;;AAGF;AAEA;;AAEA;AAEA;;AAEA;EACE,kBAAkB;EAClB,MAAM;EACN,OAAO;;EAEP,WAAW;EACX,YAAY;EACZ,SAAS;;EAET,eAAe;;EAEf,UAAU;;EAEV,yDAAyD;AAyD3D;;AAxDE;;IAEE,yBAAyB;IACzB,wCAAwC;EAC1C;;AAEA,yDAAyD;;AACzD;IACE,UAAU;EACZ;;AAEA;;IAEE,6CAA6C;;IAE7C,4CAA4C;IAC5C,oDAAoD;EACtD;;AAEA,yDAAyD;;AACzD;IACE,UAAU;EACZ;;AAEA;IACE,oBAAoB;EACtB;;AAEA,yDAAyD;;AACzD;IACE,+CAA+C;IAC/C,uDAAuD;EACzD;;AAEA,yDAAyD;;AACzD;;IAEE,wDAAwD;EAC1D;;AAEA,yDAAyD;;AACzD;;IAEE,sCAAsC;EACxC;;AAEA,yDAAyD;;AACzD;IACE,gBAAgB;;IAEhB,UAAU;EACZ;;AAEA;IACE,iCAAiC;EACnC;;AAGF;EACE,gBAAiB;;EAEjB,mBAAmB;AACrB",sourcesContent:['@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n@value checkboxSize: 14px;\n\n.checkbox {\n position: relative;\n\n display: inline-block;\n\n text-align: left;\n\n color: var(--ring-text-color);\n outline: none;\n\n &:hover .cell {\n transition: background-color var(--ring-ease);\n\n border-color: var(--ring-border-hover-color);\n }\n}\n\n.cell {\n position: relative;\n top: -2px;\n\n display: inline-block;\n\n box-sizing: border-box;\n width: checkboxSize;\n height: checkboxSize;\n\n user-select: none;\n transition: border-color var(--ring-ease), background-color var(--ring-ease), box-shadow var(--ring-ease);\n vertical-align: middle;\n pointer-events: none;\n\n border: 1px solid var(--ring-borders-color);\n border-radius: var(--ring-border-radius-small);\n background-color: var(--ring-content-background-color);\n}\n\n.icon.icon {\n position: absolute;\n\n top: -1px;\n left: -1px;\n\n width: calc(unit * 2);\n height: calc(unit * 2);\n\n opacity: 0;\n color: var(--ring-white-text-color);\n\n & svg {\n position: absolute;\n top: 0;\n left: 0;\n }\n}\n\n.check {\n composes: icon;\n}\n\n.minus {\n composes: icon;\n}\n\n.input {\n position: absolute;\n top: 0;\n left: 0;\n\n width: 100%;\n height: 100%;\n margin: 0;\n\n cursor: pointer;\n\n opacity: 0;\n\n /* stylelint-disable-next-line selector-max-specificity */\n &:checked + .cell,\n &:indeterminate + .cell {\n border-color: transparent;\n background-color: var(--ring-main-color);\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n &:checked + .cell .check {\n opacity: 1;\n }\n\n &:focus-visible + .cell,\n &.focus + .cell {\n transition: background-color var(--ring-ease);\n\n border-color: var(--ring-border-hover-color);\n box-shadow: 0 0 0 1px var(--ring-border-hover-color);\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n &:indeterminate + .cell .minus {\n opacity: 1;\n }\n\n &[disabled] {\n pointer-events: none;\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n &[disabled][disabled] + .cell {\n border-color: var(--ring-border-disabled-color);\n background-color: var(--ring-disabled-background-color);\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n &[disabled]:checked + .cell,\n &[disabled]:indeterminate + .cell {\n border-color: var(--ring-border-selected-disabled-color);\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n &[disabled]:checked + .cell .check,\n &[disabled]:indeterminate + .cell .minus {\n color: var(--ring-icon-disabled-color);\n }\n\n /* stylelint-disable-next-line selector-max-specificity */\n &:indeterminate:indeterminate + .cell .check {\n transition: none;\n\n opacity: 0;\n }\n\n &[disabled] ~ .label {\n color: var(--ring-disabled-color);\n }\n}\n\n.label {\n margin-left: unit;\n\n line-height: normal;\n}\n',null],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,checkboxSize:"14px",checkbox:"checkbox_dccf",cell:"cell_edda",icon:"icon_b476",check:"check_a219 icon_b476",minus:"minus_de65 icon_b476",input:"input_a330",focus:"focus_eaa3",label:"label_dcc7"};const u=s},5486:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>l});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i)()(o());a.push([e.id,".label_bed7 {\n display: block;\n\n margin-bottom: calc(var(--ring-unit)*0.5);\n}\n\n.formLabel_f9ba {\n color: var(--ring-text-color);\n\n font-size: var(--ring-font-size);\n line-height: var(--ring-line-height);\n}\n\n.secondaryLabel_e8a1 {\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n\n.disabledLabel_e4c1 {\n color: var(--ring-disabled-color);\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/control-label/control-label.css"],names:[],mappings:"AAAA;EACE,cAAc;;EAEd,yCAA2C;AAC7C;;AAEA;EACE,6BAA6B;;EAE7B,gCAAgC;EAChC,oCAAoC;AACtC;;AAEA;EACE,kCAAkC;;EAElC,wCAAwC;EACxC,2CAA2C;AAC7C;;AAEA;EACE,iCAAiC;AACnC",sourcesContent:[".label {\n display: block;\n\n margin-bottom: calc(var(--ring-unit) * 0.5);\n}\n\n.formLabel {\n color: var(--ring-text-color);\n\n font-size: var(--ring-font-size);\n line-height: var(--ring-line-height);\n}\n\n.secondaryLabel {\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n\n.disabledLabel {\n color: var(--ring-disabled-color);\n}\n"],sourceRoot:""}]),a.locals={label:"label_bed7",formLabel:"formLabel_f9ba",secondaryLabel:"secondaryLabel_e8a1",disabledLabel:"disabledLabel_e4c1"};const l=a},6506:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>s});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(5280),c=a()(o());c.i(l.A),c.push([e.id,".dropdown_a1de {\n display: inline-block;\n}\n\n.anchor_fdbe.anchor_fdbe {\n margin: 0 -3px;\n padding: 0 3px;\n\n font: inherit;\n}\n\n.chevron_ffc6 {\n margin-left: 2px;\n\n line-height: normal;\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/dropdown/dropdown.css"],names:[],mappings:"AAEA;EACE,qBAAqB;AACvB;;AAEA;EACE,cAAc;EACd,cAAc;;EAEd,aAAa;AACf;;AAEA;EACE,gBAAgB;;EAEhB,mBAAmB;AACrB",sourcesContent:['@import "../global/variables.css";\n\n.dropdown {\n display: inline-block;\n}\n\n.anchor.anchor {\n margin: 0 -3px;\n padding: 0 3px;\n\n font: inherit;\n}\n\n.chevron {\n margin-left: 2px;\n\n line-height: normal;\n}\n'],sourceRoot:""}]),c.locals={dropdown:"dropdown_a1de",anchor:"anchor_fdbe",chevron:"chevron_ffc6"};const s=c},9106:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>l});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i)()(o());a.push([e.id,'/* https://readymag.com/artemtiunov/RingUILanguage/colours/ */\n\n/*\nUnit shouldn\'t be CSS custom property because it is not intended to change\nAlso it won\'t form in FF47 https://bugzilla.mozilla.org/show_bug.cgi?id=594933\n*/\n\n.clearfix_c694::after {\n display: block;\n clear: both;\n\n content: "";\n }\n\n.font_a1f6 {\n font-family: var(--ring-font-family);\n font-size: var(--ring-font-size);\n line-height: var(--ring-line-height);\n}\n\n.font-lower_c3c9 {\n\n line-height: var(--ring-line-height-lower);\n}\n\n.font-smaller_d963 {\n\n font-size: var(--ring-font-size-smaller);\n}\n\n.font-smaller-lower_ff5f {\n\n line-height: var(--ring-line-height-lowest);\n}\n\n.font-larger-lower_b336 {\n\n font-size: var(--ring-font-size-larger);\n}\n\n.font-larger_f035 {\n\n line-height: var(--ring-line-height-taller);\n}\n\n/* To be used at large sizes */\n/* As close as possible to Helvetica Neue Thin (to replace Gotham) */\n.thin-font_de5b {\n font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;\n font-size: var(--ring-font-size);\n font-weight: 100; /* Renders Helvetica Neue UltraLight on OS X */\n}\n\n.monospace-font_ac33 {\n font-family: var(--ring-font-family-monospace);\n font-size: var(--ring-font-size-smaller);\n}\n\n.ellipsis_e43b {\n overflow: hidden;\n\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.resetButton_ddd2 {\n overflow: visible;\n\n padding: 0;\n\n text-align: left;\n\n color: inherit;\n border: 0;\n\n background-color: transparent;\n\n font: inherit;\n}\n\n.resetButton_ddd2::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n }\n\n/* Note: footer also has top margin which isn\'t taken into account here */\n\n/* Media breakpoints (minimal values) */\n\n/* Media queries */\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/global/global.css"],names:[],mappings:"AAAA,6DAA6D;;AAE7D;;;CAGC;;AAIC;IACE,cAAc;IACd,WAAW;;IAEX,WAAW;EACb;;AAGF;EACE,oCAAoC;EACpC,gCAAgC;EAChC,oCAAoC;AACtC;;AAEA;;EAGE,0CAA0C;AAC5C;;AAEA;;EAGE,wCAAwC;AAC1C;;AAEA;;EAGE,2CAA2C;AAC7C;;AAEA;;EAGE,uCAAuC;AACzC;;AAEA;;EAGE,2CAA2C;AAC7C;;AAEA,8BAA8B;AAC9B,oEAAoE;AACpE;EACE,uEAAuE;EACvE,gCAAgC;EAChC,gBAAgB,EAAE,+CAA+C;AACnE;;AAEA;EACE,8CAA8C;EAC9C,wCAAwC;AAC1C;;AAEA;EACE,gBAAgB;;EAEhB,mBAAmB;EACnB,uBAAuB;AACzB;;AAEA;EACE,iBAAiB;;EAEjB,UAAU;;EAEV,gBAAgB;;EAEhB,cAAc;EACd,SAAS;;EAET,6BAA6B;;EAE7B,aAAa;AAOf;;AALE;IACE,UAAU;;IAEV,SAAS;EACX;;AAGF,yEAAyE;;AAGzE,uCAAuC;;AAKvC,kBAAkB",sourcesContent:['/* https://readymag.com/artemtiunov/RingUILanguage/colours/ */\n\n/*\nUnit shouldn\'t be CSS custom property because it is not intended to change\nAlso it won\'t form in FF47 https://bugzilla.mozilla.org/show_bug.cgi?id=594933\n*/\n@value unit: 8px;\n\n.clearfix {\n &::after {\n display: block;\n clear: both;\n\n content: "";\n }\n}\n\n.font {\n font-family: var(--ring-font-family);\n font-size: var(--ring-font-size);\n line-height: var(--ring-line-height);\n}\n\n.font-lower {\n composes: font;\n\n line-height: var(--ring-line-height-lower);\n}\n\n.font-smaller {\n composes: font-lower;\n\n font-size: var(--ring-font-size-smaller);\n}\n\n.font-smaller-lower {\n composes: font-smaller;\n\n line-height: var(--ring-line-height-lowest);\n}\n\n.font-larger-lower {\n composes: font-lower;\n\n font-size: var(--ring-font-size-larger);\n}\n\n.font-larger {\n composes: font-larger-lower;\n\n line-height: var(--ring-line-height-taller);\n}\n\n/* To be used at large sizes */\n/* As close as possible to Helvetica Neue Thin (to replace Gotham) */\n.thin-font {\n font-family: "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif;\n font-size: var(--ring-font-size);\n font-weight: 100; /* Renders Helvetica Neue UltraLight on OS X */\n}\n\n.monospace-font {\n font-family: var(--ring-font-family-monospace);\n font-size: var(--ring-font-size-smaller);\n}\n\n.ellipsis {\n overflow: hidden;\n\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.resetButton {\n overflow: visible;\n\n padding: 0;\n\n text-align: left;\n\n color: inherit;\n border: 0;\n\n background-color: transparent;\n\n font: inherit;\n\n &::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n }\n}\n\n/* Note: footer also has top margin which isn\'t taken into account here */\n@value footer-height: calc(unit * 8);\n\n/* Media breakpoints (minimal values) */\n@value breakpoint-small: 640px;\n@value breakpoint-middle: 960px;\n@value breakpoint-large: 1200px;\n\n/* Media queries */\n@value extra-small-screen-media: (max-width: calc(breakpoint-small - 1px));\n@value small-screen-media: (min-width: breakpoint-small) and (max-width: calc(breakpoint-middle - 1px));\n@value middle-screen-media: (min-width: breakpoint-middle) and (max-width: calc(breakpoint-large - 1px));\n@value large-screen-media: (min-width: breakpoint-large);\n'],sourceRoot:""}]),a.locals={unit:"8px","footer-height":"64px","breakpoint-small":"640px","breakpoint-middle":"960px","breakpoint-large":"1200px","extra-small-screen-media":"(max-width: 639px)","small-screen-media":"(min-width: 640px) and (max-width: 959px)","middle-screen-media":"(min-width: 960px) and (max-width: 1199px)","large-screen-media":"(min-width: 1200px)",clearfix:"clearfix_c694",font:"font_a1f6","font-lower":"font-lower_c3c9 font_a1f6","font-smaller":"font-smaller_d963 font-lower_c3c9 font_a1f6","font-smaller-lower":"font-smaller-lower_ff5f font-smaller_d963 font-lower_c3c9 font_a1f6","font-larger-lower":"font-larger-lower_b336 font-lower_c3c9 font_a1f6","font-larger":"font-larger_f035 font-larger-lower_b336 font-lower_c3c9 font_a1f6","thin-font":"thin-font_de5b","monospace-font":"monospace-font_ac33",ellipsis:"ellipsis_e43b",resetButton:"resetButton_ddd2"};const l=a},5280:(e,n,t)=>{"use strict";t.d(n,{A:()=>l});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i)()(o());a.push([e.id,'/* stylelint-disable color-no-hex */\n\n.light_f331,\n:root {\n --ring-unit: 8px;\n\n /* Element */\n --ring-line-components: 223, 229, 235;\n --ring-line-color: rgb(var(--ring-line-components)); /* #dfe5eb */\n --ring-borders-components: 197, 209, 219;\n --ring-borders-color: rgb(var(--ring-borders-components)); /* #c5d1db */\n --ring-icon-components: 184, 209, 229;\n --ring-icon-color: rgb(var(--ring-icon-components)); /* #b8d1e5 */\n --ring-icon-secondary-components: 153, 153, 153;\n --ring-icon-secondary-color: rgb(var(--ring-icon-secondary-components)); /* #999 */\n --ring-border-disabled-components: 232, 232, 232;\n --ring-border-disabled-color: rgb(var(--ring-border-disabled-components)); /* #e8e8e8 */\n --ring-border-selected-disabled-components: 212, 212, 212;\n --ring-border-selected-disabled-color: rgb(var(--ring-border-selected-disabled-components)); /* #d4d4d4 */\n --ring-border-unselected-disabled-components: 232, 232, 232;\n --ring-border-unselected-disabled-color: rgb(var(--ring-border-unselected-disabled-components)); /* #e8e8e8 */ /* TODO remove in 6.0 */\n --ring-icon-disabled-components: 212, 212, 212;\n --ring-icon-disabled-color: rgb(var(--ring-icon-disabled-components)); /* #d4d4d4 */\n --ring-border-hover-components: 128, 198, 255;\n --ring-border-hover-color: rgb(var(--ring-border-hover-components)); /* #80c6ff */\n --ring-icon-hover-components: var(--ring-link-hover-color);\n --ring-icon-hover-color: var(--ring-link-hover-color);\n --ring-main-components: 0, 128, 229;\n --ring-main-color: rgb(var(--ring-main-components)); /* #0080e5 */\n --ring-action-link-components: var(--ring-main-components);\n --ring-action-link-color: rgb(var(--ring-main-components)); /* #0080e5 */\n --ring-main-hover-components: 0, 112, 204;\n --ring-main-hover-color: rgb(var(--ring-main-hover-components)); /* #0070cc */\n --ring-icon-error-components: 219, 88, 96;\n --ring-icon-error-color: rgb(var(--ring-icon-error-components)); /* #db5860 */\n --ring-icon-warning-components: 237, 162, 0;\n --ring-icon-warning-color: rgb(var(--ring-icon-warning-components)); /* #eda200 */\n --ring-icon-success-components: 89, 168, 105;\n --ring-icon-success-color: rgb(var(--ring-icon-success-components)); /* #59a869 */\n --ring-pale-control-components: 207, 219, 229;\n --ring-pale-control-color: rgb(var(--ring-pale-control-components)); /* #cfdbe5 */\n --ring-popup-border-components: 0, 28, 54;\n --ring-popup-border-color: var(--ring-line-color);\n --ring-popup-shadow-components: rgba(var(--ring-popup-border-components), 0.1);\n --ring-popup-shadow-color: rgba(var(--ring-popup-border-components), 0.1);\n --ring-popup-secondary-shadow-color: rgba(var(--ring-popup-border-components), 0.04);\n --ring-message-shadow-color: rgba(var(--ring-popup-border-components), 0.3);\n --ring-pinned-shadow-components: 115, 117, 119;\n --ring-pinned-shadow-color: rgb(var(--ring-pinned-shadow-components)); /* #737577 */\n --ring-button-danger-hover-components: var(--ring-icon-error-color);\n --ring-button-danger-hover-color: var(--ring-icon-error-color);\n --ring-button-primary-border-components: 0, 98, 178;\n --ring-button-primary-border-color: rgb(var(--ring-button-primary-border-components)); /* #0062b2 */\n --ring-popup-shadow: 0 2px 8px var(--ring-popup-shadow-color), 0 1px 2px var(--ring-popup-secondary-shadow-color);\n --ring-dialog-shadow: 0 4px 24px var(--ring-popup-shadow-color), 0 2px 6px var(--ring-popup-secondary-shadow-color);\n\n /* Text */\n --ring-search-components: 102, 158, 204;\n --ring-search-color: rgb(var(--ring-search-components)); /* #669ecc */\n --ring-hint-components: 64, 99, 128;\n --ring-hint-color: rgb(var(--ring-hint-components)); /* #406380 */\n --ring-link-components: 15, 91, 153;\n --ring-link-color: rgb(var(--ring-link-components)); /* #0f5b99 */\n --ring-link-hover-components: 255, 0, 140;\n --ring-link-hover-color: rgb(var(--ring-link-hover-components)); /* #ff008c */\n --ring-error-components: 169, 15, 26;\n --ring-error-color: rgb(var(--ring-error-components)); /* #a90f1a */\n --ring-warning-components: 178, 92, 0;\n --ring-warning-color: rgb(var(--ring-warning-components)); /* #b25c00 */\n --ring-success-components: 12, 117, 35;\n --ring-success-color: rgb(var(--ring-success-components)); /* #0c7523 */\n --ring-text-components: 31, 35, 38;\n --ring-text-color: rgb(var(--ring-text-components)); /* #1f2326 */\n --ring-active-text-color: var(--ring-text-color);\n --ring-white-text-components: 255, 255, 255;\n --ring-white-text-color: rgb(var(--ring-white-text-components)); /* #fff */\n --ring-heading-color: var(--ring-text-color);\n --ring-secondary-components: 115, 117, 119;\n --ring-secondary-color: rgb(var(--ring-secondary-components)); /* #737577 */\n --ring-disabled-components: 153, 153, 153;\n --ring-disabled-color: rgb(var(--ring-disabled-components)); /* #999 */\n\n /* Background */\n --ring-content-background-components: 255, 255, 255;\n --ring-content-background-color: rgb(var(--ring-content-background-components)); /* #fff */\n --ring-popup-background-components: 255, 255, 255;\n --ring-popup-background-color: rgb(var(--ring-popup-background-components)); /* #fff */\n --ring-sidebar-background-components: 247, 249, 250;\n --ring-sidebar-background-color: rgb(var(--ring-sidebar-background-components)); /* #f7f9fa */\n --ring-selected-background-components: 212, 237, 255;\n --ring-selected-background-color: rgb(var(--ring-selected-background-components)); /* #d4edff */\n --ring-hover-background-components: 235, 246, 255;\n --ring-hover-background-color: rgb(var(--ring-hover-background-components)); /* #ebf6ff */\n --ring-navigation-background-components: 255, 255, 255;\n --ring-navigation-background-color: rgb(var(--ring-navigation-background-components)); /* #fff */\n --ring-tag-background-components: 230, 236, 242;\n --ring-tag-background-color: rgb(var(--ring-tag-background-components)); /* #e6ecf2 */\n --ring-tag-hover-background-components: 211, 218, 224;\n --ring-tag-hover-background-color: rgb(var(--ring-tag-hover-background-components)); /* #d3dae0 */\n --ring-removed-background-components: 255, 213, 203;\n --ring-removed-background-color: rgb(var(--ring-removed-background-components)); /* #ffd5cb */\n --ring-warning-background-components: 250, 236, 205;\n --ring-warning-background-color: rgb(var(--ring-warning-background-components)); /* #faeccd */\n --ring-added-background-components: 216, 240, 216;\n --ring-added-background-color: rgb(var(--ring-added-background-components)); /* #d8f0d8 */\n --ring-disabled-background-components: 245, 245, 245;\n --ring-disabled-background-color: rgb(var(--ring-disabled-background-components)); /* #f5f5f5 */\n --ring-disabled-selected-background-components: 232, 232, 232;\n --ring-disabled-selected-background-color: rgb(var(--ring-disabled-selected-background-components)); /* #e8e8e8 */\n --ring-button-danger-active-components: 255, 231, 232;\n --ring-button-danger-active-color: rgb(var(--ring-button-danger-active-components)); /* #ffe7e8 */\n --ring-button-loader-background-components: 51, 163, 255;\n --ring-button-loader-background: rgb(var(--ring-button-loader-background-components)); /* #33a3ff */\n --ring-button-primary-background-components: 26, 152, 255;\n --ring-button-primary-background-color: rgb(var(--ring-button-primary-background-components)); /* #1a98ff */\n --ring-table-loader-background-color: rgba(var(--ring-content-background-components), 0.5); /* #ffffff80 */\n\n /* Code */\n --ring-code-background-color: var(--ring-content-background-color);\n --ring-code-components: 0, 0, 0;\n --ring-code-color: rgb(var(--ring-code-components)); /* #000 */\n --ring-code-comment-components: 112, 112, 112;\n --ring-code-comment-color: rgb(var(--ring-code-comment-components)); /* #707070 */\n --ring-code-meta-components: 112, 112, 112;\n --ring-code-meta-color: rgb(var(--ring-code-meta-components)); /* #707070 */\n --ring-code-keyword-components: 0, 0, 128;\n --ring-code-keyword-color: rgb(var(--ring-code-keyword-components)); /* #000080 */\n --ring-code-tag-background-components: 239, 239, 239;\n --ring-code-tag-background-color: rgb(var(--ring-code-tag-background-components)); /* #efefef */\n --ring-code-tag-color: var(--ring-code-keyword-color);\n --ring-code-tag-font-weight: bold;\n --ring-code-field-components: 102, 14, 122;\n --ring-code-field-color: rgb(var(--ring-code-field-components)); /* #660e7a */\n --ring-code-attribute-components: 0, 0, 255;\n --ring-code-attribute-color: rgb(var(--ring-code-attribute-components)); /* #00f */\n --ring-code-number-color: var(--ring-code-attribute-color);\n --ring-code-string-components: 0, 122, 0;\n --ring-code-string-color: rgb(var(--ring-code-string-components)); /* #007a00 */\n --ring-code-addition-components: 170, 222, 170;\n --ring-code-addition-color: rgb(var(--ring-code-addition-components)); /* #aadeaa */\n --ring-code-deletion-components: 200, 200, 200;\n --ring-code-deletion-color: rgb(var(--ring-code-deletion-components)); /* #c8c8c8 */\n\n /* Metrics */\n --ring-border-radius: 4px;\n --ring-border-radius-small: 2px;\n --ring-font-size-larger: 15px;\n --ring-font-size: 14px;\n --ring-font-size-smaller: 12px;\n --ring-line-height-taller: 21px;\n --ring-line-height: 20px;\n --ring-line-height-lower: 18px;\n --ring-line-height-lowest: 16px;\n --ring-ease: 0.3s ease-out;\n --ring-fast-ease: 0.15s ease-out;\n --ring-font-family: system-ui, -apple-system, Segoe UI, Roboto, Noto Sans, Ubuntu, Cantarell, Helvetica Neue, Arial, sans-serif;\n --ring-font-family-monospace:\n Menlo,\n "Bitstream Vera Sans Mono",\n "Ubuntu Mono",\n Consolas,\n "Courier New",\n Courier,\n monospace;\n\n /* Common z-index-values */\n\n /* Invisible element is an absolutely positioned element which should be below */\n /* all other elements on the page */\n --ring-invisible-element-z-index: -1;\n\n /* z-index for position: fixed elements */\n --ring-fixed-z-index: 1;\n\n /* Elements that should overlay all other elements on the page */\n --ring-overlay-z-index: 5;\n\n /* Alerts should de displayed above overlays */\n --ring-alert-z-index: 6;\n}\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/global/variables.css"],names:[],mappings:"AAAA,mCAAmC;;AAEnC;;EAEE,gBAAgB;;EAEhB,YAAY;EACZ,qCAAqC;EACrC,mDAAmD,EAAE,YAAY;EACjE,wCAAwC;EACxC,yDAAyD,EAAE,YAAY;EACvE,qCAAqC;EACrC,mDAAmD,EAAE,YAAY;EACjE,+CAA+C;EAC/C,uEAAuE,EAAE,SAAS;EAClF,gDAAgD;EAChD,yEAAyE,EAAE,YAAY;EACvF,yDAAyD;EACzD,2FAA2F,EAAE,YAAY;EACzG,2DAA2D;EAC3D,+FAA+F,EAAE,YAAY,EAAE,uBAAuB;EACtI,8CAA8C;EAC9C,qEAAqE,EAAE,YAAY;EACnF,6CAA6C;EAC7C,mEAAmE,EAAE,YAAY;EACjF,0DAA0D;EAC1D,qDAAqD;EACrD,mCAAmC;EACnC,mDAAmD,EAAE,YAAY;EACjE,0DAA0D;EAC1D,0DAA0D,EAAE,YAAY;EACxE,yCAAyC;EACzC,+DAA+D,EAAE,YAAY;EAC7E,yCAAyC;EACzC,+DAA+D,EAAE,YAAY;EAC7E,2CAA2C;EAC3C,mEAAmE,EAAE,YAAY;EACjF,4CAA4C;EAC5C,mEAAmE,EAAE,YAAY;EACjF,6CAA6C;EAC7C,mEAAmE,EAAE,YAAY;EACjF,yCAAyC;EACzC,iDAAiD;EACjD,8EAA8E;EAC9E,yEAAyE;EACzE,oFAAoF;EACpF,2EAA2E;EAC3E,8CAA8C;EAC9C,qEAAqE,EAAE,YAAY;EACnF,mEAAmE;EACnE,8DAA8D;EAC9D,mDAAmD;EACnD,qFAAqF,EAAE,YAAY;EACnG,iHAAiH;EACjH,mHAAmH;;EAEnH,SAAS;EACT,uCAAuC;EACvC,uDAAuD,EAAE,YAAY;EACrE,mCAAmC;EACnC,mDAAmD,EAAE,YAAY;EACjE,mCAAmC;EACnC,mDAAmD,EAAE,YAAY;EACjE,yCAAyC;EACzC,+DAA+D,EAAE,YAAY;EAC7E,oCAAoC;EACpC,qDAAqD,EAAE,YAAY;EACnE,qCAAqC;EACrC,yDAAyD,EAAE,YAAY;EACvE,sCAAsC;EACtC,yDAAyD,EAAE,YAAY;EACvE,kCAAkC;EAClC,mDAAmD,EAAE,YAAY;EACjE,gDAAgD;EAChD,2CAA2C;EAC3C,+DAA+D,EAAE,SAAS;EAC1E,4CAA4C;EAC5C,0CAA0C;EAC1C,6DAA6D,EAAE,YAAY;EAC3E,yCAAyC;EACzC,2DAA2D,EAAE,SAAS;;EAEtE,eAAe;EACf,mDAAmD;EACnD,+EAA+E,EAAE,SAAS;EAC1F,iDAAiD;EACjD,2EAA2E,EAAE,SAAS;EACtF,mDAAmD;EACnD,+EAA+E,EAAE,YAAY;EAC7F,oDAAoD;EACpD,iFAAiF,EAAE,YAAY;EAC/F,iDAAiD;EACjD,2EAA2E,EAAE,YAAY;EACzF,sDAAsD;EACtD,qFAAqF,EAAE,SAAS;EAChG,+CAA+C;EAC/C,uEAAuE,EAAE,YAAY;EACrF,qDAAqD;EACrD,mFAAmF,EAAE,YAAY;EACjG,mDAAmD;EACnD,+EAA+E,EAAE,YAAY;EAC7F,mDAAmD;EACnD,+EAA+E,EAAE,YAAY;EAC7F,iDAAiD;EACjD,2EAA2E,EAAE,YAAY;EACzF,oDAAoD;EACpD,iFAAiF,EAAE,YAAY;EAC/F,6DAA6D;EAC7D,mGAAmG,EAAE,YAAY;EACjH,qDAAqD;EACrD,mFAAmF,EAAE,YAAY;EACjG,wDAAwD;EACxD,qFAAqF,EAAE,YAAY;EACnG,yDAAyD;EACzD,6FAA6F,EAAE,YAAY;EAC3G,0FAA0F,EAAE,cAAc;;EAE1G,SAAS;EACT,kEAAkE;EAClE,+BAA+B;EAC/B,mDAAmD,EAAE,SAAS;EAC9D,6CAA6C;EAC7C,mEAAmE,EAAE,YAAY;EACjF,0CAA0C;EAC1C,6DAA6D,EAAE,YAAY;EAC3E,yCAAyC;EACzC,mEAAmE,EAAE,YAAY;EACjF,oDAAoD;EACpD,iFAAiF,EAAE,YAAY;EAC/F,qDAAqD;EACrD,iCAAiC;EACjC,0CAA0C;EAC1C,+DAA+D,EAAE,YAAY;EAC7E,2CAA2C;EAC3C,uEAAuE,EAAE,SAAS;EAClF,0DAA0D;EAC1D,wCAAwC;EACxC,iEAAiE,EAAE,YAAY;EAC/E,8CAA8C;EAC9C,qEAAqE,EAAE,YAAY;EACnF,8CAA8C;EAC9C,qEAAqE,EAAE,YAAY;;EAEnF,YAAY;EACZ,yBAAyB;EACzB,+BAA+B;EAC/B,6BAA6B;EAC7B,sBAAsB;EACtB,8BAA8B;EAC9B,+BAA+B;EAC/B,wBAAwB;EACxB,8BAA8B;EAC9B,+BAA+B;EAC/B,0BAA0B;EAC1B,gCAAgC;EAChC,+HAAgD;EAChD;;;;;;;aAOW;;EAEX,0BAA0B;;EAE1B,gFAAgF;EAChF,mCAAmC;EACnC,oCAAoC;;EAEpC,yCAAyC;EACzC,uBAAuB;;EAEvB,gEAAgE;EAChE,yBAAyB;;EAEzB,8CAA8C;EAC9C,uBAAuB;AACzB",sourcesContent:['/* stylelint-disable color-no-hex */\n\n.light,\n:root {\n --ring-unit: 8px;\n\n /* Element */\n --ring-line-components: 223, 229, 235;\n --ring-line-color: rgb(var(--ring-line-components)); /* #dfe5eb */\n --ring-borders-components: 197, 209, 219;\n --ring-borders-color: rgb(var(--ring-borders-components)); /* #c5d1db */\n --ring-icon-components: 184, 209, 229;\n --ring-icon-color: rgb(var(--ring-icon-components)); /* #b8d1e5 */\n --ring-icon-secondary-components: 153, 153, 153;\n --ring-icon-secondary-color: rgb(var(--ring-icon-secondary-components)); /* #999 */\n --ring-border-disabled-components: 232, 232, 232;\n --ring-border-disabled-color: rgb(var(--ring-border-disabled-components)); /* #e8e8e8 */\n --ring-border-selected-disabled-components: 212, 212, 212;\n --ring-border-selected-disabled-color: rgb(var(--ring-border-selected-disabled-components)); /* #d4d4d4 */\n --ring-border-unselected-disabled-components: 232, 232, 232;\n --ring-border-unselected-disabled-color: rgb(var(--ring-border-unselected-disabled-components)); /* #e8e8e8 */ /* TODO remove in 6.0 */\n --ring-icon-disabled-components: 212, 212, 212;\n --ring-icon-disabled-color: rgb(var(--ring-icon-disabled-components)); /* #d4d4d4 */\n --ring-border-hover-components: 128, 198, 255;\n --ring-border-hover-color: rgb(var(--ring-border-hover-components)); /* #80c6ff */\n --ring-icon-hover-components: var(--ring-link-hover-color);\n --ring-icon-hover-color: var(--ring-link-hover-color);\n --ring-main-components: 0, 128, 229;\n --ring-main-color: rgb(var(--ring-main-components)); /* #0080e5 */\n --ring-action-link-components: var(--ring-main-components);\n --ring-action-link-color: rgb(var(--ring-main-components)); /* #0080e5 */\n --ring-main-hover-components: 0, 112, 204;\n --ring-main-hover-color: rgb(var(--ring-main-hover-components)); /* #0070cc */\n --ring-icon-error-components: 219, 88, 96;\n --ring-icon-error-color: rgb(var(--ring-icon-error-components)); /* #db5860 */\n --ring-icon-warning-components: 237, 162, 0;\n --ring-icon-warning-color: rgb(var(--ring-icon-warning-components)); /* #eda200 */\n --ring-icon-success-components: 89, 168, 105;\n --ring-icon-success-color: rgb(var(--ring-icon-success-components)); /* #59a869 */\n --ring-pale-control-components: 207, 219, 229;\n --ring-pale-control-color: rgb(var(--ring-pale-control-components)); /* #cfdbe5 */\n --ring-popup-border-components: 0, 28, 54;\n --ring-popup-border-color: var(--ring-line-color);\n --ring-popup-shadow-components: rgba(var(--ring-popup-border-components), 0.1);\n --ring-popup-shadow-color: rgba(var(--ring-popup-border-components), 0.1);\n --ring-popup-secondary-shadow-color: rgba(var(--ring-popup-border-components), 0.04);\n --ring-message-shadow-color: rgba(var(--ring-popup-border-components), 0.3);\n --ring-pinned-shadow-components: 115, 117, 119;\n --ring-pinned-shadow-color: rgb(var(--ring-pinned-shadow-components)); /* #737577 */\n --ring-button-danger-hover-components: var(--ring-icon-error-color);\n --ring-button-danger-hover-color: var(--ring-icon-error-color);\n --ring-button-primary-border-components: 0, 98, 178;\n --ring-button-primary-border-color: rgb(var(--ring-button-primary-border-components)); /* #0062b2 */\n --ring-popup-shadow: 0 2px 8px var(--ring-popup-shadow-color), 0 1px 2px var(--ring-popup-secondary-shadow-color);\n --ring-dialog-shadow: 0 4px 24px var(--ring-popup-shadow-color), 0 2px 6px var(--ring-popup-secondary-shadow-color);\n\n /* Text */\n --ring-search-components: 102, 158, 204;\n --ring-search-color: rgb(var(--ring-search-components)); /* #669ecc */\n --ring-hint-components: 64, 99, 128;\n --ring-hint-color: rgb(var(--ring-hint-components)); /* #406380 */\n --ring-link-components: 15, 91, 153;\n --ring-link-color: rgb(var(--ring-link-components)); /* #0f5b99 */\n --ring-link-hover-components: 255, 0, 140;\n --ring-link-hover-color: rgb(var(--ring-link-hover-components)); /* #ff008c */\n --ring-error-components: 169, 15, 26;\n --ring-error-color: rgb(var(--ring-error-components)); /* #a90f1a */\n --ring-warning-components: 178, 92, 0;\n --ring-warning-color: rgb(var(--ring-warning-components)); /* #b25c00 */\n --ring-success-components: 12, 117, 35;\n --ring-success-color: rgb(var(--ring-success-components)); /* #0c7523 */\n --ring-text-components: 31, 35, 38;\n --ring-text-color: rgb(var(--ring-text-components)); /* #1f2326 */\n --ring-active-text-color: var(--ring-text-color);\n --ring-white-text-components: 255, 255, 255;\n --ring-white-text-color: rgb(var(--ring-white-text-components)); /* #fff */\n --ring-heading-color: var(--ring-text-color);\n --ring-secondary-components: 115, 117, 119;\n --ring-secondary-color: rgb(var(--ring-secondary-components)); /* #737577 */\n --ring-disabled-components: 153, 153, 153;\n --ring-disabled-color: rgb(var(--ring-disabled-components)); /* #999 */\n\n /* Background */\n --ring-content-background-components: 255, 255, 255;\n --ring-content-background-color: rgb(var(--ring-content-background-components)); /* #fff */\n --ring-popup-background-components: 255, 255, 255;\n --ring-popup-background-color: rgb(var(--ring-popup-background-components)); /* #fff */\n --ring-sidebar-background-components: 247, 249, 250;\n --ring-sidebar-background-color: rgb(var(--ring-sidebar-background-components)); /* #f7f9fa */\n --ring-selected-background-components: 212, 237, 255;\n --ring-selected-background-color: rgb(var(--ring-selected-background-components)); /* #d4edff */\n --ring-hover-background-components: 235, 246, 255;\n --ring-hover-background-color: rgb(var(--ring-hover-background-components)); /* #ebf6ff */\n --ring-navigation-background-components: 255, 255, 255;\n --ring-navigation-background-color: rgb(var(--ring-navigation-background-components)); /* #fff */\n --ring-tag-background-components: 230, 236, 242;\n --ring-tag-background-color: rgb(var(--ring-tag-background-components)); /* #e6ecf2 */\n --ring-tag-hover-background-components: 211, 218, 224;\n --ring-tag-hover-background-color: rgb(var(--ring-tag-hover-background-components)); /* #d3dae0 */\n --ring-removed-background-components: 255, 213, 203;\n --ring-removed-background-color: rgb(var(--ring-removed-background-components)); /* #ffd5cb */\n --ring-warning-background-components: 250, 236, 205;\n --ring-warning-background-color: rgb(var(--ring-warning-background-components)); /* #faeccd */\n --ring-added-background-components: 216, 240, 216;\n --ring-added-background-color: rgb(var(--ring-added-background-components)); /* #d8f0d8 */\n --ring-disabled-background-components: 245, 245, 245;\n --ring-disabled-background-color: rgb(var(--ring-disabled-background-components)); /* #f5f5f5 */\n --ring-disabled-selected-background-components: 232, 232, 232;\n --ring-disabled-selected-background-color: rgb(var(--ring-disabled-selected-background-components)); /* #e8e8e8 */\n --ring-button-danger-active-components: 255, 231, 232;\n --ring-button-danger-active-color: rgb(var(--ring-button-danger-active-components)); /* #ffe7e8 */\n --ring-button-loader-background-components: 51, 163, 255;\n --ring-button-loader-background: rgb(var(--ring-button-loader-background-components)); /* #33a3ff */\n --ring-button-primary-background-components: 26, 152, 255;\n --ring-button-primary-background-color: rgb(var(--ring-button-primary-background-components)); /* #1a98ff */\n --ring-table-loader-background-color: rgba(var(--ring-content-background-components), 0.5); /* #ffffff80 */\n\n /* Code */\n --ring-code-background-color: var(--ring-content-background-color);\n --ring-code-components: 0, 0, 0;\n --ring-code-color: rgb(var(--ring-code-components)); /* #000 */\n --ring-code-comment-components: 112, 112, 112;\n --ring-code-comment-color: rgb(var(--ring-code-comment-components)); /* #707070 */\n --ring-code-meta-components: 112, 112, 112;\n --ring-code-meta-color: rgb(var(--ring-code-meta-components)); /* #707070 */\n --ring-code-keyword-components: 0, 0, 128;\n --ring-code-keyword-color: rgb(var(--ring-code-keyword-components)); /* #000080 */\n --ring-code-tag-background-components: 239, 239, 239;\n --ring-code-tag-background-color: rgb(var(--ring-code-tag-background-components)); /* #efefef */\n --ring-code-tag-color: var(--ring-code-keyword-color);\n --ring-code-tag-font-weight: bold;\n --ring-code-field-components: 102, 14, 122;\n --ring-code-field-color: rgb(var(--ring-code-field-components)); /* #660e7a */\n --ring-code-attribute-components: 0, 0, 255;\n --ring-code-attribute-color: rgb(var(--ring-code-attribute-components)); /* #00f */\n --ring-code-number-color: var(--ring-code-attribute-color);\n --ring-code-string-components: 0, 122, 0;\n --ring-code-string-color: rgb(var(--ring-code-string-components)); /* #007a00 */\n --ring-code-addition-components: 170, 222, 170;\n --ring-code-addition-color: rgb(var(--ring-code-addition-components)); /* #aadeaa */\n --ring-code-deletion-components: 200, 200, 200;\n --ring-code-deletion-color: rgb(var(--ring-code-deletion-components)); /* #c8c8c8 */\n\n /* Metrics */\n --ring-border-radius: 4px;\n --ring-border-radius-small: 2px;\n --ring-font-size-larger: 15px;\n --ring-font-size: 14px;\n --ring-font-size-smaller: 12px;\n --ring-line-height-taller: 21px;\n --ring-line-height: 20px;\n --ring-line-height-lower: 18px;\n --ring-line-height-lowest: 16px;\n --ring-ease: 0.3s ease-out;\n --ring-fast-ease: 0.15s ease-out;\n --ring-font-family: system-ui, Arial, sans-serif;\n --ring-font-family-monospace:\n Menlo,\n "Bitstream Vera Sans Mono",\n "Ubuntu Mono",\n Consolas,\n "Courier New",\n Courier,\n monospace;\n\n /* Common z-index-values */\n\n /* Invisible element is an absolutely positioned element which should be below */\n /* all other elements on the page */\n --ring-invisible-element-z-index: -1;\n\n /* z-index for position: fixed elements */\n --ring-fixed-z-index: 1;\n\n /* Elements that should overlay all other elements on the page */\n --ring-overlay-z-index: 5;\n\n /* Alerts should de displayed above overlays */\n --ring-alert-z-index: 6;\n}\n'],sourceRoot:""}]),a.locals={light:"light_f331"};const l=a},9173:(e,n,t)=>{"use strict";t.d(n,{A:()=>l});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i)()(o());a.push([e.id,"/* stylelint-disable color-no-hex */\n\n.ring-ui-theme-dark,\n.dark_d4a9,\n:root.dark_d4a9 {\n --ring-line-components: 71, 81, 89;\n --ring-line-color: rgb(var(--ring-line-components)); /* #475159 */\n --ring-borders-components: 64, 99, 128;\n --ring-borders-color: rgb(var(--ring-borders-components)); /* #406380 */\n --ring-icon-components: 128, 146, 157;\n --ring-icon-color: rgb(var(--ring-icon-components)); /* #80929d */\n --ring-icon-secondary-components: 128, 146, 157;\n --ring-icon-secondary-color: rgb(var(--ring-icon-secondary-components)); /* #80929d */\n --ring-border-disabled-components: 54, 54, 54;\n --ring-border-disabled-color: rgb(var(--ring-border-disabled-components)); /* #363636 */\n --ring-border-selected-disabled-components: 54, 54, 54;\n --ring-border-selected-disabled-color: rgb(var(--ring-border-selected-disabled-components)); /* #363636 */\n --ring-border-unselected-disabled-components: 54, 54, 54;\n --ring-border-unselected-disabled-color: rgb(var(--ring-border-unselected-disabled-components)); /* #363636 */ /* TODO remove in 6.0 */\n --ring-icon-disabled-components: 80, 82, 83;\n --ring-icon-disabled-color: rgb(var(--ring-icon-disabled-components)); /* #505253 */\n --ring-border-hover-components: 112, 177, 230;\n --ring-border-hover-color: rgb(var(--ring-border-hover-components)); /* #70b1e6 */\n --ring-main-components: 0, 142, 255;\n --ring-main-color: rgb(var(--ring-main-components)); /* #008eff */\n --ring-action-link-components: var(--ring-main-components);\n --ring-action-link-color: rgb(var(--ring-main-components)); /* #008eff */\n --ring-main-hover-components: 0, 126, 229;\n --ring-main-hover-color: rgb(var(--ring-main-hover-components)); /* #007ee5 */\n --ring-icon-error-components: 219, 88, 96;\n --ring-icon-error-color: rgb(var(--ring-icon-error-components)); /* #db5860 */\n --ring-icon-warning-components: 237, 162, 0;\n --ring-icon-warning-color: rgb(var(--ring-icon-warning-components)); /* #eda200 */\n --ring-icon-success-components: 71, 212, 100;\n --ring-icon-success-color: rgb(var(--ring-icon-success-components)); /* #47d464 */\n --ring-popup-border-components: 0, 42, 76;\n --ring-popup-border-color: rgba(var(--ring-popup-border-components), 0.1);\n --ring-popup-shadow-color: rgba(var(--ring-popup-border-components), 0.15);\n --ring-message-shadow-color: rgba(var(--ring-popup-border-components), 0.3);\n --ring-pinned-shadow-components: 0, 0, 0;\n --ring-pinned-shadow-color: rgb(var(--ring-pinned-shadow-components)); /* #000 */\n --ring-button-danger-hover-color: var(--ring-error-color);\n --ring-button-primary-border-components: 128, 198, 255;\n --ring-button-primary-border-color: rgb(var(--ring-button-primary-border-components)); /* #80c6ff */\n\n /* Text */\n --ring-hint-components: 128, 146, 157;\n --ring-hint-color: rgb(var(--ring-hint-components)); /* #80929d */\n --ring-link-components: 112, 177, 230;\n --ring-link-color: rgb(var(--ring-link-components)); /* #70b1e6 */\n --ring-error-components: 219, 88, 96;\n --ring-error-color: rgb(var(--ring-error-components)); /* #db5860 */\n --ring-warning-components: 237, 162, 0;\n --ring-warning-color: rgb(var(--ring-warning-components)); /* #eda200 */\n --ring-success-components: 71, 212, 100;\n --ring-success-color: rgb(var(--ring-success-components)); /* #47d464 */\n --ring-text-components: 187, 187, 187;\n --ring-text-color: rgb(var(--ring-text-components)); /* #bbb */\n --ring-active-text-components: 255, 255, 255;\n --ring-active-text-color: rgb(var(--ring-active-text-components)); /* #fff */\n --ring-heading-color: var(--ring-text-color);\n --ring-secondary-components: 128, 146, 157;\n --ring-secondary-color: rgb(var(--ring-secondary-components)); /* #80929d */\n --ring-disabled-components: 81, 95, 104;\n --ring-disabled-color: rgb(var(--ring-disabled-components)); /* #515F68 */\n\n /* Background */\n --ring-content-background-components: 35, 39, 43;\n --ring-content-background-color: rgb(var(--ring-content-background-components)); /* #23272b */\n --ring-popup-background-components: 17, 19, 20;\n --ring-popup-background-color: rgb(var(--ring-popup-background-components)); /* #111314 */\n --ring-sidebar-background-components: 40, 52, 61;\n --ring-sidebar-background-color: rgb(var(--ring-sidebar-background-components)); /* #28343d */\n --ring-selected-background-components: 6, 38, 64;\n --ring-selected-background-color: rgb(var(--ring-selected-background-components)); /* #062640 */\n --ring-hover-background-components: 11, 26, 38;\n --ring-hover-background-color: rgb(var(--ring-hover-background-components)); /* #0b1a26 */\n --ring-navigation-background-components: 17, 19, 20;\n --ring-navigation-background-color: rgb(var(--ring-navigation-background-components)); /* #111314 */\n --ring-tag-background-components: 62, 77, 89;\n --ring-tag-background-color: rgb(var(--ring-tag-background-components)); /* #3e4d59 */\n --ring-tag-hover-background-components: 51, 62, 71;\n --ring-tag-hover-background-color: rgb(var(--ring-tag-hover-background-components)); /* #333e47 */\n --ring-removed-background-components: 143, 82, 71;\n --ring-removed-background-color: rgb(var(--ring-removed-background-components)); /* #8f5247 */\n --ring-warning-background-components: 89, 61, 1;\n --ring-warning-background-color: rgb(var(--ring-warning-background-components)); /* #593d01 */\n --ring-added-background-components: 54, 89, 71;\n --ring-added-background-color: rgb(var(--ring-added-background-components)); /* #365947 */\n --ring-disabled-background-components: 44, 47, 51;\n --ring-disabled-background-color: rgb(var(--ring-disabled-background-components)); /* #2C2F33 */\n --ring-disabled-selected-background-components: 44, 47, 51;\n --ring-disabled-selected-background-color: rgb(var(--ring-disabled-selected-background-components)); /* #2C2F33 */\n --ring-button-danger-active-components: 38, 8, 10;\n --ring-button-danger-active-color: rgb(var(--ring-button-danger-active-components)); /* #26080a */\n --ring-button-primary-background-components: 0, 126, 229;\n --ring-button-primary-background-color: rgb(var(--ring-button-primary-background-components)); /* #007ee5 */\n --ring-table-loader-background-color: rgba(var(--ring-content-background-components), 0.5); /* #23272b80 */\n\n /* Code */\n --ring-code-background-components: 43, 43, 43;\n --ring-code-background-color: rgb(var(--ring-code-background-components)); /* #2b2b2b */\n --ring-code-components: 169, 183, 198;\n --ring-code-color: rgb(var(--ring-code-components)); /* #a9b7c6 */\n --ring-code-meta-components: 187, 181, 41;\n --ring-code-meta-color: rgb(var(--ring-code-meta-components)); /* #bbb529 */\n --ring-code-keyword-components: 204, 120, 50;\n --ring-code-keyword-color: rgb(var(--ring-code-keyword-components)); /* #cc7832 */\n --ring-code-tag-background-components: 43, 43, 43;\n --ring-code-tag-background-color: rgb(var(--ring-code-tag-background-components)); /* #2b2b2b */\n --ring-code-tag-components: 232, 191, 106;\n --ring-code-tag-color: rgb(var(--ring-code-tag-components)); /* #e8bf6a */\n --ring-code-tag-font-weight: normal;\n --ring-code-field-components: 152, 118, 170;\n --ring-code-field-color: rgb(var(--ring-code-tag-font-weight)); /* #9876aa */\n --ring-code-attribute-components: 186, 186, 186;\n --ring-code-attribute-color: rgb(var(--ring-code-attribute-components)); /* #bababa */\n --ring-code-number-components: 104, 151, 187;\n --ring-code-number-color: rgb(var(--ring-code-number-components)); /* #6897bb */\n --ring-code-string-components: 106, 135, 89;\n --ring-code-string-color: rgb(var(--ring-code-string-components)); /* #6a8759 */\n --ring-code-addition-components: 68, 113, 82;\n --ring-code-addition-color: rgb(var(--ring-code-addition-components)); /* #447152 */\n --ring-code-deletion-components: 101, 110, 118;\n --ring-code-deletion-color: rgb(var(--ring-code-deletion-components)); /* #656e76 */\n\n color-scheme: dark;\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/global/variables_dark.css"],names:[],mappings:"AAAA,mCAAmC;;AAEnC;;;EAGE,kCAAkC;EAClC,mDAAmD,EAAE,YAAY;EACjE,sCAAsC;EACtC,yDAAyD,EAAE,YAAY;EACvE,qCAAqC;EACrC,mDAAmD,EAAE,YAAY;EACjE,+CAA+C;EAC/C,uEAAuE,EAAE,YAAY;EACrF,6CAA6C;EAC7C,yEAAyE,EAAE,YAAY;EACvF,sDAAsD;EACtD,2FAA2F,EAAE,YAAY;EACzG,wDAAwD;EACxD,+FAA+F,EAAE,YAAY,EAAE,uBAAuB;EACtI,2CAA2C;EAC3C,qEAAqE,EAAE,YAAY;EACnF,6CAA6C;EAC7C,mEAAmE,EAAE,YAAY;EACjF,mCAAmC;EACnC,mDAAmD,EAAE,YAAY;EACjE,0DAA0D;EAC1D,0DAA0D,EAAE,YAAY;EACxE,yCAAyC;EACzC,+DAA+D,EAAE,YAAY;EAC7E,yCAAyC;EACzC,+DAA+D,EAAE,YAAY;EAC7E,2CAA2C;EAC3C,mEAAmE,EAAE,YAAY;EACjF,4CAA4C;EAC5C,mEAAmE,EAAE,YAAY;EACjF,yCAAyC;EACzC,yEAAyE;EACzE,0EAA0E;EAC1E,2EAA2E;EAC3E,wCAAwC;EACxC,qEAAqE,EAAE,SAAS;EAChF,yDAAyD;EACzD,sDAAsD;EACtD,qFAAqF,EAAE,YAAY;;EAEnG,SAAS;EACT,qCAAqC;EACrC,mDAAmD,EAAE,YAAY;EACjE,qCAAqC;EACrC,mDAAmD,EAAE,YAAY;EACjE,oCAAoC;EACpC,qDAAqD,EAAE,YAAY;EACnE,sCAAsC;EACtC,yDAAyD,EAAE,YAAY;EACvE,uCAAuC;EACvC,yDAAyD,EAAE,YAAY;EACvE,qCAAqC;EACrC,mDAAmD,EAAE,SAAS;EAC9D,4CAA4C;EAC5C,iEAAiE,EAAE,SAAS;EAC5E,4CAA4C;EAC5C,0CAA0C;EAC1C,6DAA6D,EAAE,YAAY;EAC3E,uCAAuC;EACvC,2DAA2D,EAAE,YAAY;;EAEzE,eAAe;EACf,gDAAgD;EAChD,+EAA+E,EAAE,YAAY;EAC7F,8CAA8C;EAC9C,2EAA2E,EAAE,YAAY;EACzF,gDAAgD;EAChD,+EAA+E,EAAE,YAAY;EAC7F,gDAAgD;EAChD,iFAAiF,EAAE,YAAY;EAC/F,8CAA8C;EAC9C,2EAA2E,EAAE,YAAY;EACzF,mDAAmD;EACnD,qFAAqF,EAAE,YAAY;EACnG,4CAA4C;EAC5C,uEAAuE,EAAE,YAAY;EACrF,kDAAkD;EAClD,mFAAmF,EAAE,YAAY;EACjG,iDAAiD;EACjD,+EAA+E,EAAE,YAAY;EAC7F,+CAA+C;EAC/C,+EAA+E,EAAE,YAAY;EAC7F,8CAA8C;EAC9C,2EAA2E,EAAE,YAAY;EACzF,iDAAiD;EACjD,iFAAiF,EAAE,YAAY;EAC/F,0DAA0D;EAC1D,mGAAmG,EAAE,YAAY;EACjH,iDAAiD;EACjD,mFAAmF,EAAE,YAAY;EACjG,wDAAwD;EACxD,6FAA6F,EAAE,YAAY;EAC3G,0FAA0F,EAAE,cAAc;;EAE1G,SAAS;EACT,6CAA6C;EAC7C,yEAAyE,EAAE,YAAY;EACvF,qCAAqC;EACrC,mDAAmD,EAAE,YAAY;EACjE,yCAAyC;EACzC,6DAA6D,EAAE,YAAY;EAC3E,4CAA4C;EAC5C,mEAAmE,EAAE,YAAY;EACjF,iDAAiD;EACjD,iFAAiF,EAAE,YAAY;EAC/F,yCAAyC;EACzC,2DAA2D,EAAE,YAAY;EACzE,mCAAmC;EACnC,2CAA2C;EAC3C,8DAA8D,EAAE,YAAY;EAC5E,+CAA+C;EAC/C,uEAAuE,EAAE,YAAY;EACrF,4CAA4C;EAC5C,iEAAiE,EAAE,YAAY;EAC/E,2CAA2C;EAC3C,iEAAiE,EAAE,YAAY;EAC/E,4CAA4C;EAC5C,qEAAqE,EAAE,YAAY;EACnF,8CAA8C;EAC9C,qEAAqE,EAAE,YAAY;;EAEnF,kBAAkB;AACpB",sourcesContent:["/* stylelint-disable color-no-hex */\n\n:global(.ring-ui-theme-dark),\n.dark,\n:root.dark {\n --ring-line-components: 71, 81, 89;\n --ring-line-color: rgb(var(--ring-line-components)); /* #475159 */\n --ring-borders-components: 64, 99, 128;\n --ring-borders-color: rgb(var(--ring-borders-components)); /* #406380 */\n --ring-icon-components: 128, 146, 157;\n --ring-icon-color: rgb(var(--ring-icon-components)); /* #80929d */\n --ring-icon-secondary-components: 128, 146, 157;\n --ring-icon-secondary-color: rgb(var(--ring-icon-secondary-components)); /* #80929d */\n --ring-border-disabled-components: 54, 54, 54;\n --ring-border-disabled-color: rgb(var(--ring-border-disabled-components)); /* #363636 */\n --ring-border-selected-disabled-components: 54, 54, 54;\n --ring-border-selected-disabled-color: rgb(var(--ring-border-selected-disabled-components)); /* #363636 */\n --ring-border-unselected-disabled-components: 54, 54, 54;\n --ring-border-unselected-disabled-color: rgb(var(--ring-border-unselected-disabled-components)); /* #363636 */ /* TODO remove in 6.0 */\n --ring-icon-disabled-components: 80, 82, 83;\n --ring-icon-disabled-color: rgb(var(--ring-icon-disabled-components)); /* #505253 */\n --ring-border-hover-components: 112, 177, 230;\n --ring-border-hover-color: rgb(var(--ring-border-hover-components)); /* #70b1e6 */\n --ring-main-components: 0, 142, 255;\n --ring-main-color: rgb(var(--ring-main-components)); /* #008eff */\n --ring-action-link-components: var(--ring-main-components);\n --ring-action-link-color: rgb(var(--ring-main-components)); /* #008eff */\n --ring-main-hover-components: 0, 126, 229;\n --ring-main-hover-color: rgb(var(--ring-main-hover-components)); /* #007ee5 */\n --ring-icon-error-components: 219, 88, 96;\n --ring-icon-error-color: rgb(var(--ring-icon-error-components)); /* #db5860 */\n --ring-icon-warning-components: 237, 162, 0;\n --ring-icon-warning-color: rgb(var(--ring-icon-warning-components)); /* #eda200 */\n --ring-icon-success-components: 71, 212, 100;\n --ring-icon-success-color: rgb(var(--ring-icon-success-components)); /* #47d464 */\n --ring-popup-border-components: 0, 42, 76;\n --ring-popup-border-color: rgba(var(--ring-popup-border-components), 0.1);\n --ring-popup-shadow-color: rgba(var(--ring-popup-border-components), 0.15);\n --ring-message-shadow-color: rgba(var(--ring-popup-border-components), 0.3);\n --ring-pinned-shadow-components: 0, 0, 0;\n --ring-pinned-shadow-color: rgb(var(--ring-pinned-shadow-components)); /* #000 */\n --ring-button-danger-hover-color: var(--ring-error-color);\n --ring-button-primary-border-components: 128, 198, 255;\n --ring-button-primary-border-color: rgb(var(--ring-button-primary-border-components)); /* #80c6ff */\n\n /* Text */\n --ring-hint-components: 128, 146, 157;\n --ring-hint-color: rgb(var(--ring-hint-components)); /* #80929d */\n --ring-link-components: 112, 177, 230;\n --ring-link-color: rgb(var(--ring-link-components)); /* #70b1e6 */\n --ring-error-components: 219, 88, 96;\n --ring-error-color: rgb(var(--ring-error-components)); /* #db5860 */\n --ring-warning-components: 237, 162, 0;\n --ring-warning-color: rgb(var(--ring-warning-components)); /* #eda200 */\n --ring-success-components: 71, 212, 100;\n --ring-success-color: rgb(var(--ring-success-components)); /* #47d464 */\n --ring-text-components: 187, 187, 187;\n --ring-text-color: rgb(var(--ring-text-components)); /* #bbb */\n --ring-active-text-components: 255, 255, 255;\n --ring-active-text-color: rgb(var(--ring-active-text-components)); /* #fff */\n --ring-heading-color: var(--ring-text-color);\n --ring-secondary-components: 128, 146, 157;\n --ring-secondary-color: rgb(var(--ring-secondary-components)); /* #80929d */\n --ring-disabled-components: 81, 95, 104;\n --ring-disabled-color: rgb(var(--ring-disabled-components)); /* #515F68 */\n\n /* Background */\n --ring-content-background-components: 35, 39, 43;\n --ring-content-background-color: rgb(var(--ring-content-background-components)); /* #23272b */\n --ring-popup-background-components: 17, 19, 20;\n --ring-popup-background-color: rgb(var(--ring-popup-background-components)); /* #111314 */\n --ring-sidebar-background-components: 40, 52, 61;\n --ring-sidebar-background-color: rgb(var(--ring-sidebar-background-components)); /* #28343d */\n --ring-selected-background-components: 6, 38, 64;\n --ring-selected-background-color: rgb(var(--ring-selected-background-components)); /* #062640 */\n --ring-hover-background-components: 11, 26, 38;\n --ring-hover-background-color: rgb(var(--ring-hover-background-components)); /* #0b1a26 */\n --ring-navigation-background-components: 17, 19, 20;\n --ring-navigation-background-color: rgb(var(--ring-navigation-background-components)); /* #111314 */\n --ring-tag-background-components: 62, 77, 89;\n --ring-tag-background-color: rgb(var(--ring-tag-background-components)); /* #3e4d59 */\n --ring-tag-hover-background-components: 51, 62, 71;\n --ring-tag-hover-background-color: rgb(var(--ring-tag-hover-background-components)); /* #333e47 */\n --ring-removed-background-components: 143, 82, 71;\n --ring-removed-background-color: rgb(var(--ring-removed-background-components)); /* #8f5247 */\n --ring-warning-background-components: 89, 61, 1;\n --ring-warning-background-color: rgb(var(--ring-warning-background-components)); /* #593d01 */\n --ring-added-background-components: 54, 89, 71;\n --ring-added-background-color: rgb(var(--ring-added-background-components)); /* #365947 */\n --ring-disabled-background-components: 44, 47, 51;\n --ring-disabled-background-color: rgb(var(--ring-disabled-background-components)); /* #2C2F33 */\n --ring-disabled-selected-background-components: 44, 47, 51;\n --ring-disabled-selected-background-color: rgb(var(--ring-disabled-selected-background-components)); /* #2C2F33 */\n --ring-button-danger-active-components: 38, 8, 10;\n --ring-button-danger-active-color: rgb(var(--ring-button-danger-active-components)); /* #26080a */\n --ring-button-primary-background-components: 0, 126, 229;\n --ring-button-primary-background-color: rgb(var(--ring-button-primary-background-components)); /* #007ee5 */\n --ring-table-loader-background-color: rgba(var(--ring-content-background-components), 0.5); /* #23272b80 */\n\n /* Code */\n --ring-code-background-components: 43, 43, 43;\n --ring-code-background-color: rgb(var(--ring-code-background-components)); /* #2b2b2b */\n --ring-code-components: 169, 183, 198;\n --ring-code-color: rgb(var(--ring-code-components)); /* #a9b7c6 */\n --ring-code-meta-components: 187, 181, 41;\n --ring-code-meta-color: rgb(var(--ring-code-meta-components)); /* #bbb529 */\n --ring-code-keyword-components: 204, 120, 50;\n --ring-code-keyword-color: rgb(var(--ring-code-keyword-components)); /* #cc7832 */\n --ring-code-tag-background-components: 43, 43, 43;\n --ring-code-tag-background-color: rgb(var(--ring-code-tag-background-components)); /* #2b2b2b */\n --ring-code-tag-components: 232, 191, 106;\n --ring-code-tag-color: rgb(var(--ring-code-tag-components)); /* #e8bf6a */\n --ring-code-tag-font-weight: normal;\n --ring-code-field-components: 152, 118, 170;\n --ring-code-field-color: rgb(var(--ring-code-tag-font-weight)); /* #9876aa */\n --ring-code-attribute-components: 186, 186, 186;\n --ring-code-attribute-color: rgb(var(--ring-code-attribute-components)); /* #bababa */\n --ring-code-number-components: 104, 151, 187;\n --ring-code-number-color: rgb(var(--ring-code-number-components)); /* #6897bb */\n --ring-code-string-components: 106, 135, 89;\n --ring-code-string-color: rgb(var(--ring-code-string-components)); /* #6a8759 */\n --ring-code-addition-components: 68, 113, 82;\n --ring-code-addition-color: rgb(var(--ring-code-addition-components)); /* #447152 */\n --ring-code-deletion-components: 101, 110, 118;\n --ring-code-deletion-color: rgb(var(--ring-code-deletion-components)); /* #656e76 */\n\n color-scheme: dark;\n}\n"],sourceRoot:""}]),a.locals={dark:"dark_d4a9"};const l=a},5066:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,'.icon_aaa7 {\n display: inline-block;\n\n fill: currentColor;\n}\n\n.glyph_f986 {\n display: inline-flex;\n\n margin-right: -1px;\n margin-left: -1px;\n\n pointer-events: none;\n}\n\n.glyph_f986[width="10"] {\n vertical-align: -1px;\n }\n\n.glyph_f986[width="14"] {\n margin-right: -2px;\n margin-left: 0;\n\n vertical-align: -3px;\n }\n\n.glyph_f986[width="16"] {\n vertical-align: -3px;\n }\n\n.glyph_f986[width="20"] {\n vertical-align: -2px;\n }\n\n.glyph_f986.compatibilityMode_d631 {\n width: 16px;\n height: 16px;\n margin-right: 0;\n margin-left: 0;\n }\n\n/* HACK: This media query hack makes styles applied for WebKit browsers only */\n/* stylelint-disable-next-line media-feature-name-no-vendor-prefix */\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n .glyph_f986 {\n width: auto; /* Safari size bug workaround, see https://youtrack.jetbrains.com/issue/RG-1983 */\n }\n}\n\n.gray_f6a8 {\n color: var(--ring-icon-secondary-color);\n}\n\n.hover_fc27 {\n color: var(--ring-icon-hover-color);\n}\n\n.green_bfb1 {\n color: var(--ring-icon-success-color);\n}\n\n.magenta_b045 {\n color: var(--ring-link-hover-color);\n}\n\n.red_a7ec {\n color: var(--ring-icon-error-color);\n}\n\n.blue_ec1e {\n color: var(--ring-main-color);\n}\n\n.white_c896 {\n color: var(--ring-white-text-color);\n}\n\n.loading_c5e2 {\n animation-name: icon-loading_fe22;\n animation-duration: 1200ms;\n animation-iteration-count: infinite;\n}\n\n@keyframes icon-loading_fe22 {\n 0% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(0.9);\n\n opacity: 0.5;\n }\n\n 100% {\n transform: scale(1);\n }\n}\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/icon/icon.css"],names:[],mappings:"AAIA;EACE,qBAAqB;;EAErB,kBAAkB;AACpB;;AAEA;EACE,oBAAoB;;EAEpB,kBAAkB;EAClB,iBAAiB;;EAEjB,oBAAoB;AA2BtB;;AAzBE;IACE,oBAAoB;EACtB;;AAEA;IACE,kBAAkB;IAClB,cAAc;;IAEd,oBAAoB;EACtB;;AAEA;IACE,oBAAoB;EACtB;;AAEA;IACE,oBAAoB;EACtB;;AAEA;IACE,WAAqB;IACrB,YAAsB;IACtB,eAAe;IACf,cAAc;EAChB;;AAGF,8EAA8E;AAC9E,oEAAoE;AACpE;EACE;IACE,WAAW,EAAE,iFAAiF;EAChG;AACF;;AAEA;EACE,uCAAuC;AACzC;;AAEA;EACE,mCAAmC;AACrC;;AAEA;EACE,qCAAqC;AACvC;;AAEA;EACE,mCAAmC;AACrC;;AAEA;EACE,mCAAmC;AACrC;;AAEA;EACE,6BAA6B;AAC/B;;AAEA;EACE,mCAAmC;AACrC;;AAEA;EACE,iCAA4B;EAC5B,0BAA0B;EAC1B,mCAAmC;AACrC;;AAEA;EACE;IACE,mBAAmB;EACrB;;EAEA;IACE,qBAAqB;;IAErB,YAAY;EACd;;EAEA;IACE,mBAAmB;EACrB;AACF",sourcesContent:['@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n\n.icon {\n display: inline-block;\n\n fill: currentColor;\n}\n\n.glyph {\n display: inline-flex;\n\n margin-right: -1px;\n margin-left: -1px;\n\n pointer-events: none;\n\n &[width="10"] {\n vertical-align: -1px;\n }\n\n &[width="14"] {\n margin-right: -2px;\n margin-left: 0;\n\n vertical-align: -3px;\n }\n\n &[width="16"] {\n vertical-align: -3px;\n }\n\n &[width="20"] {\n vertical-align: -2px;\n }\n\n &.compatibilityMode {\n width: calc(unit * 2);\n height: calc(unit * 2);\n margin-right: 0;\n margin-left: 0;\n }\n}\n\n/* HACK: This media query hack makes styles applied for WebKit browsers only */\n/* stylelint-disable-next-line media-feature-name-no-vendor-prefix */\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n .glyph {\n width: auto; /* Safari size bug workaround, see https://youtrack.jetbrains.com/issue/RG-1983 */\n }\n}\n\n.gray {\n color: var(--ring-icon-secondary-color);\n}\n\n.hover {\n color: var(--ring-icon-hover-color);\n}\n\n.green {\n color: var(--ring-icon-success-color);\n}\n\n.magenta {\n color: var(--ring-link-hover-color);\n}\n\n.red {\n color: var(--ring-icon-error-color);\n}\n\n.blue {\n color: var(--ring-main-color);\n}\n\n.white {\n color: var(--ring-white-text-color);\n}\n\n.loading {\n animation-name: icon-loading;\n animation-duration: 1200ms;\n animation-iteration-count: infinite;\n}\n\n@keyframes icon-loading {\n 0% {\n transform: scale(1);\n }\n\n 50% {\n transform: scale(0.9);\n\n opacity: 0.5;\n }\n\n 100% {\n transform: scale(1);\n }\n}\n'],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,icon:"icon_aaa7",glyph:"glyph_f986",compatibilityMode:"compatibilityMode_d631",gray:"gray_f6a8",hover:"hover_fc27",green:"green_bfb1",magenta:"magenta_b045",red:"red_a7ec",blue:"blue_ec1e",white:"white_c896",loading:"loading_c5e2","icon-loading":"icon-loading_fe22"};const u=s},8976:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,":root {\n --ring-input-xs: 96px;\n --ring-input-s: 96px;\n --ring-input-m: 240px;\n --ring-input-l: 400px;\n}\n\n/**\n * @name Input Sizes\n */\n\n/* XS */\n\n.ring-input-size_xs.ring-input-size_xs {\n display: inline-block;\n\n width: 96px;\n\n width: var(--ring-input-xs);\n}\n\n.ring-input-size_xs.ring-input-size_xs ~ .ring-error-bubble {\n left: 98px;\n left: calc(var(--ring-input-xs) + 2px);\n}\n\n/* S */\n\n.ring-input-size_s.ring-input-size_s {\n display: inline-block;\n\n width: 96px;\n\n width: var(--ring-input-s);\n}\n\n.ring-input-size_s.ring-input-size_s ~ .ring-error-bubble {\n left: 98px;\n left: calc(var(--ring-input-s) + 2px);\n}\n\n/* M */\n\n.ring-input-size_m.ring-input-size_m {\n display: inline-block;\n\n width: 240px;\n\n width: var(--ring-input-m);\n}\n\n.ring-input-size_m.ring-input-size_m ~ .ring-error-bubble {\n left: 242px;\n left: calc(var(--ring-input-m) + 2px);\n}\n\n.ring-input-size_md.ring-input-size_md {\n display: inline-block;\n\n width: 240px;\n\n width: var(--ring-input-m);\n}\n\n.ring-input-size_md.ring-input-size_md ~ .ring-error-bubble {\n left: 242px;\n left: calc(var(--ring-input-m) + 2px);\n}\n\n/* L */\n\n.ring-input-size_l.ring-input-size_l {\n display: inline-block;\n\n width: 400px;\n\n width: var(--ring-input-l);\n}\n\n.ring-input-size_l.ring-input-size_l ~ .ring-error-bubble {\n left: 402px;\n left: calc(var(--ring-input-l) + 2px);\n}\n\n.ring-input-height_s.ring-input-height_s {\n --ring-input-padding-block: 1px;\n}\n\n.ring-input-height_m.ring-input-height_m {\n --ring-input-padding-block: 3px;\n}\n\n.ring-input-height_l.ring-input-height_l {\n --ring-input-padding-block: 5px;\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/input-size/input-size.css"],names:[],mappings:"AAIA;EACE,qBAAgC;EAChC,oBAA+B;EAC/B,qBAA+B;EAC/B,qBAA+B;AACjC;;AAEA;;EAEE;;AAEF,OAAO;;AAEP;EACE,qBAAqB;;EAErB,WAA2B;;EAA3B,2BAA2B;AAC7B;;AAEA;EACE,UAAsC;EAAtC,sCAAsC;AACxC;;AAEA,MAAM;;AAEN;EACE,qBAAqB;;EAErB,WAA0B;;EAA1B,0BAA0B;AAC5B;;AAEA;EACE,UAAqC;EAArC,qCAAqC;AACvC;;AAEA,MAAM;;AAEN;EACE,qBAAqB;;EAErB,YAA0B;;EAA1B,0BAA0B;AAC5B;;AAEA;EACE,WAAqC;EAArC,qCAAqC;AACvC;;AAEA;EACE,qBAAqB;;EAErB,YAA0B;;EAA1B,0BAA0B;AAC5B;;AAEA;EACE,WAAqC;EAArC,qCAAqC;AACvC;;AAEA,MAAM;;AAEN;EACE,qBAAqB;;EAErB,YAA0B;;EAA1B,0BAA0B;AAC5B;;AAEA;EACE,WAAqC;EAArC,qCAAqC;AACvC;;AAEA;EACE,+BAA+B;AACjC;;AAEA;EACE,+BAA+B;AACjC;;AAEA;EACE,+BAA+B;AACjC",sourcesContent:['@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n\n:root {\n --ring-input-xs: calc(unit * 12);\n --ring-input-s: calc(unit * 12);\n --ring-input-m: calc(unit * 30);\n --ring-input-l: calc(unit * 50);\n}\n\n/**\n * @name Input Sizes\n */\n\n/* XS */\n\n:global(.ring-input-size_xs.ring-input-size_xs) {\n display: inline-block;\n\n width: var(--ring-input-xs);\n}\n\n:global(.ring-input-size_xs.ring-input-size_xs ~ .ring-error-bubble) {\n left: calc(var(--ring-input-xs) + 2px);\n}\n\n/* S */\n\n:global(.ring-input-size_s.ring-input-size_s) {\n display: inline-block;\n\n width: var(--ring-input-s);\n}\n\n:global(.ring-input-size_s.ring-input-size_s ~ .ring-error-bubble) {\n left: calc(var(--ring-input-s) + 2px);\n}\n\n/* M */\n\n:global(.ring-input-size_m.ring-input-size_m) {\n display: inline-block;\n\n width: var(--ring-input-m);\n}\n\n:global(.ring-input-size_m.ring-input-size_m ~ .ring-error-bubble) {\n left: calc(var(--ring-input-m) + 2px);\n}\n\n:global(.ring-input-size_md.ring-input-size_md) {\n display: inline-block;\n\n width: var(--ring-input-m);\n}\n\n:global(.ring-input-size_md.ring-input-size_md ~ .ring-error-bubble) {\n left: calc(var(--ring-input-m) + 2px);\n}\n\n/* L */\n\n:global(.ring-input-size_l.ring-input-size_l) {\n display: inline-block;\n\n width: var(--ring-input-l);\n}\n\n:global(.ring-input-size_l.ring-input-size_l ~ .ring-error-bubble) {\n left: calc(var(--ring-input-l) + 2px);\n}\n\n:global(.ring-input-height_s.ring-input-height_s) {\n --ring-input-padding-block: 1px;\n}\n\n:global(.ring-input-height_m.ring-input-height_m) {\n --ring-input-padding-block: 3px;\n}\n\n:global(.ring-input-height_l.ring-input-height_l) {\n --ring-input-padding-block: 5px;\n}\n'],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`};const u=s},8266:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>p});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=t(9892),u=a()(o());u.i(c.A),u.i(s.default),u.i(l.default,"",!0),u.push([e.id,'.outerContainer_cb70 {\n --ring-input-icon-offset: 20px;\n --ring-input-padding-inline: 8px;\n --ring-input-background-color: var(--ring-content-background-color);\n}\n\n.borderless_f79b {\n /* stylelint-disable-next-line length-zero-no-unit */\n --ring-input-padding-inline: 0px;\n}\n\n.container_ee33 {\n position: relative;\n\n box-sizing: border-box;\n\n font-size: var(--ring-font-size);\n line-height: var(--ring-line-height);\n}\n\n.container_ee33 * {\n box-sizing: border-box;\n }\n\n.input_f220 {\n --ring-input-padding-start: var(--ring-input-padding-inline);\n --ring-input-padding-end: var(--ring-input-padding-inline);\n\n width: 100%;\n\n margin: 0;\n padding-top: var(--ring-input-padding-block);\n padding-right: var(--ring-input-padding-end);\n padding-bottom: var(--ring-input-padding-block);\n padding-left: var(--ring-input-padding-start);\n\n transition: border-color var(--ring-ease);\n\n color: var(--ring-text-color);\n border: 1px solid var(--ring-borders-color);\n border-radius: var(--ring-border-radius);\n outline: none;\n background-color: var(--ring-input-background-color);\n\n font: inherit;\n\n caret-color: var(--ring-main-color);\n}\n\n[dir="rtl"] .input_f220 {\n padding-right: var(--ring-input-padding-start);\n padding-left: var(--ring-input-padding-end);\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.input_f220:hover {\n transition: none;\n\n border-color: var(--ring-border-hover-color);\n }}\n\n.error_ff90 .input_f220 {\n border-color: var(--ring-icon-error-color);\n }\n\n.input_f220:focus {\n transition: none;\n\n border-color: var(--ring-main-color);\n }\n\n.input_f220[disabled] {\n color: var(--ring-disabled-color);\n border-color: var(--ring-border-disabled-color);\n background-color: var(--ring-disabled-background-color);\n\n -webkit-text-fill-color: var(--ring-disabled-color); /* Required for Safari, see RG-2063 for details */\n }\n\n/*\n Kill yellow/blue webkit autocomplete\n https://css-tricks.com/snippets/css/change-autocomplete-styles-webkit-browsers/\n */\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.input_f220:-webkit-autofill:hover {\n -webkit-transition: background-color 50000s ease-in-out 0s;\n transition: background-color 50000s ease-in-out 0s;\n }}\n\n.input_f220:-webkit-autofill,\n .input_f220:-webkit-autofill:focus {\n -webkit-transition: background-color 50000s ease-in-out 0s;\n transition: background-color 50000s ease-in-out 0s;\n }\n\n.borderless_f79b .input_f220 {\n border-color: transparent;\n background-color: transparent;\n}\n\n.withIcon_f066 .input_f220 {\n --ring-input-padding-start: calc(var(--ring-input-padding-inline) + var(--ring-input-icon-offset));\n}\n\n.clearable_fd1e .input_f220 {\n --ring-input-padding-end: calc(var(--ring-input-padding-inline) + var(--ring-input-icon-offset));\n}\n\n.icon_e49c {\n position: absolute;\n top: calc(var(--ring-input-padding-block) + 1px);\n left: var(--ring-input-padding-inline);\n\n pointer-events: none;\n\n color: var(--ring-icon-secondary-color);\n}\n\n[dir="rtl"] .icon_e49c {\n right: 8px;\n left: auto;\n }\n\n.clear_ffc3 {\n position: absolute;\n top: calc(var(--ring-input-padding-block) + 2px);\n right: var(--ring-input-padding-inline);\n\n height: auto;\n\n padding-right: 0;\n\n line-height: inherit;\n}\n\n.empty_cc0d .clear_ffc3 {\n display: none;\n }\n\n[dir="rtl"] .clear_ffc3 {\n right: auto;\n left: 8px;\n }\n\ntextarea.input_f220 {\n overflow: hidden;\n\n box-sizing: border-box;\n\n resize: none;\n}\n\n.input_f220::-moz-placeholder {\n color: var(--ring-disabled-color);\n}\n\n.input_f220::placeholder {\n color: var(--ring-disabled-color);\n}\n\n.input_f220::-webkit-search-cancel-button {\n -webkit-appearance: none;\n}\n\n.errorText_e447 {\n margin-top: 4px;\n\n color: var(--ring-error-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n\n.sizeS_c560 {\n width: 96px;\n}\n\n.sizeM_aee6 {\n width: 240px;\n}\n\n.sizeL_b0ca {\n width: 400px;\n}\n\n.sizeFULL_f4f9 {\n width: 100%;\n}\n\n.heightS_a68d {\n --ring-input-padding-block: 1px;\n}\n\n.heightM_bc35 {\n --ring-input-padding-block: 3px;\n}\n\n.heightL_f82d {\n --ring-input-padding-block: 5px;\n}\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/input/input.css",""],names:[],mappings:"AAKA;EACE,8BAA0C;EAC1C,gCAAiC;EACjC,mEAAmE;AACrE;;AAEA;EACE,oDAAoD;EACpD,gCAAgC;AAClC;;AAEA;EACE,kBAAkB;;EAElB,sBAAsB;;EAEtB,gCAAgC;EAChC,oCAAoC;AAKtC;;AAHE;IACE,sBAAsB;EACxB;;AAGF;EACE,4DAA4D;EAC5D,0DAA0D;;EAE1D,WAAW;;EAEX,SAAS;EACT,4CAA4C;EAC5C,4CAA4C;EAC5C,+CAA+C;EAC/C,6CAA6C;;EAE7C,yCAAyC;;EAEzC,6BAA6B;EAC7B,2CAA2C;EAC3C,wCAAwC;EACxC,aAAa;EACb,oDAAoD;;EAEpD,aAAa;;EAEb,mCAAmC;AA0CrC;;AAxCE;IACE,8CAA8C;IAC9C,2CAA2C;EAC7C;;ACxDF,wGAAA;IAAA,iBAAA;;IAAA,6CAAA;GAAA,CAAA;;ADgEE;IACE,0CAA0C;EAC5C;;AAEA;IACE,gBAAgB;;IAEhB,oCAAoC;EACtC;;AAEA;IACE,iCAAiC;IACjC,+CAA+C;IAC/C,uDAAuD;;IAEvD,mDAAmD,EAAE,iDAAiD;EACxG;;AAEA;;;GAGC;;ACrFH,wGAAA;MAAA,2DAAA;MAAA,mDAAA;KAAA,CAAA;;ADuFI;;MAGE,0DAAkD;MAAlD,kDAAkD;IACpD;;AAIJ;EACE,yBAAyB;EACzB,6BAA6B;AAC/B;;AAEA;EACE,kGAAkG;AACpG;;AAEA;EACE,gGAAgG;AAClG;;AAEA;EACE,kBAAkB;EAClB,gDAAgD;EAChD,sCAAsC;;EAEtC,oBAAoB;;EAEpB,uCAAuC;AAMzC;;AAJE;IACE,UAAW;IACX,UAAU;EACZ;;AAGF;EACE,kBAAkB;EAClB,gDAAgD;EAChD,uCAAuC;;EAEvC,YAAY;;EAEZ,gBAAgB;;EAEhB,oBAAoB;AAUtB;;AARE;IACE,aAAa;EACf;;AAEA;IACE,WAAW;IACX,SAAU;EACZ;;AAGF;EACE,gBAAgB;;EAEhB,sBAAsB;;EAEtB,YAAY;AACd;;AAEA;EACE,iCAAiC;AACnC;;AAFA;EACE,iCAAiC;AACnC;;AAEA;EACE,wBAAwB;AAC1B;;AAEA;EACE,eAA0B;;EAE1B,8BAA8B;;EAE9B,wCAAwC;EACxC,2CAA2C;AAC7C;;AAEA;EACE,WAAsB;AACxB;;AAEA;EACE,YAAsB;AACxB;;AAEA;EACE,YAAsB;AACxB;;AAEA;EACE,WAAW;AACb;;AAEA;EACE,+BAA+B;AACjC;;AAEA;EACE,+BAA+B;AACjC;;AAEA;EACE,+BAA+B;AACjC",sourcesContent:['@import "../global/variables.css";\n@import "../button/button.css";\n\n@value unit from "../global/global.css";\n\n.outerContainer {\n --ring-input-icon-offset: calc(unit * 2.5);\n --ring-input-padding-inline: unit;\n --ring-input-background-color: var(--ring-content-background-color);\n}\n\n.borderless {\n /* stylelint-disable-next-line length-zero-no-unit */\n --ring-input-padding-inline: 0px;\n}\n\n.container {\n position: relative;\n\n box-sizing: border-box;\n\n font-size: var(--ring-font-size);\n line-height: var(--ring-line-height);\n\n & * {\n box-sizing: border-box;\n }\n}\n\n.input {\n --ring-input-padding-start: var(--ring-input-padding-inline);\n --ring-input-padding-end: var(--ring-input-padding-inline);\n\n width: 100%;\n\n margin: 0;\n padding-top: var(--ring-input-padding-block);\n padding-right: var(--ring-input-padding-end);\n padding-bottom: var(--ring-input-padding-block);\n padding-left: var(--ring-input-padding-start);\n\n transition: border-color var(--ring-ease);\n\n color: var(--ring-text-color);\n border: 1px solid var(--ring-borders-color);\n border-radius: var(--ring-border-radius);\n outline: none;\n background-color: var(--ring-input-background-color);\n\n font: inherit;\n\n caret-color: var(--ring-main-color);\n\n [dir="rtl"] & {\n padding-right: var(--ring-input-padding-start);\n padding-left: var(--ring-input-padding-end);\n }\n\n &:hover {\n transition: none;\n\n border-color: var(--ring-border-hover-color);\n }\n\n .error & {\n border-color: var(--ring-icon-error-color);\n }\n\n &:focus {\n transition: none;\n\n border-color: var(--ring-main-color);\n }\n\n &[disabled] {\n color: var(--ring-disabled-color);\n border-color: var(--ring-border-disabled-color);\n background-color: var(--ring-disabled-background-color);\n\n -webkit-text-fill-color: var(--ring-disabled-color); /* Required for Safari, see RG-2063 for details */\n }\n\n /*\n Kill yellow/blue webkit autocomplete\n https://css-tricks.com/snippets/css/change-autocomplete-styles-webkit-browsers/\n */\n &:-webkit-autofill {\n &,\n &:hover,\n &:focus {\n transition: background-color 50000s ease-in-out 0s;\n }\n }\n}\n\n.borderless .input {\n border-color: transparent;\n background-color: transparent;\n}\n\n.withIcon .input {\n --ring-input-padding-start: calc(var(--ring-input-padding-inline) + var(--ring-input-icon-offset));\n}\n\n.clearable .input {\n --ring-input-padding-end: calc(var(--ring-input-padding-inline) + var(--ring-input-icon-offset));\n}\n\n.icon {\n position: absolute;\n top: calc(var(--ring-input-padding-block) + 1px);\n left: var(--ring-input-padding-inline);\n\n pointer-events: none;\n\n color: var(--ring-icon-secondary-color);\n\n [dir="rtl"] & {\n right: unit;\n left: auto;\n }\n}\n\n.clear {\n position: absolute;\n top: calc(var(--ring-input-padding-block) + 2px);\n right: var(--ring-input-padding-inline);\n\n height: auto;\n\n padding-right: 0;\n\n line-height: inherit;\n\n .empty & {\n display: none;\n }\n\n [dir="rtl"] & {\n right: auto;\n left: unit;\n }\n}\n\ntextarea.input {\n overflow: hidden;\n\n box-sizing: border-box;\n\n resize: none;\n}\n\n.input::placeholder {\n color: var(--ring-disabled-color);\n}\n\n.input::-webkit-search-cancel-button {\n -webkit-appearance: none;\n}\n\n.errorText {\n margin-top: calc(unit / 2);\n\n color: var(--ring-error-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n\n.sizeS {\n width: calc(unit * 12);\n}\n\n.sizeM {\n width: calc(unit * 30);\n}\n\n.sizeL {\n width: calc(unit * 50);\n}\n\n.sizeFULL {\n width: 100%;\n}\n\n.heightS {\n --ring-input-padding-block: 1px;\n}\n\n.heightM {\n --ring-input-padding-block: 3px;\n}\n\n.heightL {\n --ring-input-padding-block: 5px;\n}\n',null],sourceRoot:""}]),u.locals={unit:`${l.default.locals.unit}`,outerContainer:"outerContainer_cb70",borderless:"borderless_f79b",container:"container_ee33",input:"input_f220",error:"error_ff90",withIcon:"withIcon_f066",clearable:"clearable_fd1e",icon:"icon_e49c",clear:"clear_ffc3",empty:"empty_cc0d",errorText:"errorText_e447",sizeS:"sizeS_c560",sizeM:"sizeM_aee6",sizeL:"sizeL_b0ca",sizeFULL:"sizeFULL_f4f9",heightS:"heightS_a68d",heightM:"heightM_bc35",heightL:"heightL_f82d"};const p=u},6960:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>s});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(5280),c=a()(o());c.i(l.A),c.push([e.id,".link_e6e5 {\n cursor: pointer;\n transition: color var(--ring-fast-ease);\n\n color: var(--ring-link-color);\n\n outline: none;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.link_e6e5:hover {\n transition: none;\n\n color: var(--ring-link-hover-color);\n }}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.link_e6e5:hover {\n text-decoration: none;\n }}\n\n.link_e6e5 {\n text-decoration: none;\n }\n\n.link_e6e5.hover_bed7 {\n transition: none;\n\n color: var(--ring-link-hover-color);\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.link_e6e5:hover .inner_e3ba {\n border-width: 0;\n border-bottom: 2px solid;\n border-image-source: linear-gradient(currentcolor 50%, transparent 50%);\n border-image-slice: 0 0 100% 0;\n }}\n\n.link_e6e5.active_f804 {\n color: inherit;\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.link_e6e5.compatibilityUnderlineMode_e7a0:hover {\n text-decoration: underline;\n\n /* stylelint-disable-next-line selector-max-specificity */\n }\n .link_e6e5.compatibilityUnderlineMode_e7a0:hover .inner_e3ba {\n border: none;\n }}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.link_e6e5.pseudo_d9ae:hover {\n text-decoration: none;\n\n /* stylelint-disable-next-line selector-max-specificity */\n }\n .link_e6e5.pseudo_d9ae:hover .inner_e3ba {\n border: none;\n }}\n\n.link_e6e5:focus-visible {\n box-shadow: 0 0 0 2px var(--ring-border-hover-color);\n }\n\n@media (min-resolution: 2dppx) {@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.link_e6e5:hover .inner_e3ba {\n border-bottom-width: 1px;\n }}\n}\n\n.text_e98a {\n border-radius: var(--ring-border-radius);\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.inherit_d267:not(:hover) {\n color: inherit;\n}}\n\n.pseudo_d9ae {\n margin: 0;\n padding: 0;\n\n text-align: left;\n\n border: 0;\n\n background: transparent;\n\n font: inherit;\n}\n\n.pseudo_d9ae::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n }\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/link/link.css",""],names:[],mappings:"AAEA;EACE,eAAe;EACf,uCAAuC;;EAEvC,6BAA6B;;EA2C7B,aAAa;AAKf;;ACtDA,wGAAA;IAAA,iBAAA;;IAAA,oCAAA;GAAA,CAAA;;AAAA,wGAAA;IAAA,sBAAA;GAAA,CAAA;;ADQE;IAEE,qBAAqB;EACvB;;AAEA;IAEE,gBAAgB;;IAEhB,mCAAmC;EACrC;;AClBF,wGAAA;IAAA,gBAAA;IAAA,yBAAA;IAAA,wEAAA;IAAA,+BAAA;GAAA,CAAA;;AD2BE;IACE,cAAc;EAChB;;AC7BF,wGAAA;IAAA,2BAAA;;IAAA,0DAAA;GAAA;IAAA;MAAA,aAAA;KAAA,CAAA;;AAAA,wGAAA;IAAA,sBAAA;;IAAA,0DAAA;GAAA;IAAA;MAAA,aAAA;KAAA,CAAA;;ADmDE;IACE,oDAAoD;EACtD;;AAGF,gCCxDA,wGAAA;IAAA,yBAAA;GAAA,CAAA;AD4DA;;AAEA;EACE,wCAAwC;AAC1C;;AChEA,wGAAA;EAAA,eAAA;CAAA,CAAA;;ADsEA;EACE,SAAS;EACT,UAAU;;EAEV,gBAAgB;;EAEhB,SAAS;;EAET,uBAAuB;;EAEvB,aAAa;AAOf;;AALE;IACE,UAAU;;IAEV,SAAS;EACX",sourcesContent:['@import "../global/variables.css";\n\n.link {\n cursor: pointer;\n transition: color var(--ring-fast-ease);\n\n color: var(--ring-link-color);\n\n &,\n &:hover {\n text-decoration: none;\n }\n\n &:hover,\n &.hover {\n transition: none;\n\n color: var(--ring-link-hover-color);\n }\n\n &:hover .inner {\n border-width: 0;\n border-bottom: 2px solid;\n border-image-source: linear-gradient(currentcolor 50%, transparent 50%);\n border-image-slice: 0 0 100% 0;\n }\n\n &.active {\n color: inherit;\n }\n\n &.compatibilityUnderlineMode:hover {\n text-decoration: underline;\n\n /* stylelint-disable-next-line selector-max-specificity */\n & .inner {\n border: none;\n }\n }\n\n &.pseudo:hover {\n text-decoration: none;\n\n /* stylelint-disable-next-line selector-max-specificity */\n & .inner {\n border: none;\n }\n }\n\n outline: none;\n\n &:focus-visible {\n box-shadow: 0 0 0 2px var(--ring-border-hover-color);\n }\n}\n\n@media (min-resolution: 2dppx) {\n .link:hover .inner {\n border-bottom-width: 1px;\n }\n}\n\n.text {\n border-radius: var(--ring-border-radius);\n}\n\n.inherit:not(:hover) {\n color: inherit;\n}\n\n.pseudo {\n margin: 0;\n padding: 0;\n\n text-align: left;\n\n border: 0;\n\n background: transparent;\n\n font: inherit;\n\n &::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n }\n}\n',null],sourceRoot:""}]),c.locals={link:"link_e6e5",hover:"hover_bed7",inner:"inner_e3ba",active:"active_f804",compatibilityUnderlineMode:"compatibilityUnderlineMode_e7a0",pseudo:"pseudo_d9ae",text:"text_e98a",inherit:"inherit_d267"};const s=c},480:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,'.list_a01c {\n position: relative;\n\n z-index: 1;\n\n border-radius: var(--ring-border-radius);\n\n line-height: normal;\n}\n\n.simpleInner_a4f8 {\n overflow: auto;\n}\n\n.scrolling_a910 {\n pointer-events: none;\n}\n\n.separator_c26e {\n display: block;\n\n min-height: 8px;\n\n margin-top: 8px;\n padding: 0 16px 1px;\n\n text-align: right;\n white-space: nowrap;\n\n color: var(--ring-secondary-color);\n border-top: 1px solid var(--ring-line-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lower);\n}\n\n.separator_first_ec9e {\n margin-top: 0;\n padding-top: 0;\n\n border: none;\n}\n\n.item_eadd {\n display: block;\n\n box-sizing: border-box;\n\n width: 100%;\n\n text-align: left;\n vertical-align: bottom;\n white-space: nowrap;\n text-decoration: none;\n\n outline: none;\n\n font-size: var(--ring-font-size);\n}\n\n.item_eadd.item_eadd {\n padding: 3px 16px 5px;\n\n line-height: 24px;\n}\n\n.itemContainer_f365 {\n position: relative;\n}\n\n.compact_efa8 {\n line-height: 16px;\n}\n\n.error_aa15 {\n cursor: default;\n}\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.error_aa15:hover {\n color: var(--ring-error-color);\n }}\n\n/* Override ring-link */\n\n.error_aa15,\n .error_aa15:focus,\n .error_aa15:visited {\n color: var(--ring-error-color);\n }\n\n.add_a8da {\n padding: 8px 16px;\n\n line-height: 32px;\n}\n\n.top_c4d5 {\n display: flex;\n align-items: baseline;\n flex-direction: row;\n}\n\n.left_ea6b {\n align-self: center;\n flex-shrink: 0;\n}\n\n.label_dac9 {\n overflow: hidden;\n flex-grow: 1;\n flex-shrink: 1;\n\n text-align: left;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n[dir="rtl"] .label_dac9 {\n text-align: right;\n direction: ltr;\n }\n\n.description_efcc {\n overflow: hidden;\n flex-shrink: 100;\n\n padding-left: 8px;\n\n text-align: right;\n white-space: nowrap;\n text-overflow: ellipsis;\n\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n font-weight: 400;\n line-height: var(--ring-line-height-lowest);\n}\n\n.right_df77 {\n display: flex;\n align-items: center;\n align-self: center;\n flex-direction: row;\n flex-shrink: 0;\n}\n\n.details_a2b7 {\n margin-bottom: 6px;\n\n white-space: normal;\n\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n\n.padded_a74d {\n margin-left: 20px;\n}\n\n/* Override :last-child */\n.hint_d29d.hint_d29d {\n margin-bottom: 0;\n\n border-top: 1px solid var(--ring-line-color);\n background-color: var(--ring-sidebar-background-color);\n\n font-size: var(--ring-font-size-smaller);\n}\n\n.action_d10e {\n cursor: pointer;\n\n color: var(--ring-text-color);\n}\n\n/* override link */\n.actionLink_a4c7.actionLink_a4c7 {\n transition: none;\n}\n\n.hover_a4cd:not(.error_aa15) {\n background-color: var(--ring-selected-background-color);\n}\n\n.icon_f1f3 {\n display: inline-block;\n\n width: 20px;\n height: 20px;\n margin-left: 16px;\n\n background-repeat: no-repeat;\n background-position: center;\n\n background-size: contain;\n}\n\n.highlight_e4dd {\n color: var(--ring-link-hover-color);\n}\n\n.service_a4fc {\n color: var(--ring-secondary-color);\n}\n\n.glyph_dfd5 {\n float: left;\n\n width: 20px;\n\n margin-right: 8px;\n\n color: var(--ring-icon-secondary-color);\n}\n\n.avatar_f258 {\n\n top: 0;\n\n height: 20px;\n\n -o-object-fit: cover;\n\n object-fit: cover;\n -o-object-position: center;\n object-position: center;\n}\n\n.rightGlyph_fb77 {\n\n float: right;\n\n margin-right: 0;\n margin-left: 16px;\n}\n\n.checkboxContainer_c949 {\n position: absolute;\n top: 7px;\n left: 19px;\n\n width: 20px;\n height: 20px;\n margin-right: 8px;\n}\n\n.compact_efa8 .checkboxContainer_c949 {\n top: 0;\n\n width: 16px;\n height: 16px;\n}\n\n.title_e1bf {\n display: block;\n\n margin-top: 10px;\n margin-bottom: 6px;\n padding: 8px 16px 0;\n\n text-align: left;\n}\n\n[dir="rtl"] .title_e1bf {\n text-align: right;\n direction: ltr;\n }\n\n.title_first_ac55 {\n margin-top: 0;\n}\n\n.text_fe0e {\n letter-spacing: 1.5px;\n text-transform: uppercase;\n\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n}\n\n.fade_d35c {\n position: absolute;\n bottom: 0;\n\n width: 100%;\n height: 24px;\n\n pointer-events: none;\n\n background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--ring-content-background-color));\n}\n\n.disabled_c3d8 {\n pointer-events: none;\n\n color: var(--ring-disabled-color);\n}\n',"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/list/list.css",""],names:[],mappings:"AAKA;EACE,kBAAkB;;EAElB,UAAU;;EAEV,wCAAwC;;EAExC,mBAAmB;AACrB;;AAEA;EACE,cAAc;AAChB;;AAEA;EACE,oBAAoB;AACtB;;AAEA;EACE,cAAc;;EAEd,eAAuB;;EAEvB,eAAuB;EACvB,mBAA6B;;EAE7B,iBAAiB;EACjB,mBAAmB;;EAEnB,kCAAkC;EAClC,4CAA4C;;EAE5C,wCAAwC;EACxC,0CAA0C;AAC5C;;AAEA;EACE,aAAa;EACb,cAAc;;EAEd,YAAY;AACd;;AAEA;EACE,cAAc;;EAEd,sBAAsB;;EAEtB,WAAW;;EAEX,gBAAgB;EAChB,sBAAsB;EACtB,mBAAmB;EACnB,qBAAqB;;EAErB,aAAa;;EAEb,gCAAgC;AAClC;;AAEA;EACE,qBAA+B;;EAE/B,iBAA2B;AAC7B;;AAEA;EACE,kBAAkB;AACpB;;AAEA;EACE,iBAA2B;AAC7B;;AAEA;EACE,eAAe;AASjB;;ACzFA,wGAAA;IAAA,+BAAA;GAAA,CAAA;;ADkFE,uBAAuB;;AACvB;;;IAIE,8BAA8B;EAChC;;AAGF;EACE,iBAA4B;;EAE5B,iBAA2B;AAC7B;;AAEA;EACE,aAAa;EACb,qBAAqB;EACrB,mBAAmB;AACrB;;AAEA;EACE,kBAAkB;EAClB,cAAc;AAChB;;AAEA;EACE,gBAAgB;EAChB,YAAY;EACZ,cAAc;;EAEd,gBAAgB;EAChB,mBAAmB;EACnB,uBAAuB;AAMzB;;AAJE;IACE,iBAAiB;IACjB,cAAc;EAChB;;AAGF;EACE,gBAAgB;EAChB,gBAAgB;;EAEhB,iBAAkB;;EAElB,iBAAiB;EACjB,mBAAmB;EACnB,uBAAuB;;EAEvB,kCAAkC;;EAElC,wCAAwC;EACxC,gBAAgB;EAChB,2CAA2C;AAC7C;;AAEA;EACE,aAAa;EACb,mBAAmB;EACnB,kBAAkB;EAClB,mBAAmB;EACnB,cAAc;AAChB;;AAEA;EACE,kBAAkB;;EAElB,mBAAmB;;EAEnB,kCAAkC;;EAElC,wCAAwC;EACxC,2CAA2C;AAC7C;;AAEA;EACE,iBAAiB;AACnB;;AAEA,yBAAyB;AACzB;EACE,gBAAgB;;EAEhB,4CAA4C;EAC5C,sDAAsD;;EAEtD,wCAAwC;AAC1C;;AAEA;EACE,eAAe;;EAEf,6BAA6B;AAC/B;;AAEA,kBAAkB;AAClB;EACE,gBAAgB;AAClB;;AAEA;EACE,uDAAuD;AACzD;;AAEA;EACE,qBAAqB;;EAErB,WAAW;EACX,YAAY;EACZ,iBAA2B;;EAE3B,4BAA4B;EAC5B,2BAA2B;;EAE3B,wBAAwB;AAC1B;;AAEA;EACE,mCAAmC;AACrC;;AAEA;EACE,kCAAkC;AACpC;;AAEA;EACE,WAAW;;EAEX,WAAW;;EAEX,iBAAkB;;EAElB,uCAAuC;AACzC;;AAEA;;EAGE,MAAM;;EAEN,YAAY;;EAEZ,oBAAiB;;KAAjB,iBAAiB;EACjB,0BAAuB;KAAvB,uBAAuB;AACzB;;AAEA;;EAGE,YAAY;;EAEZ,eAAe;EACf,iBAA2B;AAC7B;;AAEA;EACE,kBAAkB;EAClB,QAAQ;EACR,UAAU;;EAEV,WAAW;EACX,YAAY;EACZ,iBAAkB;AACpB;;AAEA;EACE,MAAM;;EAEN,WAAqB;EACrB,YAAsB;AACxB;;AAEA;EACE,cAAc;;EAEd,gBAAgB;EAChB,kBAAkB;EAClB,mBAAqC;;EAErC,gBAAgB;AAMlB;;AAJE;IACE,iBAAiB;IACjB,cAAc;EAChB;;AAGF;EACE,aAAa;AACf;;AAEA;EACE,qBAAqB;EACrB,yBAAyB;;EAEzB,kCAAkC;;EAElC,wCAAwC;AAC1C;;AAEA;EACE,kBAAkB;EAClB,SAAS;;EAET,WAAW;EACX,YAAsB;;EAEtB,oBAAoB;;EAEpB,oGAAoG;AACtG;;AAEA;EACE,oBAAoB;;EAEpB,iCAAiC;AACnC",sourcesContent:['@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n@value listSpacing: unit;\n\n.list {\n position: relative;\n\n z-index: 1;\n\n border-radius: var(--ring-border-radius);\n\n line-height: normal;\n}\n\n.simpleInner {\n overflow: auto;\n}\n\n.scrolling {\n pointer-events: none;\n}\n\n.separator {\n display: block;\n\n min-height: listSpacing;\n\n margin-top: listSpacing;\n padding: 0 calc(unit * 2) 1px;\n\n text-align: right;\n white-space: nowrap;\n\n color: var(--ring-secondary-color);\n border-top: 1px solid var(--ring-line-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lower);\n}\n\n.separator_first {\n margin-top: 0;\n padding-top: 0;\n\n border: none;\n}\n\n.item {\n display: block;\n\n box-sizing: border-box;\n\n width: 100%;\n\n text-align: left;\n vertical-align: bottom;\n white-space: nowrap;\n text-decoration: none;\n\n outline: none;\n\n font-size: var(--ring-font-size);\n}\n\n.item.item {\n padding: 3px calc(unit * 2) 5px;\n\n line-height: calc(unit * 3);\n}\n\n.itemContainer {\n position: relative;\n}\n\n.compact {\n line-height: calc(unit * 2);\n}\n\n.error {\n cursor: default;\n\n /* Override ring-link */\n &,\n &:hover,\n &:focus,\n &:visited {\n color: var(--ring-error-color);\n }\n}\n\n.add {\n padding: unit calc(2 * unit);\n\n line-height: calc(4 * unit);\n}\n\n.top {\n display: flex;\n align-items: baseline;\n flex-direction: row;\n}\n\n.left {\n align-self: center;\n flex-shrink: 0;\n}\n\n.label {\n overflow: hidden;\n flex-grow: 1;\n flex-shrink: 1;\n\n text-align: left;\n white-space: nowrap;\n text-overflow: ellipsis;\n\n [dir="rtl"] & {\n text-align: right;\n direction: ltr;\n }\n}\n\n.description {\n overflow: hidden;\n flex-shrink: 100;\n\n padding-left: unit;\n\n text-align: right;\n white-space: nowrap;\n text-overflow: ellipsis;\n\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n font-weight: 400;\n line-height: var(--ring-line-height-lowest);\n}\n\n.right {\n display: flex;\n align-items: center;\n align-self: center;\n flex-direction: row;\n flex-shrink: 0;\n}\n\n.details {\n margin-bottom: 6px;\n\n white-space: normal;\n\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n\n.padded {\n margin-left: 20px;\n}\n\n/* Override :last-child */\n.hint.hint {\n margin-bottom: 0;\n\n border-top: 1px solid var(--ring-line-color);\n background-color: var(--ring-sidebar-background-color);\n\n font-size: var(--ring-font-size-smaller);\n}\n\n.action {\n cursor: pointer;\n\n color: var(--ring-text-color);\n}\n\n/* override link */\n.actionLink.actionLink {\n transition: none;\n}\n\n.hover:not(.error) {\n background-color: var(--ring-selected-background-color);\n}\n\n.icon {\n display: inline-block;\n\n width: 20px;\n height: 20px;\n margin-left: calc(unit * 2);\n\n background-repeat: no-repeat;\n background-position: center;\n\n background-size: contain;\n}\n\n.highlight {\n color: var(--ring-link-hover-color);\n}\n\n.service {\n color: var(--ring-secondary-color);\n}\n\n.glyph {\n float: left;\n\n width: 20px;\n\n margin-right: unit;\n\n color: var(--ring-icon-secondary-color);\n}\n\n.avatar {\n composes: glyph;\n\n top: 0;\n\n height: 20px;\n\n object-fit: cover;\n object-position: center;\n}\n\n.rightGlyph {\n composes: glyph;\n\n float: right;\n\n margin-right: 0;\n margin-left: calc(unit * 2);\n}\n\n.checkboxContainer {\n position: absolute;\n top: 7px;\n left: 19px;\n\n width: 20px;\n height: 20px;\n margin-right: unit;\n}\n\n.compact .checkboxContainer {\n top: 0;\n\n width: calc(unit * 2);\n height: calc(unit * 2);\n}\n\n.title {\n display: block;\n\n margin-top: 10px;\n margin-bottom: 6px;\n padding: listSpacing calc(unit * 2) 0;\n\n text-align: left;\n\n [dir="rtl"] & {\n text-align: right;\n direction: ltr;\n }\n}\n\n.title_first {\n margin-top: 0;\n}\n\n.text {\n letter-spacing: 1.5px;\n text-transform: uppercase;\n\n color: var(--ring-secondary-color);\n\n font-size: var(--ring-font-size-smaller);\n}\n\n.fade {\n position: absolute;\n bottom: 0;\n\n width: 100%;\n height: calc(unit * 3);\n\n pointer-events: none;\n\n background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--ring-content-background-color));\n}\n\n.disabled {\n pointer-events: none;\n\n color: var(--ring-disabled-color);\n}\n',null],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,listSpacing:"8px",list:"list_a01c",simpleInner:"simpleInner_a4f8",scrolling:"scrolling_a910",separator:"separator_c26e",separator_first:"separator_first_ec9e",item:"item_eadd",itemContainer:"itemContainer_f365",compact:"compact_efa8",error:"error_aa15",add:"add_a8da",top:"top_c4d5",left:"left_ea6b",label:"label_dac9",description:"description_efcc",right:"right_df77",details:"details_a2b7",padded:"padded_a74d",hint:"hint_d29d",action:"action_d10e",actionLink:"actionLink_a4c7",hover:"hover_a4cd",icon:"icon_f1f3",highlight:"highlight_e4dd",service:"service_a4fc",glyph:"glyph_dfd5",avatar:"avatar_f258 glyph_dfd5",rightGlyph:"rightGlyph_fb77 glyph_dfd5",checkboxContainer:"checkboxContainer_c949",title:"title_e1bf",title_first:"title_first_ac55",text:"text_fe0e",fade:"fade_d35c",disabled:"disabled_c3d8"};const u=s},1586:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>p});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9173),c=t(9106),s=t(5280),u=a()(o());u.i(s.A),u.i(l.A,"",!0),u.i(c.default,"",!0),u.push([e.id,`:root {\n /* stylelint-disable-next-line color-no-hex */\n --ring-loader-inline-stops: #ff00eb, #bd3bff, #008eff, #58ba00, #f48700, #ff00eb;\n}\n\n.${l.A.locals.dark},\n.ring-ui-theme-dark {\n /* stylelint-disable-next-line color-no-hex */\n --ring-loader-inline-stops: #ff2eef, #d178ff, #289fff, #88d444, #ffe000, #ff2eef;\n}\n\n@keyframes spin_ad60 {\n 0% {\n transform: rotate(0);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n}\n\n@keyframes pulse_c906 {\n 0% {\n transform: scale(1);\n }\n\n 100% {\n transform: scale(1.41667);\n }\n}\n\n.loader_d294,\n.ring-loader-inline {\n /* needed for better backward-compatibility */\n\n position: relative;\n\n display: inline-block;\n\n overflow: hidden;\n\n transform: rotate(0);\n animation: spin_ad60 1s linear infinite;\n vertical-align: -3px;\n\n border-radius: 8px;\n}\n\n.loader_d294,\n .ring-loader-inline,\n .loader_d294::after,\n .ring-loader-inline::after {\n transform-origin: 50% 50%;\n }\n\n.loader_d294::after, .ring-loader-inline::after {\n display: block;\n\n width: 16px;\n height: 16px;\n\n content: "";\n animation: pulse_c906 0.85s cubic-bezier(0.68, 0, 0.74, 0.74) infinite alternate;\n\n background-image: conic-gradient(#ff00eb, #bd3bff, #008eff, #58ba00, #f48700, #ff00eb);\n\n background-image: conic-gradient(var(--ring-loader-inline-stops));\n -webkit-mask-image: radial-gradient(8px, transparent 71.875%, var(--ring-content-background-color) 71.875%);\n mask-image: radial-gradient(8px, transparent 71.875%, var(--ring-content-background-color) 71.875%);\n }\n\n.children_ece6 {\n margin-left: 4px;\n}\n`,"",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/loader-inline/loader-inline.css"],names:[],mappings:"AAKA;EACE,6CAA6C;EAC7C,gFAAgF;AAClF;;AAEA;;EAEE,6CAA6C;EAC7C,gFAAgF;AAClF;;AAEA;EACE;IACE,oBAAoB;EACtB;;EAEA;IACE,yBAAyB;EAC3B;AACF;;AAEA;EACE;IACE,mBAAmB;EACrB;;EAEA;IACE,yBAA+B;EACjC;AACF;;AAEA;;EAEE,6CAA6C;;EAE7C,kBAAkB;;EAElB,qBAAqB;;EAErB,gBAAgB;;EAEhB,oBAAoB;EACpB,uCAAkC;EAClC,oBAAoB;;EAEpB,kBAAmB;AAmBrB;;AAjBE;;;;IAEE,yBAAyB;EAC3B;;AAEA;IACE,cAAc;;IAEd,WAAqB;IACrB,YAAsB;;IAEtB,WAAW;IACX,gFAA2E;;IAE3E,sFAAiE;;IAAjE,iEAAiE;IACjE,2GAAoG;YAApG,mGAAoG;EACtG;;AAGF;EACE,gBAA2B;AAC7B",sourcesContent:['@import "../global/variables.css";\n\n@value dark from "../global/variables_dark.css";\n@value unit from "../global/global.css";\n\n:root {\n /* stylelint-disable-next-line color-no-hex */\n --ring-loader-inline-stops: #ff00eb, #bd3bff, #008eff, #58ba00, #f48700, #ff00eb;\n}\n\n.dark,\n:global(.ring-ui-theme-dark) {\n /* stylelint-disable-next-line color-no-hex */\n --ring-loader-inline-stops: #ff2eef, #d178ff, #289fff, #88d444, #ffe000, #ff2eef;\n}\n\n@keyframes spin {\n 0% {\n transform: rotate(0);\n }\n\n 100% {\n transform: rotate(360deg);\n }\n}\n\n@keyframes pulse {\n 0% {\n transform: scale(1);\n }\n\n 100% {\n transform: scale(calc(17 / 12));\n }\n}\n\n.loader,\n:global(.ring-loader-inline) {\n /* needed for better backward-compatibility */\n\n position: relative;\n\n display: inline-block;\n\n overflow: hidden;\n\n transform: rotate(0);\n animation: spin 1s linear infinite;\n vertical-align: -3px;\n\n border-radius: unit;\n\n &,\n &::after {\n transform-origin: 50% 50%;\n }\n\n &::after {\n display: block;\n\n width: calc(unit * 2);\n height: calc(unit * 2);\n\n content: "";\n animation: pulse 0.85s cubic-bezier(0.68, 0, 0.74, 0.74) infinite alternate;\n\n background-image: conic-gradient(var(--ring-loader-inline-stops));\n mask-image: radial-gradient(unit, transparent 71.875%, var(--ring-content-background-color) 71.875%);\n }\n}\n\n.children {\n margin-left: calc(unit / 2);\n}\n'],sourceRoot:""}]),u.locals={dark:`${l.A.locals.dark}`,unit:`${c.default.locals.unit}`,loader:"loader_d294",spin:"spin_ad60",pulse:"pulse_c906",children:"children_ece6"};const p=u},8890:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,".popup_f35e {\n\n position: fixed;\n z-index: var(--ring-overlay-z-index);\n top: -100vh;\n left: -100vw;\n\n overflow-y: auto;\n\n box-sizing: border-box;\n\n border: 1px solid var(--ring-popup-border-color);\n border-radius: var(--ring-border-radius);\n\n background-color: var(--ring-popup-background-color);\n box-shadow: var(--ring-popup-shadow);\n}\n\n.hidden_c587 {\n display: none;\n}\n\n.showing_b07a {\n opacity: 0;\n}\n\n.attached_ea95 {\n border-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/popup/popup.css"],names:[],mappings:"AAEA;;EAGE,eAAe;EACf,oCAAoC;EACpC,WAAW;EACX,YAAY;;EAEZ,gBAAgB;;EAEhB,sBAAsB;;EAEtB,gDAAgD;EAChD,wCAAwC;;EAExC,oDAAoD;EACpD,oCAAoC;AACtC;;AAEA;EACE,aAAa;AACf;;AAEA;EACE,UAAU;AACZ;;AAEA;EACE,aAAa;EACb,yBAAyB;EACzB,0BAA0B;AAC5B",sourcesContent:['@import "../global/variables.css";\n\n.popup {\n composes: font from "../global/global.css";\n\n position: fixed;\n z-index: var(--ring-overlay-z-index);\n top: -100vh;\n left: -100vw;\n\n overflow-y: auto;\n\n box-sizing: border-box;\n\n border: 1px solid var(--ring-popup-border-color);\n border-radius: var(--ring-border-radius);\n\n background-color: var(--ring-popup-background-color);\n box-shadow: var(--ring-popup-shadow);\n}\n\n.hidden {\n display: none;\n}\n\n.showing {\n opacity: 0;\n}\n\n.attached {\n border-top: 0;\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n}\n'],sourceRoot:""}]),s.locals={popup:`popup_f35e ${l.default.locals.font}`,hidden:"hidden_c587",showing:"showing_b07a",attached:"attached_ea95"};const u=s},4481:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,'@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.filterWithTagsFocused_ffbf.filterWithTagsFocused_ffbf:hover {\n border-color: var(--ring-main-color);\n}}\n\n.filterWithTags_ff56 {\n overflow: hidden;\n\n margin: 16px 8px 0;\n padding: 3px;\n\n text-align: left;\n\n border: 1px solid var(--ring-borders-color);\n border-radius: var(--ring-border-radius);\n}\n\n.filterWithTags_ff56 .filterWrapper_dd63 {\n padding-right: 0;\n padding-left: 0;\n\n border-bottom: none;\n }\n\n@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.filterWithTags_ff56:hover {\n border-color: var(--ring-border-hover-color);\n }}\n\n.filterWithTagsFocused_ffbf {\n border-color: var(--ring-main-color);\n}\n\n.filterWithTagsInput_ab94 {\n padding: 0;\n\n border: none;\n}\n\n.filter_deda {\n flex-grow: 1;\n\n width: 0;\n}\n\n.popup_f21d {\n overscroll-behavior: contain;\n}\n\n.filterWrapper_dd63 {\n position: relative;\n\n display: flex;\n\n margin: 0;\n padding-right: 8px;\n padding-left: 44px;\n\n border-bottom: 1px solid var(--ring-borders-color);\n}\n\n[dir="rtl"] .filterWrapper_dd63 {\n padding-right: 44px;\n padding-left: 8px;\n }\n\n.filterIcon_b648 {\n position: absolute;\n top: 7px;\n left: 16px;\n\n color: var(--ring-icon-color);\n}\n\n[dir="rtl"] .filterIcon_b648 {\n right: 16px;\n left: auto;\n }\n\n.bottomLine_c880 {\n text-align: center;\n}\n\n.bottomLine_c880.bottomLineOverItem_dfb4 {\n position: relative;\n\n z-index: var(--ring-fixed-z-index);\n\n margin-top: -36px;\n\n background-color: var(--ring-content-background-color);\n }\n\n.message_ccdf {\n display: inline-block;\n\n margin: 8px 0;\n padding: 0 16px;\n}\n\n.selectAll_ff5e {\n display: flex;\n justify-content: space-between;\n\n padding: 8px 16px 0;\n}\n',"",{version:3,sources:["","webpack://./node_modules/@jetbrains/ring-ui/components/select/select-popup.css"],names:[],mappings:"AAAA,wGAAA;EAAA,qCAAA;CAAA,CAAA;;ACIA;EACE,gBAAgB;;EAEhB,kBAA6B;EAC7B,YAAY;;EAEZ,gBAAgB;;EAEhB,2CAA2C;EAC3C,wCAAwC;AAY1C;;AAVE;IACE,gBAAgB;IAChB,eAAe;;IAEf,mBAAmB;EACrB;;ADpBF,wGAAA;IAAA,6CAAA;GAAA,CAAA;;AC2BA;EAEE,oCAAoC;AACtC;;AAEA;EACE,UAAU;;EAEV,YAAY;AACd;;AAEA;EACE,YAAY;;EAEZ,QAAQ;AACV;;AAEA;EACE,4BAA4B;AAC9B;;AAEA;EACE,kBAAkB;;EAElB,aAAa;;EAEb,SAAS;EACT,kBAAmB;EACnB,kBAA8B;;EAE9B,kDAAkD;AAMpD;;AAJE;IACE,mBAA+B;IAC/B,iBAAkB;EACpB;;AAGF;EACE,kBAAkB;EAClB,QAAQ;EACR,UAAoB;;EAEpB,6BAA6B;AAM/B;;AAJE;IACE,WAAqB;IACrB,UAAU;EACZ;;AAGF;EACE,kBAAkB;AAWpB;;AATE;IACE,kBAAkB;;IAElB,kCAAkC;;IAElC,iBAAiB;;IAEjB,sDAAsD;EACxD;;AAGF;EACE,qBAAqB;;EAErB,aAAc;EACd,eAAyB;AAC3B;;AAEA;EACE,aAAa;EACb,8BAA8B;;EAE9B,mBAAmB;AACrB",sourcesContent:[null,'@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n\n.filterWithTags {\n overflow: hidden;\n\n margin: calc(unit * 2) unit 0;\n padding: 3px;\n\n text-align: left;\n\n border: 1px solid var(--ring-borders-color);\n border-radius: var(--ring-border-radius);\n\n & .filterWrapper {\n padding-right: 0;\n padding-left: 0;\n\n border-bottom: none;\n }\n\n &:hover {\n border-color: var(--ring-border-hover-color);\n }\n}\n\n.filterWithTagsFocused,\n.filterWithTagsFocused.filterWithTagsFocused:hover {\n border-color: var(--ring-main-color);\n}\n\n.filterWithTagsInput {\n padding: 0;\n\n border: none;\n}\n\n.filter {\n flex-grow: 1;\n\n width: 0;\n}\n\n.popup {\n overscroll-behavior: contain;\n}\n\n.filterWrapper {\n position: relative;\n\n display: flex;\n\n margin: 0;\n padding-right: unit;\n padding-left: calc(unit * 5.5);\n\n border-bottom: 1px solid var(--ring-borders-color);\n\n [dir="rtl"] & {\n padding-right: calc(unit * 5.5);\n padding-left: unit;\n }\n}\n\n.filterIcon {\n position: absolute;\n top: 7px;\n left: calc(unit * 2);\n\n color: var(--ring-icon-color);\n\n [dir="rtl"] & {\n right: calc(unit * 2);\n left: auto;\n }\n}\n\n.bottomLine {\n text-align: center;\n\n &.bottomLineOverItem {\n position: relative;\n\n z-index: var(--ring-fixed-z-index);\n\n margin-top: -36px;\n\n background-color: var(--ring-content-background-color);\n }\n}\n\n.message {\n display: inline-block;\n\n margin: unit 0;\n padding: 0 calc(2 * unit);\n}\n\n.selectAll {\n display: flex;\n justify-content: space-between;\n\n padding: 8px 16px 0;\n}\n'],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,filterWithTagsFocused:"filterWithTagsFocused_ffbf",filterWithTags:"filterWithTags_ff56",filterWrapper:"filterWrapper_dd63",filterWithTagsInput:"filterWithTagsInput_ab94",filter:"filter_deda",popup:"popup_f21d",filterIcon:"filterIcon_b648",bottomLine:"bottomLine_c880",bottomLineOverItem:"bottomLineOverItem_dfb4",message:"message_ccdf",selectAll:"selectAll_ff5e"};const u=s},2636:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>p});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(9892),s=t(5280),u=a()(o());u.i(s.A),u.i(l.default,"",!0),u.i(c.default,"",!0),u.push([e.id,'@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.select_e2a5:hover .value_b3a3,\n.select_e2a5:hover .icons_c4a9 {\n transition: none;\n\n color: var(--ring-main-color);\n}}\n\n.select_e2a5 {\n position: relative;\n\n display: inline-block;\n\n white-space: nowrap;\n\n color: var(--ring-text-color);\n}\n\n.toolbar_d3be {\n border-top: 1px solid var(--ring-line-color);\n}\n\n.button_ef00 {\n width: 100%;\n padding: 0;\n\n text-align: left;\n}\n\n[dir="rtl"] .button_ef00 {\n text-align: right;\n direction: ltr;\n }\n\n.toolbar_d3be .button_ef00 {\n height: 32px;\n margin: 8px 0;\n }\n\n.button_ef00.buttonSpaced_f316 {\n padding: 0 16px;\n }\n\n.icons_c4a9 {\n position: absolute;\n top: 0;\n right: 5px;\n bottom: 0;\n\n transition: color var(--ring-ease);\n\n color: var(--ring-icon-secondary-color);\n\n line-height: normal;\n}\n\n.inputMode_a6f6 .icons_c4a9 {\n font-size: var(--ring-font-size);\n }\n\n.selectedIcon_a62c {\n\n position: relative;\n top: 3px;\n\n display: inline-block;\n\n width: 16px;\n height: 16px;\n margin: 0 4px;\n\n background-repeat: no-repeat;\n background-position: center;\n\n background-size: contain;\n}\n\n.clearIcon_c750 {\n padding: 0 3px;\n\n vertical-align: -2px;\n}\n\n.sizeS_e8c3 {\n width: 96px;\n}\n\n.sizeM_ed34 {\n width: 240px;\n}\n\n.sizeL_c053 {\n width: 400px;\n}\n\n.sizeFULL_c585 {\n width: 100%;\n}\n\n.sizeAUTO_a07c {\n max-width: 100%;\n}\n\n.buttonMode_dd69 {\n position: relative;\n\n cursor: pointer;\n}\n\n.value_b3a3 {\n\n display: inline-block;\n\n box-sizing: border-box;\n width: 100%;\n height: 33px;\n padding: 0 0 3px;\n\n cursor: pointer;\n transition: color var(--ring-ease), border-color var(--ring-ease);\n text-align: left;\n vertical-align: top;\n\n color: var(--ring-text-color);\n\n border: none;\n border-bottom: 1px solid var(--ring-borders-color);\n outline: none;\n background: transparent;\n}\n\n.value_b3a3:focus {\n border-color: var(--ring-main-color);\n }\n\n.value_b3a3.open_f1b1,\n .value_b3a3:active {\n border-color: transparent;\n }\n\n.value_b3a3::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n outline: 0;\n }\n\n.buttonContainer_b2b9 {\n position: relative;\n\n font-size: var(--ring-font-size);\n}\n\n.buttonValue_b4ad {\n\n display: block;\n\n width: 100%;\n padding-left: 8px;\n\n text-align: left;\n vertical-align: -8px;\n}\n\n.buttonValue_b4ad:focus-visible {\n box-shadow: inset 0 0 0 1px var(--ring-main-color);\n}\n\n.buttonValueOpen_d9d3.buttonValueOpen_d9d3 {\n box-shadow: inset 0 0 0 1px var(--ring-main-color);\n}\n\n.buttonValueEmpty_e6b3.buttonValueEmpty_e6b3 {\n color: var(--ring-disabled-color);\n}\n\n.heightS_b721 .buttonValue_b4ad {\n font-size: var(--ring-font-size);\n}\n\n.label_e56f {\n position: relative;\n\n color: var(--ring-secondary-color);\n}\n\n:focus-visible + .icons_c4a9,\n.value_b3a3:focus,\n.value_b3a3:focus + .icons_c4a9,\n.open_f1b1,\n.open_f1b1 + .icons_c4a9,\n.buttonValueOpen_d9d3 + .icons_c4a9 {\n transition: none;\n\n color: var(--ring-main-color);\n}\n\n.disabled_b89f {\n pointer-events: none;\n\n color: var(--ring-disabled-color);\n}\n\n.disabled_b89f .value_b3a3 {\n color: var(--ring-disabled-color);\n border-bottom-style: dashed;\n }\n\n.avatar_f4dd {\n margin-right: 4px;\n\n vertical-align: -5px;\n}\n\n.popup_acec {\n min-width: 240px;\n max-width: 320px;\n}\n\n.chevron_d51f.chevron_d51f {\n padding: 0 3px;\n\n transition: none;\n vertical-align: -1px;\n\n color: inherit;\n}\n\n.chevronIcon_f6cf.chevronIcon_f6cf {\n transition: none;\n\n color: inherit;\n}\n',"",{version:3,sources:["","webpack://./node_modules/@jetbrains/ring-ui/components/select/select.css"],names:[],mappings:"AAAA,wGAAA;;EAAA,iBAAA;;EAAA,8BAAA;CAAA,CAAA;;ACKA;EACE,kBAAkB;;EAElB,qBAAqB;;EAErB,mBAAmB;;EAEnB,6BAA6B;AAC/B;;AAEA;EACE,4CAA4C;AAC9C;;AAEA;EACE,WAAW;EACX,UAAU;;EAEV,gBAAgB;AAelB;;AAbE;IACE,iBAAiB;IACjB,cAAc;EAChB;;AAEA;IACE,YAAsB;IACtB,aAAc;EAChB;;AAEA;IACE,eAAyB;EAC3B;;AAGF;EACE,kBAAkB;EAClB,MAAM;EACN,UAAU;EACV,SAAS;;EAET,kCAAkC;;EAElC,uCAAuC;;EAEvC,mBAAmB;AAKrB;;AAHE;IACE,gCAAgC;EAClC;;AAGF;;EAGE,kBAAkB;EAClB,QAAQ;;EAER,qBAAqB;;EAErB,WAAqB;EACrB,YAAsB;EACtB,aAAa;;EAEb,4BAA4B;EAC5B,2BAA2B;;EAE3B,wBAAwB;AAC1B;;AAEA;EACE,cAAc;;EAEd,oBAAoB;AACtB;;AAEA;EACE,WAAsB;AACxB;;AAEA;EACE,YAAsB;AACxB;;AAEA;EACE,YAAsB;AACxB;;AAEA;EACE,WAAW;AACb;;AAEA;EACE,eAAe;AACjB;;AAEA;EACE,kBAAkB;;EAElB,eAAe;AACjB;;AAEA;;EAIE,qBAAqB;;EAErB,sBAAsB;EACtB,WAAW;EACX,YAA4B;EAC5B,gBAAgB;;EAEhB,eAAe;EACf,iEAAiE;EACjE,gBAAgB;EAChB,mBAAmB;;EAEnB,6BAA6B;;EAE7B,YAAY;EACZ,kDAAkD;EAClD,aAAa;EACb,uBAAuB;AAiBzB;;AAfE;IACE,oCAAoC;EACtC;;AAEA;;IAEE,yBAAyB;EAC3B;;AAEA;IACE,UAAU;;IAEV,SAAS;IACT,UAAU;EACZ;;AAGF;EACE,kBAAkB;;EAElB,gCAAgC;AAClC;;AAEA;;EAGE,cAAc;;EAEd,WAAW;EACX,iBAAkB;;EAElB,gBAAgB;EAChB,oBAA8B;AAChC;;AAEA;EACE,kDAAgD;AAClD;;AAEA;EACE,kDAAgD;AAClD;;AAEA;EACE,iCAAiC;AACnC;;AAEA;EACE,gCAAgC;AAClC;;AAEA;EACE,kBAAkB;;EAElB,kCAAkC;AACpC;;AAEA;;;;;;EAQE,gBAAgB;;EAEhB,6BAA6B;AAC/B;;AAEA;EACE,oBAAoB;;EAEpB,iCAAiC;AAMnC;;AAJE;IACE,iCAAiC;IACjC,2BAA2B;EAC7B;;AAGF;EACE,iBAAiB;;EAEjB,oBAAoB;AACtB;;AAEA;EACE,gBAA0B;EAC1B,gBAA0B;AAC5B;;AAEA;EACE,cAAc;;EAEd,gBAAgB;EAChB,oBAAoB;;EAEpB,cAAc;AAChB;;AAEA;EACE,gBAAgB;;EAEhB,cAAc;AAChB",sourcesContent:[null,'@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n@value button-shadow from "../button/button.css";\n\n.select {\n position: relative;\n\n display: inline-block;\n\n white-space: nowrap;\n\n color: var(--ring-text-color);\n}\n\n.toolbar {\n border-top: 1px solid var(--ring-line-color);\n}\n\n.button {\n width: 100%;\n padding: 0;\n\n text-align: left;\n\n [dir="rtl"] & {\n text-align: right;\n direction: ltr;\n }\n\n .toolbar & {\n height: calc(4 * unit);\n margin: unit 0;\n }\n\n &.buttonSpaced {\n padding: 0 calc(2 * unit);\n }\n}\n\n.icons {\n position: absolute;\n top: 0;\n right: 5px;\n bottom: 0;\n\n transition: color var(--ring-ease);\n\n color: var(--ring-icon-secondary-color);\n\n line-height: normal;\n\n .inputMode & {\n font-size: var(--ring-font-size);\n }\n}\n\n.selectedIcon {\n composes: resetButton from "../global/global.css";\n\n position: relative;\n top: 3px;\n\n display: inline-block;\n\n width: calc(2 * unit);\n height: calc(2 * unit);\n margin: 0 4px;\n\n background-repeat: no-repeat;\n background-position: center;\n\n background-size: contain;\n}\n\n.clearIcon {\n padding: 0 3px;\n\n vertical-align: -2px;\n}\n\n.sizeS {\n width: calc(unit * 12);\n}\n\n.sizeM {\n width: calc(unit * 30);\n}\n\n.sizeL {\n width: calc(unit * 50);\n}\n\n.sizeFULL {\n width: 100%;\n}\n\n.sizeAUTO {\n max-width: 100%;\n}\n\n.buttonMode {\n position: relative;\n\n cursor: pointer;\n}\n\n.value {\n composes: ellipsis from "../global/global.css";\n composes: font from "../global/global.css";\n\n display: inline-block;\n\n box-sizing: border-box;\n width: 100%;\n height: calc(unit * 4 + 1px);\n padding: 0 0 3px;\n\n cursor: pointer;\n transition: color var(--ring-ease), border-color var(--ring-ease);\n text-align: left;\n vertical-align: top;\n\n color: var(--ring-text-color);\n\n border: none;\n border-bottom: 1px solid var(--ring-borders-color);\n outline: none;\n background: transparent;\n\n &:focus {\n border-color: var(--ring-main-color);\n }\n\n &.open,\n &:active {\n border-color: transparent;\n }\n\n &::-moz-focus-inner {\n padding: 0;\n\n border: 0;\n outline: 0;\n }\n}\n\n.buttonContainer {\n position: relative;\n\n font-size: var(--ring-font-size);\n}\n\n.buttonValue {\n composes: ellipsis from "../global/global.css";\n\n display: block;\n\n width: 100%;\n padding-left: unit;\n\n text-align: left;\n vertical-align: calc(0 - unit);\n}\n\n.buttonValue:focus-visible {\n box-shadow: button-shadow var(--ring-main-color);\n}\n\n.buttonValueOpen.buttonValueOpen {\n box-shadow: button-shadow var(--ring-main-color);\n}\n\n.buttonValueEmpty.buttonValueEmpty {\n color: var(--ring-disabled-color);\n}\n\n.heightS .buttonValue {\n font-size: var(--ring-font-size);\n}\n\n.label {\n position: relative;\n\n color: var(--ring-secondary-color);\n}\n\n.select:hover .value,\n.select:hover .icons,\n:focus-visible + .icons,\n.value:focus,\n.value:focus + .icons,\n.open,\n.open + .icons,\n.buttonValueOpen + .icons {\n transition: none;\n\n color: var(--ring-main-color);\n}\n\n.disabled {\n pointer-events: none;\n\n color: var(--ring-disabled-color);\n\n & .value {\n color: var(--ring-disabled-color);\n border-bottom-style: dashed;\n }\n}\n\n.avatar {\n margin-right: 4px;\n\n vertical-align: -5px;\n}\n\n.popup {\n min-width: calc(unit * 30);\n max-width: calc(unit * 40);\n}\n\n.chevron.chevron {\n padding: 0 3px;\n\n transition: none;\n vertical-align: -1px;\n\n color: inherit;\n}\n\n.chevronIcon.chevronIcon {\n transition: none;\n\n color: inherit;\n}\n'],sourceRoot:""}]),u.locals={unit:`${l.default.locals.unit}`,"button-shadow":`${c.default.locals["button-shadow"]}`,select:"select_e2a5",value:`value_b3a3 ${l.default.locals.ellipsis} ${l.default.locals.font}`,icons:"icons_c4a9",toolbar:"toolbar_d3be",button:"button_ef00",buttonSpaced:"buttonSpaced_f316",inputMode:"inputMode_a6f6",selectedIcon:`selectedIcon_a62c ${l.default.locals.resetButton}`,clearIcon:"clearIcon_c750",sizeS:"sizeS_e8c3",sizeM:"sizeM_ed34",sizeL:"sizeL_c053",sizeFULL:"sizeFULL_c585",sizeAUTO:"sizeAUTO_a07c",buttonMode:"buttonMode_dd69",open:"open_f1b1",buttonContainer:"buttonContainer_b2b9",buttonValue:`buttonValue_b4ad ${l.default.locals.ellipsis}`,buttonValueOpen:"buttonValueOpen_d9d3",buttonValueEmpty:"buttonValueEmpty_e6b3",heightS:"heightS_b721",label:"label_e56f",disabled:"disabled_b89f",avatar:"avatar_f4dd",popup:"popup_acec",chevron:"chevron_d51f",chevronIcon:"chevronIcon_f6cf"};const p=u},8102:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>s});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(5280),c=a()(o());c.i(l.A),c.push([e.id,".trapButton_c32e {\n position: absolute;\n left: -9999px;\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/tab-trap/tab-trap.css"],names:[],mappings:"AAEA;EACE,kBAAkB;EAClB,aAAa;AACf",sourcesContent:['@import "../global/variables.css";\n\n.trapButton {\n position: absolute;\n left: -9999px;\n}\n'],sourceRoot:""}]),c.locals={trapButton:"trapButton_c32e"};const s=c},4561:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,'@media (hover: hover), (-moz-touch-enabled: 0), (-ms-high-contrast: none), (-ms-high-contrast: active) {.tag_b7aa:hover,\n.tagAngled_c869:hover::before {\n transition: none;\n\n background-color: var(--ring-tag-hover-background-color);\n}}\n\n.tag_b7aa {\n\n position: relative;\n z-index: 1;\n\n display: inline-flex;\n\n box-sizing: border-box;\n max-width: 100%;\n height: 20px;\n\n padding: 0 8px;\n\n cursor: pointer;\n\n vertical-align: top;\n\n color: var(--ring-text-color);\n\n border: none;\n border-radius: var(--ring-border-radius);\n\n font-size: 12px;\n line-height: var(--ring-line-height);\n}\n\n.tag_b7aa,\n.tagAngled_c869::before {\n transition: background-color var(--ring-ease);\n\n background-color: var(--ring-tag-background-color);\n}\n\n.withRemove_c0a5 {\n padding-right: 22px;\n}\n\n.container_cb34 {\n position: relative;\n\n display: inline-block;\n\n max-width: calc(100% - 4px);\n\n margin-right: 4px;\n\n white-space: nowrap;\n}\n\n.focused_fd92,\n.tag_b7aa:focus-visible {\n position: relative;\n\n outline: none;\n box-shadow: 0 0 0 2px var(--ring-border-hover-color);\n}\n\n.focused_fd92,\n.focused_fd92.tagAngled_c869::before,\n.tag_b7aa:focus-visible,\n.tagAngled_c869:focus-visible::before {\n transition: none;\n\n background-color: var(--ring-tag-hover-background-color);\n}\n\n.tagAngled_c869 {\n /* it needs to fix vertical alignment broken by "overflow: hidden". Remove this class, when IE11 will be deprecated */\n\n margin-bottom: -5px !important;\n\n margin-left: 8px;\n padding-left: 4px;\n\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n}\n\n.tagAngled_c869::before {\n position: absolute;\n z-index: -1;\n top: 0;\n left: 0;\n\n box-sizing: border-box;\n width: 12px;\n height: 12px;\n\n content: "";\n transform: scaleY(1.177) rotate(45deg);\n transform-origin: 0 0;\n\n border: none;\n }\n\n.tagAngled_c869.focused_fd92,\n .tagAngled_c869:focus {\n box-shadow: 0 0 0 1px var(--ring-border-hover-color) inset, 0 0 0 1px var(--ring-border-hover-color);\n }\n\n.tagAngled_c869:focus::before {\n box-shadow:\n 1px -1px var(--ring-border-hover-color) inset,\n -0.8px 0.8px 0 0.5px var(--ring-border-hover-color);\n }\n\n.content_a838 {\n}\n\n.disabled_b740.tag_b7aa,\n.disabled_b740.tagAngled_c869::before {\n pointer-events: none;\n\n color: var(--ring-disabled-color);\n background-color: var(--ring-disabled-background-color);\n}\n\n.remove_eff8 {\n position: absolute;\n z-index: 1;\n top: 2px;\n right: 0;\n\n height: auto;\n padding: 0 4px;\n\n line-height: 16px;\n}\n\n.removeIcon_accf.removeIcon_accf {\n color: var(--ring-icon-secondary-color);\n}\n\n.icon_e877 {\n margin-right: 6px;\n\n color: var(--ring-icon-secondary-color);\n}\n\n.icon_e877 svg {\n vertical-align: -3px;\n }\n\n.avatarContainer_ee1b {\n display: inline-block;\n overflow: hidden;\n\n box-sizing: border-box;\n width: 20px;\n height: 20px;\n margin-right: 4px;\n margin-left: -8px;\n\n vertical-align: top;\n\n border-top-left-radius: var(--ring-border-radius);\n border-bottom-left-radius: var(--ring-border-radius);\n}\n\n.customIcon_ac93 {\n max-width: 16px;\n max-height: 16px;\n\n margin-right: 4px;\n\n vertical-align: bottom;\n}\n\n.avatarIcon_a8ff {\n width: 20px;\n\n margin-right: -4px;\n\n -o-object-fit: contain;\n\n object-fit: contain;\n -o-object-position: center;\n object-position: center;\n}\n',"",{version:3,sources:["","webpack://./node_modules/@jetbrains/ring-ui/components/tag/tag.css"],names:[],mappings:"AAAA,wGAAA;;EAAA,iBAAA;;EAAA,yDAAA;CAAA,CAAA;;ACKA;;EAGE,kBAAkB;EAClB,UAAU;;EAEV,oBAAoB;;EAEpB,sBAAsB;EACtB,eAAe;EACf,YAAkB;;EAElB,cAAe;;EAEf,eAAe;;EAEf,mBAAmB;;EAEnB,6BAA6B;;EAE7B,YAAY;EACZ,wCAAwC;;EAExC,eAAe;EACf,oCAAoC;AACtC;;AAEA;;EAEE,6CAA6C;;EAE7C,kDAAkD;AACpD;;AAEA;EACE,mBAAmB;AACrB;;AAEA;EACE,kBAAkB;;EAElB,qBAAqB;;EAErB,2BAAgC;;EAEhC,iBAA4B;;EAE5B,mBAAmB;AACrB;;AAEA;;EAEE,kBAAkB;;EAElB,aAAa;EACb,oDAAoD;AACtD;;AAEA;;;;EAME,gBAAgB;;EAEhB,wDAAwD;AAC1D;;AAEA;EACE,qHAAqH;;EAErH,8BAA8B;;EAE9B,gBAAiB;EACjB,iBAA4B;;EAE5B,yBAAyB;EACzB,4BAA4B;AA6B9B;;AA3BE;IACE,kBAAkB;IAClB,WAAW;IACX,MAAM;IACN,OAAO;;IAEP,sBAAsB;IACtB,WAAW;IACX,YAAY;;IAEZ,WAAW;IACX,sCAAsC;IACtC,qBAAqB;;IAErB,YAAY;EACd;;AAEA;;IAEE,oGAAoG;EACtG;;AAEA;IACE;;yDAEqD;EACvD;;AAGF;AAEA;;AAEA;;EAEE,oBAAoB;;EAEpB,iCAAiC;EACjC,uDAAuD;AACzD;;AAEA;EACE,kBAAkB;EAClB,UAAU;EACV,QAAQ;EACR,QAAQ;;EAER,YAAY;EACZ,cAAyB;;EAEzB,iBAA2B;AAC7B;;AAEA;EACE,uCAAuC;AACzC;;AAEA;EACE,iBAAiB;;EAEjB,uCAAuC;AAKzC;;AAHE;IACE,oBAAoB;EACtB;;AAGF;EACE,qBAAqB;EACrB,gBAAgB;;EAEhB,sBAAsB;EACtB,WAAiB;EACjB,YAAkB;EAClB,iBAA4B;EAC5B,iBAA2B;;EAE3B,mBAAmB;;EAEnB,iDAAiD;EACjD,oDAAoD;AACtD;;AAEA;EACE,eAAyB;EACzB,gBAA0B;;EAE1B,iBAA4B;;EAE5B,sBAAsB;AACxB;;AAEA;EACE,WAAiB;;EAEjB,kBAAkB;;EAElB,sBAAmB;;KAAnB,mBAAmB;EACnB,0BAAuB;KAAvB,uBAAuB;AACzB",sourcesContent:[null,'@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n@value max-height: 20px;\n\n.tag {\n composes: resetButton from "../global/global.css";\n\n position: relative;\n z-index: 1;\n\n display: inline-flex;\n\n box-sizing: border-box;\n max-width: 100%;\n height: max-height;\n\n padding: 0 unit;\n\n cursor: pointer;\n\n vertical-align: top;\n\n color: var(--ring-text-color);\n\n border: none;\n border-radius: var(--ring-border-radius);\n\n font-size: 12px;\n line-height: var(--ring-line-height);\n}\n\n.tag,\n.tagAngled::before {\n transition: background-color var(--ring-ease);\n\n background-color: var(--ring-tag-background-color);\n}\n\n.withRemove {\n padding-right: 22px;\n}\n\n.container {\n position: relative;\n\n display: inline-block;\n\n max-width: calc(100% - unit / 2);\n\n margin-right: calc(unit / 2);\n\n white-space: nowrap;\n}\n\n.focused,\n.tag:focus-visible {\n position: relative;\n\n outline: none;\n box-shadow: 0 0 0 2px var(--ring-border-hover-color);\n}\n\n.focused,\n.focused.tagAngled::before,\n.tag:focus-visible,\n.tagAngled:focus-visible::before,\n.tag:hover,\n.tagAngled:hover::before {\n transition: none;\n\n background-color: var(--ring-tag-hover-background-color);\n}\n\n.tagAngled {\n /* it needs to fix vertical alignment broken by "overflow: hidden". Remove this class, when IE11 will be deprecated */\n\n margin-bottom: -5px !important;\n\n margin-left: unit;\n padding-left: calc(unit / 2);\n\n border-top-left-radius: 0;\n border-bottom-left-radius: 0;\n\n &::before {\n position: absolute;\n z-index: -1;\n top: 0;\n left: 0;\n\n box-sizing: border-box;\n width: 12px;\n height: 12px;\n\n content: "";\n transform: scaleY(1.177) rotate(45deg);\n transform-origin: 0 0;\n\n border: none;\n }\n\n &.focused,\n &:focus {\n box-shadow: 0 0 0 1px var(--ring-border-hover-color) inset, 0 0 0 1px var(--ring-border-hover-color);\n }\n\n &:focus::before {\n box-shadow:\n 1px -1px var(--ring-border-hover-color) inset,\n -0.8px 0.8px 0 0.5px var(--ring-border-hover-color);\n }\n}\n\n.content {\n composes: ellipsis from "../global/global.css";\n}\n\n.disabled.tag,\n.disabled.tagAngled::before {\n pointer-events: none;\n\n color: var(--ring-disabled-color);\n background-color: var(--ring-disabled-background-color);\n}\n\n.remove {\n position: absolute;\n z-index: 1;\n top: 2px;\n right: 0;\n\n height: auto;\n padding: 0 calc(unit / 2);\n\n line-height: calc(unit * 2);\n}\n\n.removeIcon.removeIcon {\n color: var(--ring-icon-secondary-color);\n}\n\n.icon {\n margin-right: 6px;\n\n color: var(--ring-icon-secondary-color);\n\n & svg {\n vertical-align: -3px;\n }\n}\n\n.avatarContainer {\n display: inline-block;\n overflow: hidden;\n\n box-sizing: border-box;\n width: max-height;\n height: max-height;\n margin-right: calc(unit / 2);\n margin-left: calc(0 - unit);\n\n vertical-align: top;\n\n border-top-left-radius: var(--ring-border-radius);\n border-bottom-left-radius: var(--ring-border-radius);\n}\n\n.customIcon {\n max-width: calc(unit * 2);\n max-height: calc(unit * 2);\n\n margin-right: calc(unit / 2);\n\n vertical-align: bottom;\n}\n\n.avatarIcon {\n width: max-height;\n\n margin-right: -4px;\n\n object-fit: contain;\n object-position: center;\n}\n'],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,"max-height":"20px",tag:`tag_b7aa ${l.default.locals.resetButton}`,tagAngled:"tagAngled_c869",withRemove:"withRemove_c0a5",container:"container_cb34",focused:"focused_fd92",content:`content_a838 ${l.default.locals.ellipsis}`,disabled:"disabled_b740",remove:"remove_eff8",removeIcon:"removeIcon_accf",icon:"icon_e877",avatarContainer:"avatarContainer_ee1b",customIcon:"customIcon_ac93",avatarIcon:"avatarIcon_a8ff"};const u=s},6162:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>s});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(5280),c=a()(o());c.i(l.A),c.push([e.id,".text_f1dc {\n color: var(--ring-text-color);\n}\n\n.sizeS_b3aa {\n font-size: var(--ring-font-size-smaller);\n}\n\n.sizeM_ae72 {\n font-size: var(--ring-font-size);\n}\n\n.sizeL_f259 {\n font-size: var(--ring-font-size-larger);\n}\n\n.info_c0a4 {\n color: var(--ring-secondary-color);\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/text/text.css"],names:[],mappings:"AAEA;EACE,6BAA6B;AAC/B;;AAEA;EACE,wCAAwC;AAC1C;;AAEA;EACE,gCAAgC;AAClC;;AAEA;EACE,uCAAuC;AACzC;;AAEA;EACE,kCAAkC;AACpC",sourcesContent:['@import "../global/variables.css";\n\n.text {\n color: var(--ring-text-color);\n}\n\n.sizeS {\n font-size: var(--ring-font-size-smaller);\n}\n\n.sizeM {\n font-size: var(--ring-font-size);\n}\n\n.sizeL {\n font-size: var(--ring-font-size-larger);\n}\n\n.info {\n color: var(--ring-secondary-color);\n}\n'],sourceRoot:""}]),c.locals={text:"text_f1dc",sizeS:"sizeS_b3aa",sizeM:"sizeM_ae72",sizeL:"sizeL_f259",info:"info_c0a4"};const s=c},938:(e,n,t)=>{"use strict";t.r(n),t.d(n,{default:()=>u});var r=t(1404),o=t.n(r),i=t(7156),a=t.n(i),l=t(9106),c=t(5280),s=a()(o());s.i(c.A),s.i(l.default,"",!0),s.push([e.id,".tooltip_fbfb {\n max-width: 400px;\n padding: 8px;\n\n text-align: left;\n\n color: var(--ring-text-color);\n}\n\n.long_b7a5 {\n padding: 8px 12px;\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n","",{version:3,sources:["webpack://./node_modules/@jetbrains/ring-ui/components/tooltip/tooltip.css"],names:[],mappings:"AAIA;EACE,gBAA0B;EAC1B,YAAa;;EAEb,gBAAgB;;EAEhB,6BAA6B;AAC/B;;AAEA;EACE,iBAA8B;;EAE9B,wCAAwC;EACxC,2CAA2C;AAC7C",sourcesContent:['@import "../global/variables.css";\n\n@value unit from "../global/global.css";\n\n.tooltip {\n max-width: calc(unit * 50);\n padding: unit;\n\n text-align: left;\n\n color: var(--ring-text-color);\n}\n\n.long {\n padding: unit calc(unit * 1.5);\n\n font-size: var(--ring-font-size-smaller);\n line-height: var(--ring-line-height-lowest);\n}\n'],sourceRoot:""}]),s.locals={unit:`${l.default.locals.unit}`,tooltip:"tooltip_fbfb",long:"long_b7a5"};const u=s},7156:e=>{"use strict";e.exports=function(e){var n=[];return n.toString=function(){return this.map((function(n){var t="",r=void 0!==n[5];return n[4]&&(t+="@supports (".concat(n[4],") {")),n[2]&&(t+="@media ".concat(n[2]," {")),r&&(t+="@layer".concat(n[5].length>0?" ".concat(n[5]):""," {")),t+=e(n),r&&(t+="}"),n[2]&&(t+="}"),n[4]&&(t+="}"),t})).join("")},n.i=function(e,t,r,o,i){"string"==typeof e&&(e=[[null,e,void 0]]);var a={};if(r)for(var l=0;l0?" ".concat(u[5]):""," {").concat(u[1],"}")),u[5]=i),t&&(u[2]?(u[1]="@media ".concat(u[2]," {").concat(u[1],"}"),u[2]=t):u[2]=t),o&&(u[4]?(u[1]="@supports (".concat(u[4],") {").concat(u[1],"}"),u[4]=o):u[4]="".concat(o)),n.push(u))}},n}},1404:e=>{"use strict";e.exports=function(e){var n=e[1],t=e[3];if(!t)return n;if("function"==typeof btoa){var r=btoa(unescape(encodeURIComponent(JSON.stringify(t)))),o="sourceMappingURL=data:application/json;charset=utf-8;base64,".concat(r),i="/*# ".concat(o," */");return[n].concat([i]).join("\n")}return[n].join("\n")}},4504:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(7222);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},9102:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(9892);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},6860:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(1866);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},3912:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(5486);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},8764:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(6506);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},6620:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(9106);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},9468:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(5066);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},274:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(8976);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},5924:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(8266);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},7826:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(6960);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},1914:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(480);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},8130:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(1586);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},1564:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(8890);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},5103:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(4481);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},3006:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(2636);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},9344:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(8102);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},4512:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(4561);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},6932:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(6162);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},8132:(e,n,t)=>{var r=t(8298),o=t(5163),i=t(2729),a=t(9986),l=t(9742),c=t(6291),s=t(938);s=s.__esModule?s.default:s;var u={};u.styleTagTransform=c,u.setAttributes=a,u.insert=i.bind(null,"head"),u.domAPI=o,u.insertStyleElement=l;r(s,u);e.exports=s&&s.locals||{}},8298:e=>{"use strict";var n=[];function t(e){for(var t=-1,r=0;r{"use strict";var n={};e.exports=function(e,t){var r=function(e){if(void 0===n[e]){var t=document.querySelector(e);if(window.HTMLIFrameElement&&t instanceof window.HTMLIFrameElement)try{t=t.contentDocument.head}catch(e){t=null}n[e]=t}return n[e]}(e);if(!r)throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");r.appendChild(t)}},9742:e=>{"use strict";e.exports=function(e){var n=document.createElement("style");return e.setAttributes(n,e.attributes),e.insert(n,e.options),n}},9986:(e,n,t)=>{"use strict";e.exports=function(e){var n=t.nc;n&&e.setAttribute("nonce",n)}},5163:e=>{"use strict";e.exports=function(e){if("undefined"==typeof document)return{update:function(){},remove:function(){}};var n=e.insertStyleElement(e);return{update:function(t){!function(e,n,t){var r="";t.supports&&(r+="@supports (".concat(t.supports,") {")),t.media&&(r+="@media ".concat(t.media," {"));var o=void 0!==t.layer;o&&(r+="@layer".concat(t.layer.length>0?" ".concat(t.layer):""," {")),r+=t.css,o&&(r+="}"),t.media&&(r+="}"),t.supports&&(r+="}");var i=t.sourceMap;i&&"undefined"!=typeof btoa&&(r+="\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(i))))," */")),n.styleTagTransform(r,e,n.options)}(n,e,t)},remove:function(){!function(e){if(null===e.parentNode)return!1;e.parentNode.removeChild(e)}(n)}}}},6291:e=>{"use strict";e.exports=function(e,n){if(n.styleSheet)n.styleSheet.cssText=e;else{for(;n.firstChild;)n.removeChild(n.firstChild);n.appendChild(document.createTextNode(e))}}},9511:(e,n,t)=>{"use strict";var r=t(6556)("ArrayBuffer.prototype.byteLength",!0),o=t(4670);e.exports=function(e){return o(e)?r?r(e):e.byteLength:NaN}},3144:(e,n,t)=>{"use strict";var r=t(6743),o=t(1002),i=t(76),a=t(7119);e.exports=a||r.call(i,o)},2205:(e,n,t)=>{"use strict";var r=t(6743),o=t(1002),i=t(3144);e.exports=function(){return i(r,o,arguments)}},1002:e=>{"use strict";e.exports=Function.prototype.apply},76:e=>{"use strict";e.exports=Function.prototype.call},3126:(e,n,t)=>{"use strict";var r=t(6743),o=t(9675),i=t(76),a=t(3144);e.exports=function(e){if(e.length<1||"function"!=typeof e[0])throw new o("a function is required");return a(r,i,e)}},7119:e=>{"use strict";e.exports="undefined"!=typeof Reflect&&Reflect&&Reflect.apply},8075:(e,n,t)=>{"use strict";var r=t(453),o=t(487),i=o(r("String.prototype.indexOf"));e.exports=function(e,n){var t=r(e,!!n);return"function"==typeof t&&i(e,".prototype.")>-1?o(t):t}},487:(e,n,t)=>{"use strict";var r=t(6897),o=t(3036),i=t(3126),a=t(2205);e.exports=function(e){var n=i(arguments),t=e.length-(arguments.length-1);return r(n,1+(t>0?t:0),!0)},o?o(e.exports,"apply",{value:a}):e.exports.apply=a},6556:(e,n,t)=>{"use strict";var r=t(453),o=t(3126),i=o([r("%String.prototype.indexOf%")]);e.exports=function(e,n){var t=r(e,!!n);return"function"==typeof t&&i(e,".prototype.")>-1?o([t]):t}},5888:(e,n,t)=>{"use strict";e.exports=function(e,n){var t=this,r=t.constructor;return t.options=Object.assign({storeInstancesGlobally:!0},n||{}),t.callbacks={},t.directMap={},t.sequenceLevels={},t.resetTimer=null,t.ignoreNextKeyup=!1,t.ignoreNextKeypress=!1,t.nextExpectedAction=!1,t.element=e,t.addEvents(),t.options.storeInstancesGlobally&&r.instances.push(t),t},e.exports.prototype.bind=t(1210),e.exports.prototype.bindMultiple=t(4382),e.exports.prototype.unbind=t(3709),e.exports.prototype.trigger=t(3149),e.exports.prototype.reset=t(6726),e.exports.prototype.stopCallback=t(4446),e.exports.prototype.handleKey=t(4320),e.exports.prototype.addEvents=t(6687),e.exports.prototype.bindSingle=t(2214),e.exports.prototype.getKeyInfo=t(4174),e.exports.prototype.pickBestAction=t(6004),e.exports.prototype.getReverseMap=t(5193),e.exports.prototype.getMatches=t(9132),e.exports.prototype.resetSequences=t(3229),e.exports.prototype.fireCallback=t(7922),e.exports.prototype.bindSequence=t(3256),e.exports.prototype.resetSequenceTimer=t(602),e.exports.prototype.detach=t(3502),e.exports.instances=[],e.exports.reset=t(6255),e.exports.REVERSE_MAP=null},6687:(e,n,t)=>{"use strict";e.exports=function(){var e=this,n=t(2904),r=e.element;e.eventHandler=t(8178).bind(e),n(r,"keypress",e.eventHandler),n(r,"keydown",e.eventHandler),n(r,"keyup",e.eventHandler)}},1210:e=>{"use strict";e.exports=function(e,n,t){return e=e instanceof Array?e:[e],this.bindMultiple(e,n,t),this}},4382:e=>{"use strict";e.exports=function(e,n,t){for(var r=0;r{"use strict";e.exports=function(e,n,r,o){var i=this;function a(n){return function(){i.nextExpectedAction=n,++i.sequenceLevels[e],i.resetSequenceTimer()}}function l(n){var a;i.fireCallback(r,n,e),"keyup"!==o&&(a=t(3970),i.ignoreNextKeyup=a(n)),setTimeout((function(){i.resetSequences()}),10)}i.sequenceLevels[e]=0;for(var c=0;c{"use strict";e.exports=function(e,n,t,r,o){var i=this;i.directMap[e+":"+t]=n;var a,l=(e=e.replace(/\s+/g," ")).split(" ");l.length>1?i.bindSequence(e,l,n,t):(a=i.getKeyInfo(e,t),i.callbacks[a.key]=i.callbacks[a.key]||[],i.getMatches(a.key,a.modifiers,{type:a.action},r,e,o),i.callbacks[a.key][r?"unshift":"push"]({callback:n,modifiers:a.modifiers,action:a.action,seq:r,level:o,combo:e}))}},3502:(e,n,t)=>{var r=t(2904).off;e.exports=function(){var e=this,n=e.element;r(n,"keypress",e.eventHandler),r(n,"keydown",e.eventHandler),r(n,"keyup",e.eventHandler)}},2904:e=>{function n(e,n,t,r){return!e.addEventListener&&(n="on"+n),(e.addEventListener||e.attachEvent).call(e,n,t,r),t}e.exports=n,e.exports.on=n,e.exports.off=function(e,n,t,r){return!e.removeEventListener&&(n="on"+n),(e.removeEventListener||e.detachEvent).call(e,n,t,r),t}},7922:(e,n,t)=>{"use strict";e.exports=function(e,n,r,o){this.stopCallback(n,n.target||n.srcElement,r,o)||!1===e(n,r)&&(t(2156)(n),t(1849)(n))}},4174:(e,n,t)=>{"use strict";e.exports=function(e,n){var r,o,i,a,l,c,s=[];for(r=t(7486)(e),a=t(7641),l=t(7984),c=t(5962),i=0;i{"use strict";e.exports=function(e,n,r,o,i,a){var l,c,s,u,p=this,f=[],d=r.type;"keypress"!==d||r.code&&"Arrow"===r.code.slice(0,5)||(p.callbacks["any-character"]||[]).forEach((function(e){f.push(e)}));if(!p.callbacks[e])return f;for(s=t(5962),"keyup"===d&&s(e)&&(n=[e]),l=0;l{"use strict";e.exports=function(){var e,n=this.constructor;if(!n.REVERSE_MAP)for(var r in n.REVERSE_MAP={},e=t(6814))r>95&&r<112||e.hasOwnProperty(r)&&(n.REVERSE_MAP[e[r]]=r);return n.REVERSE_MAP}},4320:(e,n,t)=>{"use strict";e.exports=function(e,n,r){var o,i,a,l,c=this,s={},u=0,p=!1;for(o=c.getMatches(e,n,r),i=0;i{"use strict";e.exports=function(e){var n,r=this;"number"!=typeof e.which&&(e.which=e.keyCode);var o=t(3970)(e);void 0!==o&&("keyup"!==e.type||r.ignoreNextKeyup!==o?(n=t(5273),r.handleKey(o,n(e),e)):r.ignoreNextKeyup=!1)}},7238:e=>{"use strict";e.exports=function(e,n){return e.sort().join(",")===n.sort().join(",")}},6004:e=>{"use strict";e.exports=function(e,n,t){return t||(t=this.getReverseMap()[e]?"keydown":"keypress"),"keypress"===t&&n.length&&(t="keydown"),t}},6726:e=>{"use strict";e.exports=function(){return this.callbacks={},this.directMap={},this}},602:e=>{"use strict";e.exports=function(){var e=this;clearTimeout(e.resetTimer),e.resetTimer=setTimeout((function(){e.resetSequences()}),1e3)}},3229:e=>{"use strict";e.exports=function(e){var n=this;e=e||{};var t,r=!1;for(t in n.sequenceLevels)e[t]?r=!0:n.sequenceLevels[t]=0;r||(n.nextExpectedAction=!1)}},4446:e=>{"use strict";e.exports=function(e,n){if((" "+n.className+" ").indexOf(" combokeys ")>-1)return!1;var t=n.tagName.toLowerCase();return"input"===t||"select"===t||"textarea"===t||n.isContentEditable}},3149:e=>{"use strict";e.exports=function(e,n){return this.directMap[e+":"+n]&&this.directMap[e+":"+n]({},e),this}},3709:e=>{"use strict";e.exports=function(e,n){return this.bind(e,(function(){}),n)}},6255:e=>{"use strict";e.exports=function(){this.instances.forEach((function(e){e.reset()}))}},3970:(e,n,t)=>{"use strict";e.exports=function(e){var n,r;if(n=t(6814),r=t(4082),"keypress"===e.type){var o=String.fromCharCode(e.which);return e.shiftKey||(o=o.toLowerCase()),o}return void 0!==n[e.which]?n[e.which]:void 0!==r[e.which]?r[e.which]:String.fromCharCode(e.which).toLowerCase()}},5273:e=>{"use strict";e.exports=function(e){var n=[];return e.shiftKey&&n.push("shift"),e.altKey&&n.push("alt"),e.ctrlKey&&n.push("ctrl"),e.metaKey&&n.push("meta"),n}},5962:e=>{"use strict";e.exports=function(e){return"shift"===e||"ctrl"===e||"alt"===e||"meta"===e}},7486:e=>{"use strict";e.exports=function(e){return"+"===e?["+"]:e.split("+")}},2156:e=>{"use strict";e.exports=function(e){e.preventDefault?e.preventDefault():e.returnValue=!1}},7984:e=>{"use strict";e.exports={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"}},7641:e=>{"use strict";e.exports={option:"alt",command:"meta",return:"enter",escape:"esc",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"}},4082:e=>{"use strict";e.exports={106:"*",107:"plus",109:"minus",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"}},6814:e=>{"use strict";e.exports={8:"backspace",9:"tab",13:"enter",16:"shift",17:"ctrl",18:"alt",20:"capslock",27:"esc",32:"space",33:"pageup",34:"pagedown",35:"end",36:"home",37:"left",38:"up",39:"right",40:"down",45:"ins",46:"del",91:"meta",93:"meta",173:"minus",187:"plus",189:"minus",224:"meta"};for(var n=1;n<20;++n)e.exports[111+n]="f"+n;for(n=0;n<=9;++n)e.exports[n+96]=n},1849:e=>{"use strict";e.exports=function(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0}},4982:(e,n,t)=>{"use strict";var r=t(6525),o=t(8075),i=t(1589),a=t(453),l=t(4552),c=t(920),s=t(7653),u=t(7244),p=t(4634),f=t(4670),d=t(2120),g=t(4035),h=t(7070),A=t(1189),b=t(1539),v=t(593),m=t(5767),y=t(9511),E=o("SharedArrayBuffer.prototype.byteLength",!0),C=o("Date.prototype.getTime"),w=Object.getPrototypeOf,x=o("Object.prototype.toString"),S=a("%Set%",!0),_=o("Map.prototype.has",!0),k=o("Map.prototype.get",!0),O=o("Map.prototype.size",!0),B=o("Set.prototype.add",!0),P=o("Set.prototype.delete",!0),T=o("Set.prototype.has",!0),I=o("Set.prototype.size",!0);function j(e,n,t,r){for(var o,i=l(e);(o=i.next())&&!o.done;)if(N(n,o.value,t,r))return P(e,o.value),!0;return!1}function z(e){return void 0===e?null:"object"!=typeof e?"symbol"!=typeof e&&("string"!=typeof e&&"number"!=typeof e||+e==+e):void 0}function D(e,n,t,o,i,a){var l=z(t);if(null!=l)return l;var c=k(n,l),s=r({},i,{strict:!1});return!(void 0===c&&!_(n,l)||!N(o,c,s,a))&&(!_(e,l)&&N(o,c,s,a))}function R(e,n,t){var r=z(t);return null!=r?r:T(n,r)&&!T(e,r)}function M(e,n,t,r,o,i){for(var a,c,s=l(e);(a=s.next())&&!a.done;)if(N(t,c=a.value,o,i)&&N(r,k(n,c),o,i))return P(e,c),!0;return!1}function N(e,n,t,o){var a=t||{};if(a.strict?s(e,n):e===n)return!0;if(b(e)!==b(n))return!1;if(!e||!n||"object"!=typeof e&&"object"!=typeof n)return a.strict?s(e,n):e==n;var c,P=o.has(e),z=o.has(n);if(P&&z){if(o.get(e)===o.get(n))return!0}else c={};return P||o.set(e,c),z||o.set(n,c),function(e,n,t,o){var a,c;if(typeof e!=typeof n)return!1;if(null==e||null==n)return!1;if(x(e)!==x(n))return!1;if(u(e)!==u(n))return!1;var s=p(e),b=p(n);if(s!==b)return!1;var P=e instanceof Error,z=n instanceof Error;if(P!==z)return!1;if((P||z)&&(e.name!==n.name||e.message!==n.message))return!1;var L=g(e),H=g(n);if(L!==H)return!1;if((L||H)&&(e.source!==n.source||i(e)!==i(n)))return!1;var U=d(e),W=d(n);if(U!==W)return!1;if((U||W)&&C(e)!==C(n))return!1;if(t.strict&&w&&w(e)!==w(n))return!1;var G=m(e),Y=m(n);if(G!==Y)return!1;if(G||Y){if(e.length!==n.length)return!1;for(a=0;a=0;a--)if(Z[a]!=J[a])return!1;for(a=Z.length-1;a>=0;a--)if(!N(e[c=Z[a]],n[c],t,o))return!1;var ee=v(e),ne=v(n);if(ee!==ne)return!1;if("Set"===ee||"Set"===ne)return function(e,n,t,r){if(I(e)!==I(n))return!1;var o,i,a,c=l(e),s=l(n);for(;(o=c.next())&&!o.done;)if(o.value&&"object"==typeof o.value)a||(a=new S),B(a,o.value);else if(!T(n,o.value)){if(t.strict)return!1;if(!R(e,n,o.value))return!1;a||(a=new S),B(a,o.value)}if(a){for(;(i=s.next())&&!i.done;)if(i.value&&"object"==typeof i.value){if(!j(a,i.value,t.strict,r))return!1}else if(!t.strict&&!T(e,i.value)&&!j(a,i.value,t.strict,r))return!1;return 0===I(a)}return!0}(e,n,t,o);if("Map"===ee)return function(e,n,t,o){if(O(e)!==O(n))return!1;var i,a,c,s,u,p,f=l(e),d=l(n);for(;(i=f.next())&&!i.done;)if(s=i.value[0],u=i.value[1],s&&"object"==typeof s)c||(c=new S),B(c,s);else if(void 0===(p=k(n,s))&&!_(n,s)||!N(u,p,t,o)){if(t.strict)return!1;if(!D(e,n,s,u,t,o))return!1;c||(c=new S),B(c,s)}if(c){for(;(a=d.next())&&!a.done;)if(s=a.value[0],p=a.value[1],s&&"object"==typeof s){if(!M(c,e,s,p,t,o))return!1}else if(!(t.strict||e.has(s)&&N(k(e,s),p,t,o)||M(c,e,s,p,r({},t,{strict:!1}),o)))return!1;return 0===I(c)}return!0}(e,n,t,o);return!0}(e,n,a,o)}function F(e){return!(!e||"object"!=typeof e||"number"!=typeof e.length)&&("function"==typeof e.copy&&"function"==typeof e.slice&&(!(e.length>0&&"number"!=typeof e[0])&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))))}e.exports=function(e,n,t){return N(e,n,t,c())}},41:(e,n,t)=>{"use strict";var r=t(3036),o=t(8068),i=t(9675),a=t(5795);e.exports=function(e,n,t){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new i("`obj` must be an object or a function`");if("string"!=typeof n&&"symbol"!=typeof n)throw new i("`property` must be a string or a symbol`");if(arguments.length>3&&"boolean"!=typeof arguments[3]&&null!==arguments[3])throw new i("`nonEnumerable`, if provided, must be a boolean or null");if(arguments.length>4&&"boolean"!=typeof arguments[4]&&null!==arguments[4])throw new i("`nonWritable`, if provided, must be a boolean or null");if(arguments.length>5&&"boolean"!=typeof arguments[5]&&null!==arguments[5])throw new i("`nonConfigurable`, if provided, must be a boolean or null");if(arguments.length>6&&"boolean"!=typeof arguments[6])throw new i("`loose`, if provided, must be a boolean");var l=arguments.length>3?arguments[3]:null,c=arguments.length>4?arguments[4]:null,s=arguments.length>5?arguments[5]:null,u=arguments.length>6&&arguments[6],p=!!a&&a(e,n);if(r)r(e,n,{configurable:null===s&&p?p.configurable:!s,enumerable:null===l&&p?p.enumerable:!l,value:t,writable:null===c&&p?p.writable:!c});else{if(!u&&(l||c||s))throw new o("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable.");e[n]=t}}},8452:(e,n,t)=>{"use strict";var r=t(1189),o="function"==typeof Symbol&&"symbol"==typeof Symbol("foo"),i=Object.prototype.toString,a=Array.prototype.concat,l=t(41),c=t(592)(),s=function(e,n,t,r){if(n in e)if(!0===r){if(e[n]===t)return}else if("function"!=typeof(o=r)||"[object Function]"!==i.call(o)||!r())return;var o;c?l(e,n,t,!0):l(e,n,t)},u=function(e,n){var t=arguments.length>2?arguments[2]:{},i=r(n);o&&(i=a.call(i,Object.getOwnPropertySymbols(n)));for(var l=0;l{"use strict";var r,o=t(3126),i=t(5795);try{r=[].__proto__===Array.prototype}catch(e){if(!e||"object"!=typeof e||!("code"in e)||"ERR_PROTO_ACCESS"!==e.code)throw e}var a=!!r&&i&&i(Object.prototype,"__proto__"),l=Object,c=l.getPrototypeOf;e.exports=a&&"function"==typeof a.get?o([a.get]):"function"==typeof c&&function(e){return c(null==e?e:l(e))}},3036:e=>{"use strict";var n=Object.defineProperty||!1;if(n)try{n({},"a",{value:1})}catch(e){n=!1}e.exports=n},1237:e=>{"use strict";e.exports=EvalError},9383:e=>{"use strict";e.exports=Error},9290:e=>{"use strict";e.exports=RangeError},9538:e=>{"use strict";e.exports=ReferenceError},8068:e=>{"use strict";e.exports=SyntaxError},9675:e=>{"use strict";e.exports=TypeError},5345:e=>{"use strict";e.exports=URIError},9612:e=>{"use strict";e.exports=Object},2682:(e,n,t)=>{"use strict";var r=t(9600),o=Object.prototype.toString,i=Object.prototype.hasOwnProperty;e.exports=function(e,n,t){if(!r(n))throw new TypeError("iterator must be a function");var a,l;arguments.length>=3&&(a=t),l=e,"[object Array]"===o.call(l)?function(e,n,t){for(var r=0,o=e.length;r{"use strict";var n=Object.prototype.toString,t=Math.max,r=function(e,n){for(var t=[],r=0;r{"use strict";var r=t(9353);e.exports=Function.prototype.bind||r},4462:e=>{"use strict";var n=function(){return"string"==typeof function(){}.name},t=Object.getOwnPropertyDescriptor;if(t)try{t([],"length")}catch(e){t=null}n.functionsHaveConfigurableNames=function(){if(!n()||!t)return!1;var e=t((function(){}),"name");return!!e&&!!e.configurable};var r=Function.prototype.bind;n.boundFunctionsHaveNames=function(){return n()&&"function"==typeof r&&""!==function(){}.bind().name},e.exports=n},453:(e,n,t)=>{"use strict";var r,o=t(9612),i=t(9383),a=t(1237),l=t(9290),c=t(9538),s=t(8068),u=t(9675),p=t(5345),f=t(1514),d=t(8968),g=t(6188),h=t(8002),A=t(5880),b=t(414),v=t(3093),m=Function,y=function(e){try{return m('"use strict"; return ('+e+").constructor;")()}catch(e){}},E=t(5795),C=t(3036),w=function(){throw new u},x=E?function(){try{return w}catch(e){try{return E(arguments,"callee").get}catch(e){return w}}}():w,S=t(4039)(),_=t(3628),k=t(1064),O=t(8648),B=t(1002),P=t(76),T={},I="undefined"!=typeof Uint8Array&&_?_(Uint8Array):r,j={__proto__:null,"%AggregateError%":"undefined"==typeof AggregateError?r:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?r:ArrayBuffer,"%ArrayIteratorPrototype%":S&&_?_([][Symbol.iterator]()):r,"%AsyncFromSyncIteratorPrototype%":r,"%AsyncFunction%":T,"%AsyncGenerator%":T,"%AsyncGeneratorFunction%":T,"%AsyncIteratorPrototype%":T,"%Atomics%":"undefined"==typeof Atomics?r:Atomics,"%BigInt%":"undefined"==typeof BigInt?r:BigInt,"%BigInt64Array%":"undefined"==typeof BigInt64Array?r:BigInt64Array,"%BigUint64Array%":"undefined"==typeof BigUint64Array?r:BigUint64Array,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?r:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":i,"%eval%":eval,"%EvalError%":a,"%Float16Array%":"undefined"==typeof Float16Array?r:Float16Array,"%Float32Array%":"undefined"==typeof Float32Array?r:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?r:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?r:FinalizationRegistry,"%Function%":m,"%GeneratorFunction%":T,"%Int8Array%":"undefined"==typeof Int8Array?r:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?r:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?r:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":S&&_?_(_([][Symbol.iterator]())):r,"%JSON%":"object"==typeof JSON?JSON:r,"%Map%":"undefined"==typeof Map?r:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&S&&_?_((new Map)[Symbol.iterator]()):r,"%Math%":Math,"%Number%":Number,"%Object%":o,"%Object.getOwnPropertyDescriptor%":E,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?r:Promise,"%Proxy%":"undefined"==typeof Proxy?r:Proxy,"%RangeError%":l,"%ReferenceError%":c,"%Reflect%":"undefined"==typeof Reflect?r:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?r:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&S&&_?_((new Set)[Symbol.iterator]()):r,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?r:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":S&&_?_(""[Symbol.iterator]()):r,"%Symbol%":S?Symbol:r,"%SyntaxError%":s,"%ThrowTypeError%":x,"%TypedArray%":I,"%TypeError%":u,"%Uint8Array%":"undefined"==typeof Uint8Array?r:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?r:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?r:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?r:Uint32Array,"%URIError%":p,"%WeakMap%":"undefined"==typeof WeakMap?r:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?r:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?r:WeakSet,"%Function.prototype.call%":P,"%Function.prototype.apply%":B,"%Object.defineProperty%":C,"%Object.getPrototypeOf%":k,"%Math.abs%":f,"%Math.floor%":d,"%Math.max%":g,"%Math.min%":h,"%Math.pow%":A,"%Math.round%":b,"%Math.sign%":v,"%Reflect.getPrototypeOf%":O};if(_)try{null.error}catch(e){var z=_(_(e));j["%Error.prototype%"]=z}var D=function e(n){var t;if("%AsyncFunction%"===n)t=y("async function () {}");else if("%GeneratorFunction%"===n)t=y("function* () {}");else if("%AsyncGeneratorFunction%"===n)t=y("async function* () {}");else if("%AsyncGenerator%"===n){var r=e("%AsyncGeneratorFunction%");r&&(t=r.prototype)}else if("%AsyncIteratorPrototype%"===n){var o=e("%AsyncGenerator%");o&&_&&(t=_(o.prototype))}return j[n]=t,t},R={__proto__:null,"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},M=t(6743),N=t(9957),F=M.call(P,Array.prototype.concat),L=M.call(B,Array.prototype.splice),H=M.call(P,String.prototype.replace),U=M.call(P,String.prototype.slice),W=M.call(P,RegExp.prototype.exec),G=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,Y=/\\(\\)?/g,q=function(e,n){var t,r=e;if(N(R,r)&&(r="%"+(t=R[r])[0]+"%"),N(j,r)){var o=j[r];if(o===T&&(o=D(r)),void 0===o&&!n)throw new u("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:t,name:r,value:o}}throw new s("intrinsic "+e+" does not exist!")};e.exports=function(e,n){if("string"!=typeof e||0===e.length)throw new u("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof n)throw new u('"allowMissing" argument must be a boolean');if(null===W(/^%?[^%]*%?$/,e))throw new s("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var t=function(e){var n=U(e,0,1),t=U(e,-1);if("%"===n&&"%"!==t)throw new s("invalid intrinsic syntax, expected closing `%`");if("%"===t&&"%"!==n)throw new s("invalid intrinsic syntax, expected opening `%`");var r=[];return H(e,G,(function(e,n,t,o){r[r.length]=t?H(o,Y,"$1"):n||e})),r}(e),r=t.length>0?t[0]:"",o=q("%"+r+"%",n),i=o.name,a=o.value,l=!1,c=o.alias;c&&(r=c[0],L(t,F([0,1],c)));for(var p=1,f=!0;p=t.length){var A=E(a,d);a=(f=!!A)&&"get"in A&&!("originalValue"in A.get)?A.get:a[d]}else f=N(a,d),a=a[d];f&&!l&&(j[i]=a)}}return a}},1064:(e,n,t)=>{"use strict";var r=t(9612);e.exports=r.getPrototypeOf||null},8648:e=>{"use strict";e.exports="undefined"!=typeof Reflect&&Reflect.getPrototypeOf||null},3628:(e,n,t)=>{"use strict";var r=t(8648),o=t(1064),i=t(7176);e.exports=r?function(e){return r(e)}:o?function(e){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new TypeError("getProto: not an object");return o(e)}:i?function(e){return i(e)}:null},6549:e=>{"use strict";e.exports=Object.getOwnPropertyDescriptor},5795:(e,n,t)=>{"use strict";var r=t(6549);if(r)try{r([],"length")}catch(e){r=null}e.exports=r},9790:e=>{"use strict";var n="undefined"!=typeof BigInt&&BigInt;e.exports=function(){return"function"==typeof n&&"function"==typeof BigInt&&"bigint"==typeof n(42)&&"bigint"==typeof BigInt(42)}},592:(e,n,t)=>{"use strict";var r=t(3036),o=function(){return!!r};o.hasArrayLengthDefineBug=function(){if(!r)return null;try{return 1!==r([],"length",{value:1}).length}catch(e){return!0}},e.exports=o},4039:(e,n,t)=>{"use strict";var r="undefined"!=typeof Symbol&&Symbol,o=t(1333);e.exports=function(){return"function"==typeof r&&("function"==typeof Symbol&&("symbol"==typeof r("foo")&&("symbol"==typeof Symbol("bar")&&o())))}},1333:e=>{"use strict";e.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var e={},n=Symbol("test"),t=Object(n);if("string"==typeof n)return!1;if("[object Symbol]"!==Object.prototype.toString.call(n))return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;for(var r in e[n]=42,e)return!1;if("function"==typeof Object.keys&&0!==Object.keys(e).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(e).length)return!1;var o=Object.getOwnPropertySymbols(e);if(1!==o.length||o[0]!==n)return!1;if(!Object.prototype.propertyIsEnumerable.call(e,n))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var i=Object.getOwnPropertyDescriptor(e,n);if(42!==i.value||!0!==i.enumerable)return!1}return!0}},9092:(e,n,t)=>{"use strict";var r=t(1333);e.exports=function(){return r()&&!!Symbol.toStringTag}},9957:(e,n,t)=>{"use strict";var r=Function.prototype.call,o=Object.prototype.hasOwnProperty,i=t(6743);e.exports=i.call(r,o)},63:(e,n,t)=>{"use strict";var r=t(9957),o=t(920)(),i=t(9675),a={assert:function(e,n){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new i("`O` is not an object");if("string"!=typeof n)throw new i("`slot` must be a string");if(o.assert(e),!a.has(e,n))throw new i("`"+n+"` is not present on `O`")},get:function(e,n){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new i("`O` is not an object");if("string"!=typeof n)throw new i("`slot` must be a string");var t=o.get(e);return t&&t["$"+n]},has:function(e,n){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new i("`O` is not an object");if("string"!=typeof n)throw new i("`slot` must be a string");var t=o.get(e);return!!t&&r(t,"$"+n)},set:function(e,n,t){if(!e||"object"!=typeof e&&"function"!=typeof e)throw new i("`O` is not an object");if("string"!=typeof n)throw new i("`slot` must be a string");var r=o.get(e);r||(r={},o.set(e,r)),r["$"+n]=t}};Object.freeze&&Object.freeze(a),e.exports=a},7244:(e,n,t)=>{"use strict";var r=t(9092)(),o=t(6556)("Object.prototype.toString"),i=function(e){return!(r&&e&&"object"==typeof e&&Symbol.toStringTag in e)&&"[object Arguments]"===o(e)},a=function(e){return!!i(e)||null!==e&&"object"==typeof e&&"length"in e&&"number"==typeof e.length&&e.length>=0&&"[object Array]"!==o(e)&&"callee"in e&&"[object Function]"===o(e.callee)},l=function(){return i(arguments)}();i.isLegacyArguments=a,e.exports=l?i:a},4670:(e,n,t)=>{"use strict";var r=t(487),o=t(6556),i=t(453)("%ArrayBuffer%",!0),a=o("ArrayBuffer.prototype.byteLength",!0),l=o("Object.prototype.toString"),c=!!i&&!a&&new i(0).slice,s=!!c&&r(c);e.exports=a||s?function(e){if(!e||"object"!=typeof e)return!1;try{return a?a(e):s(e,0),!0}catch(e){return!1}}:i?function(e){return"[object ArrayBuffer]"===l(e)}:function(e){return!1}},9803:(e,n,t)=>{"use strict";if(t(9790)()){var r=BigInt.prototype.valueOf;e.exports=function(e){return null!=e&&"boolean"!=typeof e&&"string"!=typeof e&&"number"!=typeof e&&"symbol"!=typeof e&&"function"!=typeof e&&("bigint"==typeof e||function(e){try{return r.call(e),!0}catch(e){}return!1}(e))}}else e.exports=function(e){return!1}},5128:(e,n,t)=>{"use strict";var r=t(6556),o=r("Boolean.prototype.toString"),i=r("Object.prototype.toString"),a=t(9092)();e.exports=function(e){return"boolean"==typeof e||null!==e&&"object"==typeof e&&(a?function(e){try{return o(e),!0}catch(e){return!1}}(e):"[object Boolean]"===i(e))}},9600:e=>{"use strict";var n,t,r=Function.prototype.toString,o="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof o&&"function"==typeof Object.defineProperty)try{n=Object.defineProperty({},"length",{get:function(){throw t}}),t={},o((function(){throw 42}),null,n)}catch(e){e!==t&&(o=null)}else o=null;var i=/^\s*class\b/,a=function(e){try{var n=r.call(e);return i.test(n)}catch(e){return!1}},l=function(e){try{return!a(e)&&(r.call(e),!0)}catch(e){return!1}},c=Object.prototype.toString,s="function"==typeof Symbol&&!!Symbol.toStringTag,u=!(0 in[,]),p=function(){return!1};if("object"==typeof document){var f=document.all;c.call(f)===c.call(document.all)&&(p=function(e){if((u||!e)&&(void 0===e||"object"==typeof e))try{var n=c.call(e);return("[object HTMLAllCollection]"===n||"[object HTML document.all class]"===n||"[object HTMLCollection]"===n||"[object Object]"===n)&&null==e("")}catch(e){}return!1})}e.exports=o?function(e){if(p(e))return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;try{o(e,null,n)}catch(e){if(e!==t)return!1}return!a(e)&&l(e)}:function(e){if(p(e))return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if(s)return l(e);if(a(e))return!1;var n=c.call(e);return!("[object Function]"!==n&&"[object GeneratorFunction]"!==n&&!/^\[object HTML/.test(n))&&l(e)}},2120:(e,n,t)=>{"use strict";var r=t(6556),o=r("Date.prototype.getDay"),i=r("Object.prototype.toString"),a=t(9092)();e.exports=function(e){return"object"==typeof e&&null!==e&&(a?function(e){try{return o(e),!0}catch(e){return!1}}(e):"[object Date]"===i(e))}},1421:e=>{"use strict";var n,t="function"==typeof Map&&Map.prototype?Map:null,r="function"==typeof Set&&Set.prototype?Set:null;t||(n=function(e){return!1});var o=t?Map.prototype.has:null,i=r?Set.prototype.has:null;n||o||(n=function(e){return!1}),e.exports=n||function(e){if(!e||"object"!=typeof e)return!1;try{if(o.call(e),i)try{i.call(e)}catch(e){return!0}return e instanceof t}catch(e){}return!1}},1703:(e,n,t)=>{"use strict";var r=t(6556),o=r("Number.prototype.toString"),i=r("Object.prototype.toString"),a=t(9092)();e.exports=function(e){return"number"==typeof e||!(!e||"object"!=typeof e)&&(a?function(e){try{return o(e),!0}catch(e){return!1}}(e):"[object Number]"===i(e))}},4035:(e,n,t)=>{"use strict";var r,o=t(6556),i=t(9092)(),a=t(9957),l=t(5795);if(i){var c=o("RegExp.prototype.exec"),s={},u=function(){throw s},p={toString:u,valueOf:u};"symbol"==typeof Symbol.toPrimitive&&(p[Symbol.toPrimitive]=u),r=function(e){if(!e||"object"!=typeof e)return!1;var n=l(e,"lastIndex");if(!(n&&a(n,"value")))return!1;try{c(e,p)}catch(e){return e===s}}}else{var f=o("Object.prototype.toString");r=function(e){return!(!e||"object"!=typeof e&&"function"!=typeof e)&&"[object RegExp]"===f(e)}}e.exports=r},256:e=>{"use strict";var n,t="function"==typeof Map&&Map.prototype?Map:null,r="function"==typeof Set&&Set.prototype?Set:null;r||(n=function(e){return!1});var o=t?Map.prototype.has:null,i=r?Set.prototype.has:null;n||i||(n=function(e){return!1}),e.exports=n||function(e){if(!e||"object"!=typeof e)return!1;try{if(i.call(e),o)try{o.call(e)}catch(e){return!0}return e instanceof r}catch(e){}return!1}},7070:(e,n,t)=>{"use strict";var r=t(6556)("SharedArrayBuffer.prototype.byteLength",!0);e.exports=r?function(e){if(!e||"object"!=typeof e)return!1;try{return r(e),!0}catch(e){return!1}}:function(e){return!1}},4761:(e,n,t)=>{"use strict";var r=t(6556),o=r("String.prototype.valueOf"),i=r("Object.prototype.toString"),a=t(9092)();e.exports=function(e){return"string"==typeof e||!(!e||"object"!=typeof e)&&(a?function(e){try{return o(e),!0}catch(e){return!1}}(e):"[object String]"===i(e))}},3612:(e,n,t)=>{"use strict";var r=t(6556),o=r("Object.prototype.toString"),i=t(4039)(),a=t(9721);if(i){var l=r("Symbol.prototype.toString"),c=a(/^Symbol\(.*\)$/);e.exports=function(e){if("symbol"==typeof e)return!0;if(!e||"object"!=typeof e||"[object Symbol]"!==o(e))return!1;try{return function(e){return"symbol"==typeof e.valueOf()&&c(l(e))}(e)}catch(e){return!1}}}else e.exports=function(e){return!1}},7842:e=>{"use strict";var n,t="function"==typeof WeakMap&&WeakMap.prototype?WeakMap:null,r="function"==typeof WeakSet&&WeakSet.prototype?WeakSet:null;t||(n=function(e){return!1});var o=t?t.prototype.has:null,i=r?r.prototype.has:null;n||o||(n=function(e){return!1}),e.exports=n||function(e){if(!e||"object"!=typeof e)return!1;try{if(o.call(e,o),i)try{i.call(e,i)}catch(e){return!0}return e instanceof t}catch(e){}return!1}},2648:(e,n,t)=>{"use strict";var r=t(453),o=t(6556),i=r("%WeakSet%",!0),a=o("WeakSet.prototype.has",!0);if(a){var l=o("WeakMap.prototype.has",!0);e.exports=function(e){if(!e||"object"!=typeof e)return!1;try{if(a(e,a),l)try{l(e,l)}catch(e){return!0}return e instanceof i}catch(e){}return!1}}else e.exports=function(e){return!1}},4634:e=>{var n={}.toString;e.exports=Array.isArray||function(e){return"[object Array]"==n.call(e)}},1514:e=>{"use strict";e.exports=Math.abs},8968:e=>{"use strict";e.exports=Math.floor},4459:e=>{"use strict";e.exports=Number.isNaN||function(e){return e!=e}},6188:e=>{"use strict";e.exports=Math.max},8002:e=>{"use strict";e.exports=Math.min},5880:e=>{"use strict";e.exports=Math.pow},414:e=>{"use strict";e.exports=Math.round},3093:(e,n,t)=>{"use strict";var r=t(4459);e.exports=function(e){return r(e)||0===e?e:e<0?-1:1}},5228:e=>{"use strict"; /* object-assign (c) Sindre Sorhus @license MIT */var n=Object.getOwnPropertySymbols,t=Object.prototype.hasOwnProperty,r=Object.prototype.propertyIsEnumerable;e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var n={},t=0;t<10;t++)n["_"+String.fromCharCode(t)]=t;if("0123456789"!==Object.getOwnPropertyNames(n).map((function(e){return n[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,o){for(var i,a,l=function(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}(e),c=1;c{var r="function"==typeof Map&&Map.prototype,o=Object.getOwnPropertyDescriptor&&r?Object.getOwnPropertyDescriptor(Map.prototype,"size"):null,i=r&&o&&"function"==typeof o.get?o.get:null,a=r&&Map.prototype.forEach,l="function"==typeof Set&&Set.prototype,c=Object.getOwnPropertyDescriptor&&l?Object.getOwnPropertyDescriptor(Set.prototype,"size"):null,s=l&&c&&"function"==typeof c.get?c.get:null,u=l&&Set.prototype.forEach,p="function"==typeof WeakMap&&WeakMap.prototype?WeakMap.prototype.has:null,f="function"==typeof WeakSet&&WeakSet.prototype?WeakSet.prototype.has:null,d="function"==typeof WeakRef&&WeakRef.prototype?WeakRef.prototype.deref:null,g=Boolean.prototype.valueOf,h=Object.prototype.toString,A=Function.prototype.toString,b=String.prototype.match,v=String.prototype.slice,m=String.prototype.replace,y=String.prototype.toUpperCase,E=String.prototype.toLowerCase,C=RegExp.prototype.test,w=Array.prototype.concat,x=Array.prototype.join,S=Array.prototype.slice,_=Math.floor,k="function"==typeof BigInt?BigInt.prototype.valueOf:null,O=Object.getOwnPropertySymbols,B="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?Symbol.prototype.toString:null,P="function"==typeof Symbol&&"object"==typeof Symbol.iterator,T="function"==typeof Symbol&&Symbol.toStringTag&&(typeof Symbol.toStringTag===P||"symbol")?Symbol.toStringTag:null,I=Object.prototype.propertyIsEnumerable,j=("function"==typeof Reflect?Reflect.getPrototypeOf:Object.getPrototypeOf)||([].__proto__===Array.prototype?function(e){return e.__proto__}:null);function z(e,n){if(e===1/0||e===-1/0||e!=e||e&&e>-1e3&&e<1e3||C.call(/e/,n))return n;var t=/[0-9](?=(?:[0-9]{3})+(?![0-9]))/g;if("number"==typeof e){var r=e<0?-_(-e):_(e);if(r!==e){var o=String(r),i=v.call(n,o.length+1);return m.call(o,t,"$&_")+"."+m.call(m.call(i,/([0-9]{3})/g,"$&_"),/_$/,"")}}return m.call(n,t,"$&_")}var D=t(2634),R=D.custom,M=Y(R)?R:null,N={__proto__:null,double:'"',single:"'"},F={__proto__:null,double:/(["\\])/g,single:/(['\\])/g};function L(e,n,t){var r=t.quoteStyle||n,o=N[r];return o+e+o}function H(e){return m.call(String(e),/"/g,""")}function U(e){return!T||!("object"==typeof e&&(T in e||void 0!==e[T]))}function W(e){return"[object Array]"===$(e)&&U(e)}function G(e){return"[object RegExp]"===$(e)&&U(e)}function Y(e){if(P)return e&&"object"==typeof e&&e instanceof Symbol;if("symbol"==typeof e)return!0;if(!e||"object"!=typeof e||!B)return!1;try{return B.call(e),!0}catch(e){}return!1}e.exports=function e(n,r,o,l){var c=r||{};if(V(c,"quoteStyle")&&!V(N,c.quoteStyle))throw new TypeError('option "quoteStyle" must be "single" or "double"');if(V(c,"maxStringLength")&&("number"==typeof c.maxStringLength?c.maxStringLength<0&&c.maxStringLength!==1/0:null!==c.maxStringLength))throw new TypeError('option "maxStringLength", if provided, must be a positive integer, Infinity, or `null`');var h=!V(c,"customInspect")||c.customInspect;if("boolean"!=typeof h&&"symbol"!==h)throw new TypeError("option \"customInspect\", if provided, must be `true`, `false`, or `'symbol'`");if(V(c,"indent")&&null!==c.indent&&"\t"!==c.indent&&!(parseInt(c.indent,10)===c.indent&&c.indent>0))throw new TypeError('option "indent" must be "\\t", an integer > 0, or `null`');if(V(c,"numericSeparator")&&"boolean"!=typeof c.numericSeparator)throw new TypeError('option "numericSeparator", if provided, must be `true` or `false`');var y=c.numericSeparator;if(void 0===n)return"undefined";if(null===n)return"null";if("boolean"==typeof n)return n?"true":"false";if("string"==typeof n)return Q(n,c);if("number"==typeof n){if(0===n)return 1/0/n>0?"0":"-0";var C=String(n);return y?z(n,C):C}if("bigint"==typeof n){var _=String(n)+"n";return y?z(n,_):_}var O=void 0===c.depth?5:c.depth;if(void 0===o&&(o=0),o>=O&&O>0&&"object"==typeof n)return W(n)?"[Array]":"[Object]";var R=function(e,n){var t;if("\t"===e.indent)t="\t";else{if(!("number"==typeof e.indent&&e.indent>0))return null;t=x.call(Array(e.indent+1)," ")}return{base:t,prev:x.call(Array(n+1),t)}}(c,o);if(void 0===l)l=[];else if(K(l,n)>=0)return"[Circular]";function F(n,t,r){if(t&&(l=S.call(l)).push(t),r){var i={depth:c.depth};return V(c,"quoteStyle")&&(i.quoteStyle=c.quoteStyle),e(n,i,o+1,l)}return e(n,c,o+1,l)}if("function"==typeof n&&!G(n)){var q=function(e){if(e.name)return e.name;var n=b.call(A.call(e),/^function\s*([\w$]+)/);if(n)return n[1];return null}(n),X=te(n,F);return"[Function"+(q?": "+q:" (anonymous)")+"]"+(X.length>0?" { "+x.call(X,", ")+" }":"")}if(Y(n)){var re=P?m.call(String(n),/^(Symbol\(.*\))_[^)]*$/,"$1"):B.call(n);return"object"!=typeof n||P?re:Z(re)}if(function(e){if(!e||"object"!=typeof e)return!1;if("undefined"!=typeof HTMLElement&&e instanceof HTMLElement)return!0;return"string"==typeof e.nodeName&&"function"==typeof e.getAttribute}(n)){for(var oe="<"+E.call(String(n.nodeName)),ie=n.attributes||[],ae=0;ae"}if(W(n)){if(0===n.length)return"[]";var le=te(n,F);return R&&!function(e){for(var n=0;n=0)return!1;return!0}(le)?"["+ne(le,R)+"]":"[ "+x.call(le,", ")+" ]"}if(function(e){return"[object Error]"===$(e)&&U(e)}(n)){var ce=te(n,F);return"cause"in Error.prototype||!("cause"in n)||I.call(n,"cause")?0===ce.length?"["+String(n)+"]":"{ ["+String(n)+"] "+x.call(ce,", ")+" }":"{ ["+String(n)+"] "+x.call(w.call("[cause]: "+F(n.cause),ce),", ")+" }"}if("object"==typeof n&&h){if(M&&"function"==typeof n[M]&&D)return D(n,{depth:O-o});if("symbol"!==h&&"function"==typeof n.inspect)return n.inspect()}if(function(e){if(!i||!e||"object"!=typeof e)return!1;try{i.call(e);try{s.call(e)}catch(e){return!0}return e instanceof Map}catch(e){}return!1}(n)){var se=[];return a&&a.call(n,(function(e,t){se.push(F(t,n,!0)+" => "+F(e,n))})),ee("Map",i.call(n),se,R)}if(function(e){if(!s||!e||"object"!=typeof e)return!1;try{s.call(e);try{i.call(e)}catch(e){return!0}return e instanceof Set}catch(e){}return!1}(n)){var ue=[];return u&&u.call(n,(function(e){ue.push(F(e,n))})),ee("Set",s.call(n),ue,R)}if(function(e){if(!p||!e||"object"!=typeof e)return!1;try{p.call(e,p);try{f.call(e,f)}catch(e){return!0}return e instanceof WeakMap}catch(e){}return!1}(n))return J("WeakMap");if(function(e){if(!f||!e||"object"!=typeof e)return!1;try{f.call(e,f);try{p.call(e,p)}catch(e){return!0}return e instanceof WeakSet}catch(e){}return!1}(n))return J("WeakSet");if(function(e){if(!d||!e||"object"!=typeof e)return!1;try{return d.call(e),!0}catch(e){}return!1}(n))return J("WeakRef");if(function(e){return"[object Number]"===$(e)&&U(e)}(n))return Z(F(Number(n)));if(function(e){if(!e||"object"!=typeof e||!k)return!1;try{return k.call(e),!0}catch(e){}return!1}(n))return Z(F(k.call(n)));if(function(e){return"[object Boolean]"===$(e)&&U(e)}(n))return Z(g.call(n));if(function(e){return"[object String]"===$(e)&&U(e)}(n))return Z(F(String(n)));if("undefined"!=typeof window&&n===window)return"{ [object Window] }";if("undefined"!=typeof globalThis&&n===globalThis||void 0!==t.g&&n===t.g)return"{ [object globalThis] }";if(!function(e){return"[object Date]"===$(e)&&U(e)}(n)&&!G(n)){var pe=te(n,F),fe=j?j(n)===Object.prototype:n instanceof Object||n.constructor===Object,de=n instanceof Object?"":"null prototype",ge=!fe&&T&&Object(n)===n&&T in n?v.call($(n),8,-1):de?"Object":"",he=(fe||"function"!=typeof n.constructor?"":n.constructor.name?n.constructor.name+" ":"")+(ge||de?"["+x.call(w.call([],ge||[],de||[]),": ")+"] ":"");return 0===pe.length?he+"{}":R?he+"{"+ne(pe,R)+"}":he+"{ "+x.call(pe,", ")+" }"}return String(n)};var q=Object.prototype.hasOwnProperty||function(e){return e in this};function V(e,n){return q.call(e,n)}function $(e){return h.call(e)}function K(e,n){if(e.indexOf)return e.indexOf(n);for(var t=0,r=e.length;tn.maxStringLength){var t=e.length-n.maxStringLength,r="... "+t+" more character"+(t>1?"s":"");return Q(v.call(e,0,n.maxStringLength),n)+r}var o=F[n.quoteStyle||"single"];return o.lastIndex=0,L(m.call(m.call(e,o,"\\$1"),/[\x00-\x1f]/g,X),"single",n)}function X(e){var n=e.charCodeAt(0),t={8:"b",9:"t",10:"n",12:"f",13:"r"}[n];return t?"\\"+t:"\\x"+(n<16?"0":"")+y.call(n.toString(16))}function Z(e){return"Object("+e+")"}function J(e){return e+" { ? }"}function ee(e,n,t,r){return e+" ("+n+") {"+(r?ne(t,r):x.call(t,", "))+"}"}function ne(e,n){if(0===e.length)return"";var t="\n"+n.prev+n.base;return t+x.call(e,","+t)+"\n"+n.prev}function te(e,n){var t=W(e),r=[];if(t){r.length=e.length;for(var o=0;o{"use strict";var n=function(e){return e!=e};e.exports=function(e,t){return 0===e&&0===t?1/e==1/t:e===t||!(!n(e)||!n(t))}},7653:(e,n,t)=>{"use strict";var r=t(8452),o=t(487),i=t(9211),a=t(9394),l=t(6576),c=o(a(),Object);r(c,{getPolyfill:a,implementation:i,shim:l}),e.exports=c},9394:(e,n,t)=>{"use strict";var r=t(9211);e.exports=function(){return"function"==typeof Object.is?Object.is:r}},6576:(e,n,t)=>{"use strict";var r=t(9394),o=t(8452);e.exports=function(){var e=r();return o(Object,{is:e},{is:function(){return Object.is!==e}}),e}},8875:(e,n,t)=>{"use strict";var r;if(!Object.keys){var o=Object.prototype.hasOwnProperty,i=Object.prototype.toString,a=t(1093),l=Object.prototype.propertyIsEnumerable,c=!l.call({toString:null},"toString"),s=l.call((function(){}),"prototype"),u=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],p=function(e){var n=e.constructor;return n&&n.prototype===e},f={$applicationCache:!0,$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$onmozfullscreenchange:!0,$onmozfullscreenerror:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},d=function(){if("undefined"==typeof window)return!1;for(var e in window)try{if(!f["$"+e]&&o.call(window,e)&&null!==window[e]&&"object"==typeof window[e])try{p(window[e])}catch(e){return!0}}catch(e){return!0}return!1}();r=function(e){var n=null!==e&&"object"==typeof e,t="[object Function]"===i.call(e),r=a(e),l=n&&"[object String]"===i.call(e),f=[];if(!n&&!t&&!r)throw new TypeError("Object.keys called on a non-object");var g=s&&t;if(l&&e.length>0&&!o.call(e,0))for(var h=0;h0)for(var A=0;A{"use strict";var r=Array.prototype.slice,o=t(1093),i=Object.keys,a=i?function(e){return i(e)}:t(8875),l=Object.keys;a.shim=function(){if(Object.keys){var e=function(){var e=Object.keys(arguments);return e&&e.length===arguments.length}(1,2);e||(Object.keys=function(e){return o(e)?l(r.call(e)):l(e)})}else Object.keys=a;return Object.keys||a},e.exports=a},1093:e=>{"use strict";var n=Object.prototype.toString;e.exports=function(e){var t=n.call(e),r="[object Arguments]"===t;return r||(r="[object Array]"!==t&&null!==e&&"object"==typeof e&&"number"==typeof e.length&&e.length>=0&&"[object Function]"===n.call(e.callee)),r}},8403:(e,n,t)=>{"use strict";var r=t(1189),o=t(1333)(),i=t(6556),a=t(9612),l=i("Array.prototype.push"),c=i("Object.prototype.propertyIsEnumerable"),s=o?a.getOwnPropertySymbols:null;e.exports=function(e,n){if(null==e)throw new TypeError("target must be an object");var t=a(e);if(1===arguments.length)return t;for(var i=1;i{"use strict";var r=t(8452),o=t(487),i=t(8403),a=t(9133),l=t(984),c=o.apply(a()),s=function(e,n){return c(Object,arguments)};r(s,{getPolyfill:a,implementation:i,shim:l}),e.exports=s},9133:(e,n,t)=>{"use strict";var r=t(8403);e.exports=function(){return Object.assign?function(){if(!Object.assign)return!1;for(var e="abcdefghijklmnopqrst",n=e.split(""),t={},r=0;r{"use strict";var r=t(8452),o=t(9133);e.exports=function(){var e=o();return r(Object,{assign:e},{assign:function(){return Object.assign!==e}}),e}},6578:e=>{"use strict";e.exports=["Float16Array","Float32Array","Float64Array","Int8Array","Int16Array","Int32Array","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","BigInt64Array","BigUint64Array"]},2694:(e,n,t)=>{"use strict";var r=t(6925);function o(){}function i(){}i.resetWarningCache=o,e.exports=function(){function e(e,n,t,o,i,a){if(a!==r){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function n(){return e}e.isRequired=e;var t={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:n,element:e,elementType:e,instanceOf:n,node:e,objectOf:n,oneOf:n,oneOfType:n,shape:n,exact:n,checkPropTypes:i,resetWarningCache:o};return t.PropTypes=t,t}},5556:(e,n,t)=>{e.exports=t(2694)()},6925:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},2551:(e,n,t)=>{"use strict";var r=t(6540),o=t(5228),i=t(9982); /** @license React v17.0.2 * react-dom.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */function a(e){for(var n="https://reactjs.org/docs/error-decoder.html?invariant="+e,t=1;t