From 21a4dfd5a36f1c90899ed7e168483a5847ef0320 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 10 Aug 2026 14:17:07 +0000 Subject: [PATCH 1/2] ADFA-4826: Add shared IDE Compose theming in common-compose New leaf module holding the Compose theme any module can opt into: IdeColorScheme derives a Material3 scheme from the IDE's own colour resources, IdeTheme applies it and seeds LocalContentColor so text on a themed surface inherits the right colour. Compose types are exposed as `api` because consumers write Compose against them. Modules that are not Compose depend on nothing new. --- ARCHITECTURE.md | 2 +- common-compose/build.gradle.kts | 29 +++++ .../common/compose/IdeColorScheme.kt | 86 ++++++++++++ .../androidide/common/compose/IdeTheme.kt | 48 +++++++ .../common/compose/IdeColorSchemeTest.kt | 123 ++++++++++++++++++ settings.gradle.kts | 1 + 6 files changed, 288 insertions(+), 1 deletion(-) create mode 100644 common-compose/build.gradle.kts create mode 100644 common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeColorScheme.kt create mode 100644 common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeTheme.kt create mode 100644 common-compose/src/test/java/com/itsaky/androidide/common/compose/IdeColorSchemeTest.kt diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 3ecaccc691..8c1d35d005 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -64,7 +64,7 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil | Shell | `termux:{termux-app,termux-shared,termux-view,termux-emulator}` | Embedded Termux shell and terminal. | | Plugin system | `plugin-api`, `plugin-api:plugin-builder`, `plugin-manager` | In-app plugin SDK + manager — `AndroidManifest.xml` `` contract, permissions, extensions. See [plugin-api.md](docs/plugin-api.md) for the API surface & compatibility policy. | | On-device AI | `llama-api`, `llama-impl` | llama.cpp integration, shipped as a per-flavor native AAR. | -| Cross-cutting | `eventbus`, `eventbus-android`, `eventbus-events`, `common`, `common-ui`, `logger`, `resources`, `preferences`, `shared` | Shared infra and the event bus. | +| Cross-cutting | `eventbus`, `eventbus-android`, `eventbus-events`, `common`, `common-ui`, `common-compose`, `logger`, `resources`, `preferences`, `shared` | Shared infra and the event bus. `common-compose` holds the Compose theming any module can opt into (see [ADR 0009](docs/adr/0009-jetpack-compose-for-new-ui.md)); it is a leaf so modules that aren't Compose pay nothing. | | Testing | `testing:{android,unit,lsp,tooling,common}` | Shared test harnesses, split by what's under test. | **Dependency rules (enforced):** diff --git a/common-compose/build.gradle.kts b/common-compose/build.gradle.kts new file mode 100644 index 0000000000..50b8c4c042 --- /dev/null +++ b/common-compose/build.gradle.kts @@ -0,0 +1,29 @@ +import com.itsaky.androidide.build.config.BuildConfig + +plugins { + id("com.android.library") + id("kotlin-android") + alias(libs.plugins.kotlin.compose) +} + +android { + namespace = "${BuildConfig.PACKAGE_NAME}.common.compose" + + buildFeatures { + compose = true + } +} + +dependencies { + // api, not implementation: consumers write Compose against these types (ColorScheme, Typography), + // so they must be on the consumer's compile classpath. + api(platform(libs.compose.bom)) + api(libs.compose.runtime) + api(libs.compose.material3) + api(libs.compose.ui) + + implementation(libs.compose.foundation) + implementation(libs.google.material) + + testImplementation(projects.testing.unit) +} diff --git a/common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeColorScheme.kt b/common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeColorScheme.kt new file mode 100644 index 0000000000..a777ae3fb4 --- /dev/null +++ b/common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeColorScheme.kt @@ -0,0 +1,86 @@ +package com.itsaky.androidide.common.compose + +import android.content.Context +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import com.google.android.material.color.MaterialColors +import com.google.android.material.R as MaterialR + +/** + * Resolves a theme colour attribute, or null when the attribute is not defined. + * + * Exists so [ideColorScheme] can be exercised without an Android [Context]: the mapping from Material + * attributes to Compose colour roles is the part worth testing, and it is pure once resolution is a + * parameter. + */ +typealias ColorAttrResolver = (attr: Int) -> Color? + +/** + * A Compose [ColorScheme] built from the IDE's XML theme, so Compose UI matches the surrounding + * View-based IDE exactly -- including the user's light/dark choice and any theme overlay in effect. + * + * Every role falls back to the stock Material baseline ([lightColorScheme]/[darkColorScheme]) when its + * attribute is undefined, so a partial XML theme degrades to sensible colours rather than to + * transparent or black. + * + * [dark] selects the baseline. It is the caller's business rather than something read from the context + * here, because the attribute values already come from whichever theme is applied; the baseline only + * matters for roles the theme does not define. + */ +fun ideColorScheme( + dark: Boolean, + resolve: ColorAttrResolver, +): ColorScheme { + val base = if (dark) darkColorScheme() else lightColorScheme() + + fun role( + attr: Int, + fallback: Color, + ): Color = resolve(attr) ?: fallback + + return base.copy( + primary = role(MaterialR.attr.colorPrimary, base.primary), + onPrimary = role(MaterialR.attr.colorOnPrimary, base.onPrimary), + primaryContainer = role(MaterialR.attr.colorPrimaryContainer, base.primaryContainer), + onPrimaryContainer = role(MaterialR.attr.colorOnPrimaryContainer, base.onPrimaryContainer), + secondary = role(MaterialR.attr.colorSecondary, base.secondary), + onSecondary = role(MaterialR.attr.colorOnSecondary, base.onSecondary), + secondaryContainer = role(MaterialR.attr.colorSecondaryContainer, base.secondaryContainer), + onSecondaryContainer = role(MaterialR.attr.colorOnSecondaryContainer, base.onSecondaryContainer), + tertiary = role(MaterialR.attr.colorTertiary, base.tertiary), + onTertiary = role(MaterialR.attr.colorOnTertiary, base.onTertiary), + tertiaryContainer = role(MaterialR.attr.colorTertiaryContainer, base.tertiaryContainer), + onTertiaryContainer = role(MaterialR.attr.colorOnTertiaryContainer, base.onTertiaryContainer), + // colorBackground is a platform attribute, not a Material one. + background = role(android.R.attr.colorBackground, base.background), + onBackground = role(MaterialR.attr.colorOnBackground, base.onBackground), + surface = role(MaterialR.attr.colorSurface, base.surface), + onSurface = role(MaterialR.attr.colorOnSurface, base.onSurface), + surfaceVariant = role(MaterialR.attr.colorSurfaceVariant, base.surfaceVariant), + onSurfaceVariant = role(MaterialR.attr.colorOnSurfaceVariant, base.onSurfaceVariant), + outline = role(MaterialR.attr.colorOutline, base.outline), + outlineVariant = role(MaterialR.attr.colorOutlineVariant, base.outlineVariant), + error = role(MaterialR.attr.colorError, base.error), + onError = role(MaterialR.attr.colorOnError, base.onError), + errorContainer = role(MaterialR.attr.colorErrorContainer, base.errorContainer), + onErrorContainer = role(MaterialR.attr.colorOnErrorContainer, base.onErrorContainer), + ) +} + +/** [ideColorScheme] reading the live attribute values off this context's theme. */ +fun Context.ideColorScheme(dark: Boolean): ColorScheme = ideColorScheme(dark, materialColorResolver()) + +/** + * Resolves through [MaterialColors], which handles both direct colour values and colour-resource + * references. A sentinel distinguishes "undefined" from a legitimately resolved colour -- returning 0 + * would be indistinguishable from transparent black. + */ +private fun Context.materialColorResolver(): ColorAttrResolver = + { attr -> + val resolved = MaterialColors.getColor(this, attr, UNRESOLVED) + if (resolved == UNRESOLVED) null else Color(resolved) + } + +private const val UNRESOLVED = Int.MIN_VALUE diff --git a/common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeTheme.kt b/common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeTheme.kt new file mode 100644 index 0000000000..9fecc28919 --- /dev/null +++ b/common-compose/src/main/java/com/itsaky/androidide/common/compose/IdeTheme.kt @@ -0,0 +1,48 @@ +package com.itsaky.androidide.common.compose + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +/** + * Wraps Compose content in a [MaterialTheme] whose colours come from the IDE's XML theme, so a Compose + * surface is indistinguishable from the View-based UI around it. + * + * Use this instead of a bare `MaterialTheme { }`: the bare form falls back to Material's purple + * baseline, which looks nothing like the IDE and ignores the user's theme entirely. + * + * [typography] is a parameter because branding type is a separate concern from colour -- overlay + * windows brand theirs with the IDE's Atkinson Hyperlegible face, while most surfaces want the + * default. + * + * [contentColor] seeds [LocalContentColor], which is **not** something [MaterialTheme] sets. Its + * global default is [androidx.compose.ui.graphics.Color.Black], and normally only a `Surface` replaces + * it (via `contentColorFor`). Content hosted inside a View that already draws the background -- a + * `BottomSheetDialog`, an overlay window, a `ComposeView` in an XML layout -- has no `Surface`, so + * every `Text` would render black regardless of how dark the background is. Defaulting to `onSurface` + * makes that case correct; a `Surface` further down still overrides it, so screens that do use one are + * unaffected. + */ +@Composable +fun IdeTheme( + typography: Typography = MaterialTheme.typography, + contentColor: Color? = null, + content: @Composable () -> Unit, +) { + val context = LocalContext.current + val dark = isSystemInDarkTheme() + // Attribute resolution reads the theme, so it is keyed on both the context and the dark-mode flag. + val colorScheme = remember(context, dark) { context.ideColorScheme(dark) } + MaterialTheme(colorScheme = colorScheme, typography = typography) { + CompositionLocalProvider( + LocalContentColor provides (contentColor ?: colorScheme.onSurface), + content = content, + ) + } +} diff --git a/common-compose/src/test/java/com/itsaky/androidide/common/compose/IdeColorSchemeTest.kt b/common-compose/src/test/java/com/itsaky/androidide/common/compose/IdeColorSchemeTest.kt new file mode 100644 index 0000000000..0c8b63f5d9 --- /dev/null +++ b/common-compose/src/test/java/com/itsaky/androidide/common/compose/IdeColorSchemeTest.kt @@ -0,0 +1,123 @@ +package com.itsaky.androidide.common.compose + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import com.google.android.material.R as MaterialR + +/** + * The attribute-to-role mapping, tested with a fake resolver rather than a real themed + * [android.content.Context]. + * + * The interesting behaviour is entirely in the mapping and the per-role fallback, so making resolution + * a parameter buys full coverage with no Robolectric and no theme fixtures. + */ +class IdeColorSchemeTest { + private val red = Color(0xFFFF0000) + private val green = Color(0xFF00FF00) + + /** + * Every role [ideColorScheme] claims to map, paired with its name for readable failures. + * + * [ColorScheme] has no structural `equals`, so whole-scheme comparison would compare identity and + * pass vacuously. Listing the roles also makes "did the mapping forget one?" a real assertion. + */ + private fun mappedRoles(scheme: ColorScheme): List> = + listOf( + "primary" to scheme.primary, + "onPrimary" to scheme.onPrimary, + "primaryContainer" to scheme.primaryContainer, + "onPrimaryContainer" to scheme.onPrimaryContainer, + "secondary" to scheme.secondary, + "onSecondary" to scheme.onSecondary, + "secondaryContainer" to scheme.secondaryContainer, + "onSecondaryContainer" to scheme.onSecondaryContainer, + "tertiary" to scheme.tertiary, + "onTertiary" to scheme.onTertiary, + "tertiaryContainer" to scheme.tertiaryContainer, + "onTertiaryContainer" to scheme.onTertiaryContainer, + "background" to scheme.background, + "onBackground" to scheme.onBackground, + "surface" to scheme.surface, + "onSurface" to scheme.onSurface, + "surfaceVariant" to scheme.surfaceVariant, + "onSurfaceVariant" to scheme.onSurfaceVariant, + "outline" to scheme.outline, + "outlineVariant" to scheme.outlineVariant, + "error" to scheme.error, + "onError" to scheme.onError, + "errorContainer" to scheme.errorContainer, + "onErrorContainer" to scheme.onErrorContainer, + ) + + @Test + fun `a resolved attribute wins over the baseline`() { + val scheme = ideColorScheme(dark = false) { attr -> red.takeIf { attr == MaterialR.attr.colorPrimary } } + + assertEquals(red, scheme.primary) + } + + @Test + fun `an undefined attribute falls back to the light baseline`() { + val scheme = ideColorScheme(dark = false) { null } + + assertEquals(mappedRoles(lightColorScheme()), mappedRoles(scheme)) + } + + @Test + fun `an undefined attribute falls back to the dark baseline`() { + val scheme = ideColorScheme(dark = true) { null } + + assertEquals(mappedRoles(darkColorScheme()), mappedRoles(scheme)) + } + + @Test + fun `roles fall back individually, so a partial theme still yields sensible colours`() { + // A theme defining only the surface pair, as a minimal overlay might. + val scheme = + ideColorScheme(dark = false) { attr -> + when (attr) { + MaterialR.attr.colorSurface -> red + MaterialR.attr.colorOnSurface -> green + else -> null + } + } + + assertEquals(red, scheme.surface) + assertEquals(green, scheme.onSurface) + // Everything else keeps the baseline rather than going transparent or black. + assertEquals(lightColorScheme().primary, scheme.primary) + assertEquals(lightColorScheme().error, scheme.error) + } + + @Test + fun `background reads the platform attribute, not a Material one`() { + // colorBackground has no Material equivalent; mapping it to one would silently lose the theme's + // window background. + val scheme = ideColorScheme(dark = false) { attr -> red.takeIf { attr == android.R.attr.colorBackground } } + + assertEquals(red, scheme.background) + } + + @Test + fun `every role the mapping claims to cover is actually resolved`() { + // Resolving everything to one colour proves no listed role was left out of the copy() call: an + // unmapped role would still hold its baseline value. + val scheme = ideColorScheme(dark = false) { red } + + val unmapped = mappedRoles(scheme).filter { (_, color) -> color != red } + assertTrue("roles not read from the theme: ${unmapped.map { it.first }}", unmapped.isEmpty()) + } + + @Test + fun `the dark baseline differs from the light one, so the flag is not ignored`() { + val light = ideColorScheme(dark = false) { null } + val dark = ideColorScheme(dark = true) { null } + + assertTrue(mappedRoles(light) != mappedRoles(dark)) + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 29fb8afcd8..7ce1b50938 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -105,6 +105,7 @@ include( ":app", ":build-info", ":common", + ":common-compose", ":common-ui", ":editor", ":editor-api", From 46cf26143f59b0a2c03f2fa72bd12130e9889f49 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Mon, 10 Aug 2026 14:17:19 +0000 Subject: [PATCH 2/2] ADFA-4826: Move profiler and floating-window onto the shared theming Both modules carried their own near-identical copy of the IDE colour derivation. They now delegate to common-compose, so there is one place where the IDE's Compose colours are defined. --- floating-window/build.gradle.kts | 1 + .../androidide/floating/ui/FloatingTheme.kt | 55 +++----------- profiler/build.gradle.kts | 1 + .../cotg/profiler/ui/theme/ProfilerTheme.kt | 73 +++---------------- 4 files changed, 20 insertions(+), 110 deletions(-) diff --git a/floating-window/build.gradle.kts b/floating-window/build.gradle.kts index cfbbbe8a0b..bb638bc857 100644 --- a/floating-window/build.gradle.kts +++ b/floating-window/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(libs.common.kotlin.coroutines.android) implementation(libs.google.material) + implementation(projects.commonCompose) implementation(projects.editorApi) implementation(projects.common) implementation(projects.resources) diff --git a/floating-window/src/main/java/com/itsaky/androidide/floating/ui/FloatingTheme.kt b/floating-window/src/main/java/com/itsaky/androidide/floating/ui/FloatingTheme.kt index c3401c9da3..6061716a0b 100644 --- a/floating-window/src/main/java/com/itsaky/androidide/floating/ui/FloatingTheme.kt +++ b/floating-window/src/main/java/com/itsaky/androidide/floating/ui/FloatingTheme.kt @@ -2,28 +2,17 @@ package com.itsaky.androidide.floating.ui -import android.content.Context -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Typography -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight -import com.google.android.material.color.MaterialColors -import com.google.android.material.R as MatR +import com.itsaky.androidide.common.compose.IdeTheme import com.itsaky.androidide.resources.R as ResR -private const val UNRESOLVED_COLOR = Int.MIN_VALUE - private val AtkinsonHyperlegible: FontFamily = FontFamily( Font(ResR.font.atkinson_hyperlegible_regular, FontWeight.Normal), @@ -33,22 +22,22 @@ private val AtkinsonHyperlegible: FontFamily = ) /** - * Wraps floating-window content in a [MaterialTheme] whose colors are read live from the IDE's XML - * `Theme.AndroidIDE` (via the supplied window context) and whose type uses the IDE's Atkinson - * Hyperlegible face. This keeps overlay windows visually identical to the docked editor, including - * light/dark. + * Wraps floating-window content in the shared [IdeTheme] -- colors read live from the IDE's XML + * `Theme.AndroidIDE` via the window context -- with type overridden to the IDE's Atkinson Hyperlegible + * face. This keeps overlay windows visually identical to the docked editor, including light/dark. + * + * Only the typography is local to this module; the color mapping is shared so every Compose surface + * resolves theme attributes the same way. */ @Composable fun FloatingTheme(content: @Composable () -> Unit) { - val context = LocalContext.current - val dark = isSystemInDarkTheme() - val colorScheme = remember(context, dark) { context.toComposeColorScheme(dark) } val typography = remember { brandedTypography() } - MaterialTheme(colorScheme = colorScheme, typography = typography, content = content) + IdeTheme(typography = typography, content = content) } private fun brandedTypography(): Typography { val base = Typography() + fun TextStyle.branded(): TextStyle = copy(fontFamily = AtkinsonHyperlegible) return base.copy( titleMedium = base.titleMedium.branded(), @@ -59,29 +48,3 @@ private fun brandedTypography(): Typography { labelSmall = base.labelSmall.branded(), ) } - -private fun Context.toComposeColorScheme(dark: Boolean): ColorScheme { - val base = if (dark) darkColorScheme() else lightColorScheme() - - fun color(attr: Int, fallback: Color): Color { - val resolved = MaterialColors.getColor(this, attr, UNRESOLVED_COLOR) - return if (resolved == UNRESOLVED_COLOR) fallback else Color(resolved) - } - - return base.copy( - primary = color(MatR.attr.colorPrimary, base.primary), - onPrimary = color(MatR.attr.colorOnPrimary, base.onPrimary), - primaryContainer = color(MatR.attr.colorPrimaryContainer, base.primaryContainer), - onPrimaryContainer = color(MatR.attr.colorOnPrimaryContainer, base.onPrimaryContainer), - secondary = color(MatR.attr.colorSecondary, base.secondary), - onSecondary = color(MatR.attr.colorOnSecondary, base.onSecondary), - surface = color(MatR.attr.colorSurface, base.surface), - onSurface = color(MatR.attr.colorOnSurface, base.onSurface), - surfaceVariant = color(MatR.attr.colorSurfaceVariant, base.surfaceVariant), - onSurfaceVariant = color(MatR.attr.colorOnSurfaceVariant, base.onSurfaceVariant), - outline = color(MatR.attr.colorOutline, base.outline), - error = color(MatR.attr.colorError, base.error), - onError = color(MatR.attr.colorOnError, base.onError), - background = color(android.R.attr.colorBackground, base.background), - ) -} diff --git a/profiler/build.gradle.kts b/profiler/build.gradle.kts index 0e02590550..bb061392d0 100644 --- a/profiler/build.gradle.kts +++ b/profiler/build.gradle.kts @@ -32,6 +32,7 @@ protobuf { dependencies { api(projects.actions) + implementation(projects.commonCompose) implementation(projects.logger) implementation(projects.subprojects.privilegedServices) implementation(projects.subprojects.flamegraph) diff --git a/profiler/src/main/java/org/appdevforall/cotg/profiler/ui/theme/ProfilerTheme.kt b/profiler/src/main/java/org/appdevforall/cotg/profiler/ui/theme/ProfilerTheme.kt index 7760600caa..32c74150f4 100644 --- a/profiler/src/main/java/org/appdevforall/cotg/profiler/ui/theme/ProfilerTheme.kt +++ b/profiler/src/main/java/org/appdevforall/cotg/profiler/ui/theme/ProfilerTheme.kt @@ -1,68 +1,13 @@ package org.appdevforall.cotg.profiler.ui.theme -import android.content.Context -import android.util.TypedValue -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat -import com.google.android.material.R as MaterialR - +import com.itsaky.androidide.common.compose.IdeTheme + +/** + * Profiler content themed from the IDE's XML theme. + * + * A thin alias for [IdeTheme]: the attribute-to-role mapping this used to carry is shared, so every + * Compose surface in the app resolves colours the same way. + */ @Composable -fun ProfilerTheme(content: @Composable () -> Unit) { - val context = LocalContext.current - val darkTheme = isSystemInDarkTheme() - val colorScheme = - remember(context, darkTheme) { - context.toMaterial3ColorScheme(darkTheme) - } - MaterialTheme(colorScheme = colorScheme, content = content) -} - -private fun Context.toMaterial3ColorScheme(darkTheme: Boolean): ColorScheme { - val base = if (darkTheme) darkColorScheme() else lightColorScheme() - return base.copy( - primary = resolveColor(MaterialR.attr.colorPrimary, base.primary), - onPrimary = resolveColor(MaterialR.attr.colorOnPrimary, base.onPrimary), - primaryContainer = resolveColor(MaterialR.attr.colorPrimaryContainer, base.primaryContainer), - onPrimaryContainer = resolveColor(MaterialR.attr.colorOnPrimaryContainer, base.onPrimaryContainer), - secondary = resolveColor(MaterialR.attr.colorSecondary, base.secondary), - onSecondary = resolveColor(MaterialR.attr.colorOnSecondary, base.onSecondary), - secondaryContainer = resolveColor(MaterialR.attr.colorSecondaryContainer, base.secondaryContainer), - onSecondaryContainer = resolveColor(MaterialR.attr.colorOnSecondaryContainer, base.onSecondaryContainer), - tertiary = resolveColor(MaterialR.attr.colorTertiary, base.tertiary), - onTertiary = resolveColor(MaterialR.attr.colorOnTertiary, base.onTertiary), - background = resolveColor(android.R.attr.colorBackground, base.background), - onBackground = resolveColor(MaterialR.attr.colorOnBackground, base.onBackground), - surface = resolveColor(MaterialR.attr.colorSurface, base.surface), - onSurface = resolveColor(MaterialR.attr.colorOnSurface, base.onSurface), - surfaceVariant = resolveColor(MaterialR.attr.colorSurfaceVariant, base.surfaceVariant), - onSurfaceVariant = resolveColor(MaterialR.attr.colorOnSurfaceVariant, base.onSurfaceVariant), - outline = resolveColor(MaterialR.attr.colorOutline, base.outline), - error = resolveColor(MaterialR.attr.colorError, base.error), - onError = resolveColor(MaterialR.attr.colorOnError, base.onError), - ) -} - -private fun Context.resolveColor( - attr: Int, - fallback: Color, -): Color { - val value = TypedValue() - if (!theme.resolveAttribute(attr, value, true)) return fallback - val colorInt = - if (value.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) { - value.data - } else if (value.resourceId != 0) { - ContextCompat.getColor(this, value.resourceId) - } else { - return fallback - } - return Color(colorInt) -} +fun ProfilerTheme(content: @Composable () -> Unit) = IdeTheme(content = content)