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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ fun appRouteTitle(route: NativeRoute): String = when (route) {
is AppRoute.ComponentMatrix -> "Component matrix"
is AppRoute.InteropRepro -> "iOS interop repro"
is AppRoute.InteropChurn -> "Interop churn"
is AppRoute.LiquidGlassDemo -> "Liquid glass"
is AppRoute.ChromeDemo -> "Chrome demo"
is AppRoute.ToolbarStyles -> "Toolbar styles"
is AppRoute.ImmersiveDemo -> "Immersive"
Expand Down Expand Up @@ -80,6 +81,7 @@ fun appNavGraph(navigator: NativeNavigator): NativeNavGraph = nativeNavGraph {
onOpenComponentMatrix = { navigator.push(AppRoute.ComponentMatrix) },
onOpenInteropRepro = { navigator.push(AppRoute.InteropRepro) },
onOpenInteropChurn = { navigator.push(AppRoute.InteropChurn) },
onOpenLiquidGlass = { navigator.push(AppRoute.LiquidGlassDemo) },
onOpenChromeDemo = { navigator.push(AppRoute.ChromeDemo) },
onOpenToolbarStyles = { navigator.push(AppRoute.ToolbarStyles) },
)
Expand All @@ -92,6 +94,7 @@ fun appNavGraph(navigator: NativeNavigator): NativeNavGraph = nativeNavGraph {
screen<AppRoute.ImmersiveDemo> { ImmersiveDemoScreen(onBack = { navigator.pop() }) }
screen<AppRoute.InteropRepro> { InteropReproScreen() }
screen<AppRoute.InteropChurn> { InteropChurnScreen() }
screen<AppRoute.LiquidGlassDemo> { LiquidGlassDemoScreen() }
screen<AppRoute.CatalogRoot> {
ShowcaseHomeScreen(onOpenCategory = { key -> navigator.push(AppRoute.Showcase(key)) })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ sealed interface AppRoute : NativeRoute {
override val id = "debug/interop-repro"
}

/** Debug screen: Liquid Glass buttons refracting live content (device-verified iOS 26 behavior). */
data object LiquidGlassDemo : AppRoute {
override val id = "debug/liquid-glass"
}

/** Debug screen: the interop-churn regression harness (AnimatedVisibility collapse of native leaves). */
data object InteropChurn : AppRoute {
override val id = "debug/interop-churn"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ fun SettingsScreen(
onOpenComponentMatrix: () -> Unit = {},
onOpenInteropRepro: () -> Unit = {},
onOpenInteropChurn: () -> Unit = {},
onOpenLiquidGlass: () -> Unit = {},
onOpenChromeDemo: () -> Unit = {},
onOpenToolbarStyles: () -> Unit = {},
) {
Expand Down Expand Up @@ -71,6 +72,13 @@ fun SettingsScreen(
onClick = onOpenInteropRepro,
)
},
{
NativeListItem(
"Liquid glass buttons",
supporting = "Glass and prominent-glass NativeButtons refracting auto-scrolling content (iOS 26)",
onClick = onOpenLiquidGlass,
)
},
{
NativeListItem(
"Interop churn test",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package io.github.apdelrahman1911.nativecomposekit.app

import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import io.github.apdelrahman1911.nativecomposekit.components.NativeButton
import io.github.apdelrahman1911.nativecomposekit.components.NativeText
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonIosBackground
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonIosOptions
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonVariant
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeTextStyle
import kotlinx.coroutines.delay

/**
* Debug screen: **Liquid Glass buttons over live content**. Colorful stripes auto-scroll beneath two
* floating [NativeButton]s using `ios = NativeButtonIosOptions(background = Glass / ProminentGlass)` —
* on an iOS 26 device/simulator the capsules visibly refract the stripes moving under them and morph
* on press (tap them; the toggle row is the pause switch). On Android and pre-26 iOS the same code
* renders the buttons' regular variant styling — which is exactly the documented fallback contract.
*/
@Composable
fun LiquidGlassDemoScreen(autoRun: Boolean = true) {
val stripes = listOf(
Color(0xFFE57373), Color(0xFFFFB74D), Color(0xFFFFF176), Color(0xFFAED581),
Color(0xFF4DB6AC), Color(0xFF4FC3F7), Color(0xFF7986CB), Color(0xFFBA68C8),
)
var scrolling by remember { mutableStateOf(autoRun) }
val listState = rememberLazyListState()
LaunchedEffect(scrolling) {
while (scrolling) {
listState.animateScrollToItem(24)
delay(400)
listState.animateScrollToItem(0)
delay(400)
}
}
Box(Modifier.fillMaxSize()) {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 12.dp),
) {
items(count = 32) { i ->
Box(
Modifier.fillMaxWidth().height(72.dp).padding(horizontal = 12.dp, vertical = 4.dp)
.background(stripes[i % stripes.size]),
contentAlignment = Alignment.Center,
) {
NativeText("Stripe ${i + 1}", style = NativeTextStyle.Label)
}
}
}
Column(
// Clear the floating tab bar: the content inset local carries its height on iOS.
modifier = Modifier.align(Alignment.BottomCenter)
.padding(bottom = 24.dp + LocalNativeContentBottomInset.current),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
NativeButton(
text = if (scrolling) "Glass — tap to pause" else "Glass — tap to scroll",
onClick = { scrolling = !scrolling },
variant = NativeButtonVariant.Secondary,
ios = NativeButtonIosOptions(background = NativeButtonIosBackground.Glass),
)
NativeButton(
text = "Prominent glass",
onClick = { },
variant = NativeButtonVariant.Primary,
ios = NativeButtonIosOptions(background = NativeButtonIosBackground.ProminentGlass),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import io.github.apdelrahman1911.nativecomposekit.components.NativeSplitButton
import io.github.apdelrahman1911.nativecomposekit.components.feedback.LocalNativeFeedbackController
import io.github.apdelrahman1911.nativecomposekit.components.feedback.NativeAlertAction
import io.github.apdelrahman1911.nativecomposekit.components.feedback.NativeAlertActionRole
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonIosBackground
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonIosOptions
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonShape
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonSize
import io.github.apdelrahman1911.nativecomposekit.components.model.NativeButtonVariant
Expand Down Expand Up @@ -70,6 +72,24 @@ fun ButtonsShowcase() = ShowcaseScreen(
NativeButton("Destructive", { feedback.toast("Destructive") }, variant = NativeButtonVariant.Destructive)
}

ExampleLabel("Liquid Glass (iOS 26)")
// iOS: a real UIGlassEffect capsule that refracts the content beneath (Automatic below
// iOS 26). Android: the ios options are a documented no-op — these render as the variant.
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
NativeButton(
"Glass",
{ feedback.toast("Glass") },
variant = NativeButtonVariant.Secondary,
ios = NativeButtonIosOptions(background = NativeButtonIosBackground.Glass),
)
NativeButton(
"Prominent glass",
{ feedback.toast("Prominent glass") },
variant = NativeButtonVariant.Primary,
ios = NativeButtonIosOptions(background = NativeButtonIosBackground.ProminentGlass),
)
}

ExampleLabel("Sizes")
Row(
modifier = Modifier.fillMaxWidth(),
Expand Down
27 changes: 27 additions & 0 deletions docs/components/buttons.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ A text button with optional leading and trailing icons, an optional loading stat
- The control is icon-only — use `NativeIconButton`.
- You need one main action plus a related menu — use `NativeSplitButton` (a menu-bearing button's tap only opens the menu; `onClick` is not called).

#### Liquid Glass (`ios = NativeButtonIosOptions(background = …)`)

iOS 26 introduced **Liquid Glass** — a system material that refracts the content beneath a control and
morphs on press. The button family opts in per call site through `NativeButtonIosOptions`; it is an
iOS-only appearance knob layered on the variant, never a shared abstraction:

| `background` | iOS 26+ | iOS < 26 | Android |
|---|---|---|---|
| `Automatic` (default) | the variant's flat rendering | same | the variant's Material rendering |
| `Glass` | clear adaptive glass capsule; content uses the system label color | falls back to `Automatic` | no-op (`Automatic`) |
| `ProminentGlass` | glass tinted with the variant's **container** color; content keeps the variant's content color | falls back to `Automatic` | no-op (`Automatic`) |

Implementation notes and constraints:

- The glass is a **real `UIGlassEffect`** (instantiated at runtime — the class postdates the K/N
bindings), not a blur imitation. Reduce Transparency is handled by the system material itself.
- A glass button is composited **above** the Compose canvas (overlay interop placement) so the effect
can sample the content it floats over; the overlay scroll-drift trade from
[interop-notes §1](../interop-notes.md) applies while actively scrolling.
- Per the HIG, glass belongs to elements **floating above content** — action buttons over a scroll,
control strips — not to every inline form button.
- `NativeSplitButton` renders glass as `Automatic` in this release: a two-segment control needs one
*shared* glass body (`UIGlassEffectContainer` territory), and per-half capsules read wrong. The
option is accepted so call sites are forward-compatible.
- Live demo: sample app → Settings → Developer → **"Liquid glass buttons"** (glass capsules refracting
auto-scrolling stripes).

**Parameters**

| Parameter | Type | Default | Description |
Expand Down
4 changes: 4 additions & 0 deletions docs/design-system-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ Shared props where behavior aligns; a typed `ios`/`android` option object where
recorded on the component's reference page. `NativeText` is the one sanctioned Compose-fallback (Compose `Text` on
material surfaces, native `UILabel` on solid) — and that exception is documented, not silent.

- **Liquid Glass buttons** are the pattern in action: `ios = NativeButtonIosOptions(background = Glass)`
is an explicitly iOS-scoped knob — real `UIGlassEffect` on iOS 26, documented `Automatic` fallback
below iOS 26, documented no-op on Android. No fake cross-platform "glass" is drawn anywhere.

## What is intentionally NOT in the kit
- **`NativeImage` / image loading** → app-level via Coil 3 (rules 3 + 4).
- **Wheel / `UIPickerView` picker** → covered by `NativeSegmentedControl` / `NativeMenu` / `NativeRadioGroup` /
Expand Down
3 changes: 3 additions & 0 deletions docs/interop-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ fix (re-queue on skipped presents, no dedup of undelivered rects) is engine-leve

## Practical guidance

- Liquid Glass buttons (`ios = NativeButtonIosOptions(background = Glass/ProminentGlass)`) are
overlay-placed by construction — the glass must sit above the canvas to refract it — so the overlay
drift trade in §1 applies to them while scrolling.
- Don't put many menu-bearing buttons in a long scroll if the post-open drift would be noticeable;
prefer a non-menu action or present the menu from a fixed bar.
- For sheet-style content use `NativeSheet` (a real `UISheetPresentationController`) rather than
Expand Down
Loading
Loading