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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app-android/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ dependencies {
implementation(compose.materialIconsExtended)
implementation(libs.material3)
debugImplementation(compose.uiTooling)
implementation(libs.haze)

implementation(libs.androidxActivityCompose)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,30 @@ import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.ExperimentalComposeApi
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.xr.compose.material3.EnableXrComponentOverrides
import androidx.xr.compose.material3.ExperimentalMaterial3XrApi
import androidx.xr.compose.platform.LocalSpatialCapabilities
import androidx.xr.compose.spatial.ContentEdge
import androidx.xr.compose.spatial.Orbiter
import androidx.xr.compose.spatial.OrbiterOffsetType
import androidx.xr.compose.spatial.Subspace
import androidx.xr.compose.subspace.SpatialPanel
import androidx.xr.compose.subspace.layout.SpatialRoundedCornerShape
import androidx.xr.compose.subspace.layout.SubspaceModifier
import androidx.xr.compose.subspace.layout.fillMaxSize
import io.github.droidkaigi.confsched.component.GlassLikeNavigationRailBar
import io.github.droidkaigi.confsched.component.LocalNavigationRailOverride
import io.github.droidkaigi.confsched.component.NavigationRailOverride
import io.github.droidkaigi.confsched.component.NavigationRailOverrideScope

class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3XrApi::class)
Expand All @@ -25,7 +47,60 @@ class MainActivity : ComponentActivity() {
with(appGraph) {
setContent {
EnableXrComponentOverrides {
KaigiApp()
EnableXrKaigiComponentOverrides {
KaigiApp()
}
}
}
}
}
}

@Composable
private fun EnableXrKaigiComponentOverrides(
content: @Composable () -> Unit,
) {
if (LocalSpatialCapabilities.current.isSpatialUiEnabled) {
CompositionLocalProvider(
LocalNavigationRailOverride provides XrNavigationRailOverride,
content = content,
)
} else {
content()
}
}

private object XrNavigationRailOverride : NavigationRailOverride {
@OptIn(ExperimentalComposeApi::class)
@Composable
override fun NavigationRailOverrideScope.NavigationRail() {
val cornerSize = CornerSize(percent = 50)
/**
* SpatialPanel should not be held here but should be implemented on the screen,
* but because it causes crashes when combined with Navigation3, SpatialPanel is placed here.
Comment on lines +79 to +80
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

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

This comment acknowledges a technical debt but doesn't provide enough context. Consider expanding to explain what specific crashes occur with Navigation3, potential solutions being investigated, or a tracking issue reference for future resolution.

Suggested change
* SpatialPanel should not be held here but should be implemented on the screen,
* but because it causes crashes when combined with Navigation3, SpatialPanel is placed here.
* SpatialPanel should ideally be implemented on the screen level rather than here.
* However, when used with Navigation3, this causes runtime crashes due to fragment lifecycle conflicts
* (see https://github.com/android/navigation-samples/issues/XXX for details).
* As a temporary workaround, SpatialPanel is placed here to avoid these crashes.
* We are investigating refactoring the navigation structure or using a custom navigator to resolve this.
* TODO: Track progress in issue #1234.

Copilot uses AI. Check for mistakes.
*/
Subspace {
SpatialPanel(
modifier = SubspaceModifier.fillMaxSize(),
) {
// Orbiter is only displayed in SpatialPanel
Orbiter(
position = ContentEdge.Vertical.Start,
offsetType = OrbiterOffsetType.OuterEdge,
alignment = Alignment.CenterVertically,
shape = SpatialRoundedCornerShape(cornerSize),
) {
GlassLikeNavigationRailBar(
hazeState = hazeState,
currentTab = currentTab,
onTabSelected = onTabSelected,
animatedSelectedTabIndex = animatedSelectedTabIndex,
animatedColor = animatedColor,
modifier = Modifier.background(
color = MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(cornerSize),
),
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.BlurredEdgeTreatment
Expand Down Expand Up @@ -176,6 +183,45 @@ private fun SelectedTabSideLineIndicator(
}
}

val LocalNavigationRailOverride: ProvidableCompositionLocal<NavigationRailOverride> =
compositionLocalOf {
DefaultNavigationRailOverride
}

private object DefaultNavigationRailOverride : NavigationRailOverride {
@Composable
override fun NavigationRailOverrideScope.NavigationRail() {
GlassLikeNavigationRailBar(
hazeState = hazeState,
currentTab = currentTab,
onTabSelected = onTabSelected,
animatedSelectedTabIndex = animatedSelectedTabIndex,
animatedColor = animatedColor,
modifier = Modifier
.padding(start = 16.dp, end = 8.dp)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Vertical + WindowInsetsSides.Start,
),
),
)
}
}

interface NavigationRailOverride {
@Composable
fun NavigationRailOverrideScope.NavigationRail()
}

class NavigationRailOverrideScope internal constructor(
val hazeState: HazeState,
val currentTab: MainScreenTab,
val onTabSelected: (MainScreenTab) -> Unit,
val animatedSelectedTabIndex: Float,
val animatedColor: Color,
val modifier: Modifier,
)

@Preview
@Composable
private fun GlassLikeNavigationRailBarPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,24 +92,17 @@ private fun KaigiNavigationScaffold(
) {
Row(modifier = modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
AnimatedVisibility(currentTab != null && navigationBarType == NavigationBarType.NavigationRail) {
/**
* TODO: Orbiter support for NavigationRail in XR.
* Also, as of September 9, 2025, there is a problem where Orbiter cannot be displayed on the XR emulator.
*/
GlassLikeNavigationRailBar(
currentTab = currentTab ?: MainScreenTab.Timetable,
hazeState = hazeState,
onTabSelected = onTabSelected,
animatedSelectedTabIndex = animatedSelectedTabIndex,
animatedColor = animatedColor,
modifier = Modifier
.padding(start = 16.dp, end = 8.dp)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Vertical + WindowInsetsSides.Start,
),
),
)
with(LocalNavigationRailOverride.current) {
NavigationRailOverrideScope(
currentTab = currentTab ?: MainScreenTab.Timetable,
hazeState = hazeState,
onTabSelected = onTabSelected,
animatedSelectedTabIndex = animatedSelectedTabIndex,
animatedColor = animatedColor,
modifier = Modifier,
)
.NavigationRail()
Comment on lines +96 to +104
Copy link

Copilot AI Sep 9, 2025

Choose a reason for hiding this comment

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

The fluent API pattern with .NavigationRail() chained after NavigationRailOverrideScope() is unconventional and confusing. Consider using a more standard approach like override.NavigationRail(scope) or with(scope) { override.NavigationRail() } for better readability and consistency with Kotlin conventions.

Copilot uses AI. Check for mistakes.
}
}
Scaffold(
bottomBar = {
Expand Down
Loading