diff --git a/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt b/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt index 9d8dbf869c..27c30c27f3 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/HorizonActivity.kt @@ -19,15 +19,17 @@ import android.content.Intent import android.content.pm.ShortcutManager import android.content.res.Configuration import android.os.Bundle +import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalContext -import androidx.core.content.ContextCompat +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDeepLinkRequest @@ -46,9 +48,7 @@ import com.instructure.pandautils.utils.ColorKeeper import com.instructure.pandautils.utils.Const import com.instructure.pandautils.utils.ThemePrefs import com.instructure.pandautils.utils.Utils -import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.WebViewAuthenticator -import com.instructure.pandautils.utils.getActivityOrNull import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject @@ -67,12 +67,14 @@ class HorizonActivity : BaseCanvasActivity() { if (ThemePrefs.appTheme != AppTheme.LIGHT.ordinal) { setLightTheme() // Force the light theme for Horizon experience to avoid any glitches. } + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb()), + navigationBarStyle = SystemBarStyle.light(Color.Transparent.toArgb(), Color.Transparent.toArgb()) + ) setContent { navController = rememberNavController() - val activity = LocalContext.current.getActivityOrNull() - if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) HorizonTheme { HorizonNavigation(navController) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/account/AccountScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/account/AccountScreen.kt index 7bbb49ab1c..b4813820a3 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/account/AccountScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/account/AccountScreen.kt @@ -18,12 +18,15 @@ package com.instructure.horizon.features.account import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -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.add +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed @@ -48,6 +51,7 @@ import com.instructure.horizon.horizonui.foundation.HorizonTypography import com.instructure.horizon.horizonui.foundation.SpaceSize import com.instructure.horizon.horizonui.molecules.HorizonDivider import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars import com.instructure.pandautils.utils.LocaleUtils import com.instructure.pandautils.utils.getActivityOrNull @@ -75,14 +79,18 @@ fun AccountScreen( } LoadingStateWrapper(state.screenState) { - AccountContentScreen(state, navController, state.performLogout, state.switchExperience) + HorizonEdgeToEdgeSystemBars( + statusBarColor = HorizonColors.Surface.pagePrimary(), + ) { + AccountContentScreen(state, navController, state.performLogout, state.switchExperience) + } } } @Composable private fun AccountContentScreen(state: AccountUiState, navController: NavController, onLogout: () -> Unit, switchExperience: () -> Unit) { LazyColumn( - contentPadding = PaddingValues(24.dp) + contentPadding = WindowInsets.safeDrawing.add(WindowInsets(24.dp, 24.dp, 24.dp, 24.dp)).asPaddingValues() ) { item { Column { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/AiAssistantScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/AiAssistantScreen.kt index a02b39286f..534e157109 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/AiAssistantScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/aiassistant/AiAssistantScreen.kt @@ -17,6 +17,7 @@ package com.instructure.horizon.features.aiassistant import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -28,6 +29,7 @@ import androidx.navigation.compose.rememberNavController import com.instructure.horizon.R import com.instructure.horizon.features.aiassistant.navigation.AiAssistNavigation import com.instructure.horizon.horizonui.foundation.HorizonColors +import com.instructure.horizon.util.bottomSafeDrawing @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -37,10 +39,11 @@ fun AiAssistantScreen( val navController = rememberNavController() val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( - containerColor = colorResource(R.color.ai_gradient_start), + containerColor = colorResource(R.color.ai_gradient_end), onDismissRequest = { onDismiss() }, dragHandle = null, sheetState = bottomSheetState, + contentWindowInsets = { WindowInsets.bottomSafeDrawing } ) { Box( modifier = Modifier diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt index 7934525554..3ce4649d68 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/dashboard/DashboardScreen.kt @@ -31,10 +31,12 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll @@ -56,8 +58,6 @@ 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.draw.shadow -import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -91,7 +91,10 @@ import com.instructure.horizon.horizonui.molecules.IconButtonColor import com.instructure.horizon.horizonui.organisms.AnimatedHorizontalPager import com.instructure.horizon.horizonui.organisms.CollapsableHeaderScreen import com.instructure.horizon.navigation.MainNavigationRoute -import com.instructure.pandautils.compose.modifiers.conditional +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.bottomNavigationScreenInsets +import com.instructure.horizon.util.horizontalSafeDrawing +import com.instructure.horizon.util.zeroScreenInsets import kotlinx.coroutines.flow.MutableStateFlow @Composable @@ -135,79 +138,79 @@ fun DashboardScreen(uiState: DashboardUiState, mainNavController: NavHostControl } } - Scaffold( - containerColor = HorizonColors.Surface.pagePrimary(), - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { paddingValues -> - val pullToRefreshState = rememberPullToRefreshState() - val isRefreshing = refreshState.any { it } - PullToRefreshBox( - isRefreshing = isRefreshing, - onRefresh = { shouldRefresh = true }, - state = pullToRefreshState, - indicator = { - Indicator( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(top = 56.dp), - isRefreshing = isRefreshing, - containerColor = HorizonColors.Surface.pageSecondary(), - color = HorizonColors.Surface.institution(), - state = pullToRefreshState - ) - } - ){ - val scrollState = rememberScrollState() - CollapsableHeaderScreen( - modifier = Modifier.padding(paddingValues), - headerContent = { - Column( - modifier = Modifier.conditional(scrollState.canScrollBackward) { - shadow( - elevation = HorizonElevation.level3, - spotColor = Color.Transparent, + HorizonEdgeToEdgeSystemBars { + Scaffold( + contentWindowInsets = WindowInsets.zeroScreenInsets, + containerColor = HorizonColors.Surface.pagePrimary(), + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { paddingValues -> + val pullToRefreshState = rememberPullToRefreshState() + val isRefreshing = refreshState.any { it } + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { shouldRefresh = true }, + state = pullToRefreshState, + indicator = { + Indicator( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 56.dp), + isRefreshing = isRefreshing, + containerColor = HorizonColors.Surface.pageSecondary(), + color = HorizonColors.Surface.institution(), + state = pullToRefreshState + ) + } + ) { + val scrollState = rememberScrollState() + CollapsableHeaderScreen( + modifier = Modifier.padding(paddingValues), + headerContent = { + Column( + modifier = Modifier + .windowInsetsPadding(WindowInsets.bottomNavigationScreenInsets) + ) { + HomeScreenTopBar( + uiState, + mainNavController, + modifier = Modifier + .height(56.dp) + .padding(bottom = 12.dp) ) } - ) { - HomeScreenTopBar( - uiState, - mainNavController, + }, + bodyContent = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .height(56.dp) - .padding(bottom = 12.dp) - ) - } - }, - bodyContent = { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .verticalScroll(scrollState) - ) { - HorizonSpace(SpaceSize.SPACE_12) - DashboardAnnouncementBannerWidget( - mainNavController, - homeNavController, - shouldRefresh, - refreshStateFlow - ) - DashboardCourseSection( - mainNavController, - homeNavController, - shouldRefresh, - refreshStateFlow - ) - HorizonSpace(SpaceSize.SPACE_16) - NumericWidgetRow(shouldRefresh, refreshStateFlow, homeNavController) - DashboardSkillHighlightsWidget( - homeNavController, - shouldRefresh, - refreshStateFlow - ) - HorizonSpace(SpaceSize.SPACE_24) + .windowInsetsPadding(WindowInsets.horizontalSafeDrawing) + .verticalScroll(scrollState) + ) { + HorizonSpace(SpaceSize.SPACE_12) + DashboardAnnouncementBannerWidget( + mainNavController, + homeNavController, + shouldRefresh, + refreshStateFlow + ) + DashboardCourseSection( + mainNavController, + homeNavController, + shouldRefresh, + refreshStateFlow + ) + HorizonSpace(SpaceSize.SPACE_16) + NumericWidgetRow(shouldRefresh, refreshStateFlow, homeNavController) + DashboardSkillHighlightsWidget( + homeNavController, + shouldRefresh, + refreshStateFlow + ) + HorizonSpace(SpaceSize.SPACE_24) + } } - } - ) + ) + } } } } @@ -217,7 +220,8 @@ private fun HomeScreenTopBar(uiState: DashboardUiState, mainNavController: NavCo ) { Row( verticalAlignment = Alignment.Bottom, - modifier = modifier.padding(horizontal = 24.dp) + modifier = modifier + .padding(horizontal = 24.dp) ) { GlideImage( model = uiState.logoUrl, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeScreen.kt index 8c3e4be2a5..f0a51f7b25 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/home/HomeScreen.kt @@ -20,9 +20,12 @@ package com.instructure.horizon.features.home import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.NavigationBar import androidx.compose.material3.Scaffold @@ -36,7 +39,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.navigation.NavDestination @@ -53,8 +55,8 @@ import com.instructure.horizon.horizonui.molecules.IconButton import com.instructure.horizon.horizonui.molecules.IconButtonColor import com.instructure.horizon.horizonui.molecules.Spinner import com.instructure.horizon.horizonui.organisms.navelements.SelectableNavigationItem +import com.instructure.horizon.util.zeroScreenInsets import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.getActivityOrNull data class BottomNavItem( @@ -84,26 +86,30 @@ fun HomeScreen(parentNavController: NavHostController, viewModel: HomeViewModel) val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination val activity = LocalContext.current.getActivityOrNull() - if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) LaunchedEffect(key1 = uiState.theme) { val theme = uiState.theme if (theme != null && activity != null && !ThemePrefs.isThemeApplied) ThemePrefs.applyCanvasTheme(theme, activity) } - Scaffold(content = { padding -> - if (uiState.initialDataLoading) { - val spinnerColor = - if (ThemePrefs.isThemeApplied) HorizonColors.Surface.institution() else HorizonColors.Surface.inverseSecondary() - Spinner(modifier = Modifier.fillMaxSize(), color = spinnerColor) - } else { - if (uiState.showAiAssist) { - AiAssistantScreen({ uiState.updateShowAiAssist(false) }) + Scaffold( + contentWindowInsets = WindowInsets.zeroScreenInsets, + content = { padding -> + if (uiState.initialDataLoading) { + val spinnerColor = + if (ThemePrefs.isThemeApplied) HorizonColors.Surface.institution() else HorizonColors.Surface.inverseSecondary() + Spinner(modifier = Modifier.fillMaxSize(), color = spinnerColor) + } else { + if (uiState.showAiAssist) { + AiAssistantScreen({ uiState.updateShowAiAssist(false) }) + } + HomeNavigation(navController, parentNavController, Modifier.padding(padding)) } - HomeNavigation(navController, parentNavController, Modifier.padding(padding)) + }, + containerColor = HorizonColors.Surface.pagePrimary(), + bottomBar = { + BottomNavigationBar(navController, currentDestination, !uiState.initialDataLoading, { uiState.updateShowAiAssist(it) }) } - }, containerColor = HorizonColors.Surface.pagePrimary(), bottomBar = { - BottomNavigationBar(navController, currentDestination, !uiState.initialDataLoading, { uiState.updateShowAiAssist(it) }) - }) + ) } @Composable @@ -114,8 +120,15 @@ private fun BottomNavigationBar( updateShowAiAssist: (Boolean) -> Unit, modifier: Modifier = Modifier ) { - Surface(shadowElevation = HorizonElevation.level5) { - NavigationBar(containerColor = HorizonColors.Surface.pageSecondary(), modifier = modifier) { + Surface( + shadowElevation = HorizonElevation.level5, + color = HorizonColors.Surface.pageSecondary() + ) { + NavigationBar( + windowInsets = WindowInsets.zeroScreenInsets, + containerColor = HorizonColors.Surface.pageSecondary(), + modifier = modifier.windowInsetsPadding(WindowInsets.navigationBars) + ) { bottomNavItems.forEach { item -> val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true if (item.route == null) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt index 47fc1f97e3..43364ebf86 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/compose/HorizonInboxComposeScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -51,7 +52,6 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.instructure.canvasapi2.utils.ContextKeeper @@ -90,8 +90,8 @@ import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextAreaState import com.instructure.horizon.horizonui.organisms.inputs.textfield.TextField import com.instructure.horizon.horizonui.organisms.inputs.textfield.TextFieldInputSize import com.instructure.horizon.horizonui.organisms.inputs.textfield.TextFieldState -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.getActivityOrNull +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.topBarScreenInsets @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -109,50 +109,47 @@ fun HorizonInboxComposeScreen( } } } - val activity = LocalContext.current.getActivityOrNull() - LaunchedEffect(Unit) { - if (activity != null) { - ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pageSecondary)) - } - } - Scaffold( - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - containerColor = HorizonColors.Surface.pageSecondary(), - topBar = { - HorizonInboxComposeTopBar(state, navController) - } - ) { innerPadding -> - BackHandler { onExit(state, navController) } - - HorizonInboxAttachmentPicker( - showBottomSheet = state.showAttachmentPicker, - onDismissBottomSheet = { state.onShowAttachmentPickerChanged(false) }, - state = pickerState, - onFilesChanged = state.onAttachmentsChanged - ) + HorizonEdgeToEdgeSystemBars(null, null) { + Scaffold( + contentWindowInsets = WindowInsets.topBarScreenInsets, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + containerColor = HorizonColors.Surface.pageSecondary(), + topBar = { + HorizonInboxComposeTopBar(state, navController) + } + ) { innerPadding -> + BackHandler { onExit(state, navController) } + + HorizonInboxAttachmentPicker( + showBottomSheet = state.showAttachmentPicker, + onDismissBottomSheet = { state.onShowAttachmentPickerChanged(false) }, + state = pickerState, + onFilesChanged = state.onAttachmentsChanged + ) - if (state.showExitConfirmationDialog) { - Modal( - dialogState = ModalDialogState( - title = stringResource(R.string.exitConfirmationTitle), - message = stringResource(R.string.exitConfirmationMessage), - primaryButtonTitle = stringResource(R.string.exitConfirmationExitButtonLabel), - secondaryButtonTitle = stringResource(R.string.exitConfirmationCancelButtonLabel), - primaryButtonClick = { - state.updateShowExitConfirmationDialog(false) - navController.popBackStack() - }, - secondaryButtonClick = { state.updateShowExitConfirmationDialog(false) } + if (state.showExitConfirmationDialog) { + Modal( + dialogState = ModalDialogState( + title = stringResource(R.string.exitConfirmationTitle), + message = stringResource(R.string.exitConfirmationMessage), + primaryButtonTitle = stringResource(R.string.exitConfirmationExitButtonLabel), + secondaryButtonTitle = stringResource(R.string.exitConfirmationCancelButtonLabel), + primaryButtonClick = { + state.updateShowExitConfirmationDialog(false) + navController.popBackStack() + }, + secondaryButtonClick = { state.updateShowExitConfirmationDialog(false) } + ) ) + } + + HorizonInboxComposeContent( + state, + navController, + Modifier.padding(innerPadding) ) } - - HorizonInboxComposeContent( - state, - navController, - Modifier.padding(innerPadding) - ) } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt index b0af64c093..ac0fdef3aa 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/details/HorizonInboxDetailsScreen.kt @@ -23,9 +23,10 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -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.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -81,6 +82,9 @@ import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextArea import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextAreaState import com.instructure.horizon.horizonui.platform.LoadingState import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.topBarScreenInsets +import com.instructure.horizon.util.zeroScreenInsets import com.instructure.pandautils.compose.composables.ComposeCanvasWebViewWrapper import com.instructure.pandautils.compose.composables.ComposeEmbeddedWebViewCallbacks import com.instructure.pandautils.room.appdatabase.entities.FileDownloadProgressState @@ -97,41 +101,61 @@ fun HorizonInboxDetailsScreen( state: HorizonInboxDetailsUiState, navController: NavHostController ) { - Scaffold( - containerColor = HorizonColors.Surface.pagePrimary(), - topBar = { HorizonInboxDetailsHeader(state.title, state.titleIcon, state, navController) }, - ) { innerPadding -> - LoadingStateWrapper(state.loadingState, modifier = Modifier.padding(innerPadding)) { - BackHandler { onExit(state, navController) } - - state.replyState?.let { replyState -> - val viewModel: HorizonInboxAttachmentPickerViewModel = hiltViewModel() - val pickerState by viewModel.uiState.collectAsState() - HorizonInboxAttachmentPicker( - showBottomSheet = replyState.showAttachmentPicker, - onDismissBottomSheet = { replyState.onShowAttachmentPickerChanged(false) }, - state = pickerState, - onFilesChanged = replyState.onAttachmentsChanged + HorizonEdgeToEdgeSystemBars(null, null) { + Scaffold( + contentWindowInsets = WindowInsets.zeroScreenInsets, + containerColor = HorizonColors.Surface.pagePrimary(), + topBar = { + HorizonInboxDetailsHeader( + state.title, + state.titleIcon, + state, + navController ) + }, + ) { innerPadding -> + LoadingStateWrapper( + state.loadingState, + ) { + BackHandler { onExit(state, navController) } + + state.replyState?.let { replyState -> + val viewModel: HorizonInboxAttachmentPickerViewModel = hiltViewModel() + val pickerState by viewModel.uiState.collectAsState() + HorizonInboxAttachmentPicker( + showBottomSheet = replyState.showAttachmentPicker, + onDismissBottomSheet = { replyState.onShowAttachmentPickerChanged(false) }, + state = pickerState, + onFilesChanged = replyState.onAttachmentsChanged + ) - if (replyState.showExitConfirmationDialog) { - Modal( - dialogState = ModalDialogState( - title = stringResource(R.string.exitConfirmationTitle), - message = stringResource(R.string.exitConfirmationMessage), - primaryButtonTitle = stringResource(R.string.exitConfirmationExitButtonLabel), - secondaryButtonTitle = stringResource(R.string.exitConfirmationCancelButtonLabel), - primaryButtonClick = { - replyState.updateShowExitConfirmationDialog(false) - navController.popBackStack() - }, - secondaryButtonClick = { replyState.updateShowExitConfirmationDialog(false) } + if (replyState.showExitConfirmationDialog) { + Modal( + dialogState = ModalDialogState( + title = stringResource(R.string.exitConfirmationTitle), + message = stringResource(R.string.exitConfirmationMessage), + primaryButtonTitle = stringResource(R.string.exitConfirmationExitButtonLabel), + secondaryButtonTitle = stringResource(R.string.exitConfirmationCancelButtonLabel), + primaryButtonClick = { + replyState.updateShowExitConfirmationDialog(false) + navController.popBackStack() + }, + secondaryButtonClick = { + replyState.updateShowExitConfirmationDialog( + false + ) + } + ) ) - ) + } } - } - HorizonInboxDetailsContent(state) + HorizonInboxDetailsContent( + state, + Modifier + .padding(innerPadding) + ) + } } } } @@ -182,7 +206,7 @@ private fun HorizonInboxDetailsHeader( containerColor = HorizonColors.Surface.pagePrimary(), titleContentColor = HorizonColors.Text.title(), navigationIconContentColor = HorizonColors.Icon.default() - ) + ), ) } @@ -195,6 +219,7 @@ private fun HorizonInboxDetailsContent( Column( modifier = modifier .fillMaxSize() + .padding(WindowInsets.topBarScreenInsets.asPaddingValues()) .clip(HorizonCornerRadius.level4Top) .background(HorizonColors.Surface.pageSecondary()) ) { @@ -203,7 +228,6 @@ private fun HorizonInboxDetailsContent( .fillMaxSize() .background(HorizonColors.Surface.pageSecondary()), reverseLayout = state.bottomLayout, - contentPadding = PaddingValues(top = 16.dp) ) { if (state.replyState != null) { stickyHeader { HorizonInboxReplyContent(state.replyState) } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt index ce3e5977c6..c2171be8e0 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/inbox/list/HorizonInboxListScreen.kt @@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -41,7 +43,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults.Indicator import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -53,7 +54,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController import com.instructure.canvasapi2.utils.ContextKeeper @@ -82,8 +82,9 @@ import com.instructure.horizon.horizonui.organisms.inputs.multiselectsearch.Mult import com.instructure.horizon.horizonui.organisms.inputs.singleselect.SingleSelect import com.instructure.horizon.horizonui.organisms.inputs.singleselect.SingleSelectInputSize import com.instructure.horizon.horizonui.organisms.inputs.singleselect.SingleSelectState -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.getActivityOrNull +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.fullScreenInsets +import com.instructure.horizon.util.zeroScreenInsets import com.instructure.pandautils.utils.localisedFormat import java.util.Date @@ -101,22 +102,22 @@ fun HorizonInboxListScreen( } } } - val activity = LocalContext.current.getActivityOrNull() - LaunchedEffect(Unit) { - if (activity != null) { - ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) - } - } - Scaffold( - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - containerColor = HorizonColors.Surface.pagePrimary(), - ) { padding -> - InboxStateWrapper( - state, - navController, - Modifier.padding(padding) - ) + HorizonEdgeToEdgeSystemBars( + statusBarColor = HorizonColors.Surface.pagePrimary(), + navigationBarColor = HorizonColors.Surface.cardPrimary(), + ) { + Scaffold( + contentWindowInsets = WindowInsets.zeroScreenInsets, + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + containerColor = HorizonColors.Surface.pagePrimary(), + ) { padding -> + InboxStateWrapper( + state, + navController, + Modifier.padding(padding) + ) + } } } @@ -145,7 +146,9 @@ private fun InboxStateWrapper( ) }, content = { - LazyColumn { + LazyColumn( + contentPadding = WindowInsets.fullScreenInsets.asPaddingValues() + ) { inboxHeader(state, navController) if (state.loadingState.isLoading) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt index 89c4de046d..5412e46ec7 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/LearnScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -61,7 +62,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavHostController import androidx.navigation.compose.rememberNavController @@ -82,19 +82,13 @@ import com.instructure.horizon.horizonui.molecules.Spinner import com.instructure.horizon.horizonui.organisms.inputs.common.InputDropDownPopup import com.instructure.horizon.horizonui.platform.LoadingState import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.bottomNavigationScreenInsets import com.instructure.pandautils.compose.modifiers.conditional -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.getActivityOrNull import kotlinx.coroutines.delay @Composable fun LearnScreen(state: LearnUiState, mainNavController: NavHostController) { - - val activity = LocalContext.current.getActivityOrNull() - LaunchedEffect(Unit) { - if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) - } - val snackbarHostState: SnackbarHostState = remember { SnackbarHostState() } LaunchedEffect(state.screenState.snackbarMessage) { @@ -106,24 +100,31 @@ fun LearnScreen(state: LearnUiState, mainNavController: NavHostController) { } } - Scaffold( - containerColor = HorizonColors.Surface.pagePrimary(), - snackbarHost = { SnackbarHost(snackbarHostState) } - ) { padding -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(padding), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - when { - state.screenState.isError -> ErrorContent(state.screenState, state.screenState.errorMessage.orEmpty()) - state.screenState.isLoading -> LoadingContent() - else -> if (state.learningItems.isEmpty()) { - LearnScreenEmptyContent(state) - } else { - LearnScreenWrapper(state, mainNavController, Modifier.fillMaxSize()) + HorizonEdgeToEdgeSystemBars(null, null) { + Scaffold( + contentWindowInsets = WindowInsets.bottomNavigationScreenInsets, + containerColor = HorizonColors.Surface.pagePrimary(), + snackbarHost = { SnackbarHost(snackbarHostState) } + ) { padding -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(padding), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + when { + state.screenState.isError -> ErrorContent( + state.screenState, + state.screenState.errorMessage.orEmpty() + ) + + state.screenState.isLoading -> LoadingContent() + else -> if (state.learningItems.isEmpty()) { + LearnScreenEmptyContent(state) + } else { + LearnScreenWrapper(state, mainNavController, Modifier.fillMaxSize()) + } } } } @@ -186,7 +187,10 @@ private fun LearnScreenWrapper( modifier = modifier .fillMaxSize() ) { - Column(modifier = Modifier.padding(top = 16.dp)) { + Column( + modifier = Modifier + .padding(top = 16.dp) + ) { val selectedLearningItem = state.selectedLearningItem AnimatedContent(selectedLearningItem) { selectedItem -> selectedItem?.parentItem?.let { parentItem -> diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt index 62ed59915a..c693f2ee50 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/learn/course/CourseDetailsScreen.kt @@ -19,6 +19,7 @@ import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.HorizontalPager @@ -61,6 +62,7 @@ fun CourseDetailsScreen( val coroutineScope = rememberCoroutineScope() Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), containerColor = HorizonColors.Surface.pagePrimary(), ) { padding -> Column(modifier = Modifier.padding(padding)) { diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt index 8095ded43e..57039d183b 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/moduleitemsequence/ModuleItemSequenceScreen.kt @@ -29,10 +29,15 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PageSize @@ -121,65 +126,72 @@ import com.instructure.horizon.horizonui.molecules.PillType import com.instructure.horizon.horizonui.molecules.Spinner import com.instructure.horizon.horizonui.molecules.SpinnerSize import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.horizontalSafeDrawing import com.instructure.pandautils.compose.modifiers.conditional import com.instructure.pandautils.utils.Const -import com.instructure.pandautils.utils.ThemePrefs -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.getActivityOrNull import com.instructure.pandautils.utils.orDefault import kotlin.math.abs @Composable fun ModuleItemSequenceScreen(mainNavController: NavHostController, uiState: ModuleItemSequenceUiState) { - val activity = LocalContext.current.getActivityOrNull() - if (activity != null) ViewStyler.setStatusBarColor(activity, ThemePrefs.brandColor, true) if (uiState.progressScreenState.visible) ProgressScreen(uiState.progressScreenState, uiState.loadingState) val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } - Scaffold( - containerColor = HorizonColors.Surface.institution(), - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - bottomBar = { - ModuleItemSequenceBottomBar( - showNextButton = uiState.currentPosition < uiState.items.size - 1, - showPreviousButton = uiState.currentPosition > 0, - showNotebookButton = uiState.currentItem?.moduleItemContent is ModuleItemContent.Page, - showAssignmentToolsButton = uiState.currentItem?.moduleItemContent is ModuleItemContent.Assignment, - onNextClick = uiState.onNextClick, - onPreviousClick = uiState.onPreviousClick, - onAssignmentToolsClick = uiState.onAssignmentToolsClick, - onAiAssistClick = { uiState.updateShowAiAssist(true) }, - onNotebookClick = { - mainNavController.navigate( - NotebookRoute.Notebook.route( - uiState.courseId.toString(), - uiState.objectTypeAndId.first, - uiState.objectTypeAndId.second, - true, - false, - true + HorizonEdgeToEdgeSystemBars( + statusBarColor = HorizonColors.Surface.institution(), + navigationBarColor = HorizonColors.Surface.pagePrimary(), + modifier = Modifier.padding(WindowInsets.horizontalSafeDrawing.asPaddingValues()) + ){ + Scaffold( + contentWindowInsets = WindowInsets.horizontalSafeDrawing, + containerColor = HorizonColors.Surface.pagePrimary(), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + bottomBar = { + ModuleItemSequenceBottomBar( + showNextButton = uiState.currentPosition < uiState.items.size - 1, + showPreviousButton = uiState.currentPosition > 0, + showNotebookButton = uiState.currentItem?.moduleItemContent is ModuleItemContent.Page, + showAssignmentToolsButton = uiState.currentItem?.moduleItemContent is ModuleItemContent.Assignment, + onNextClick = uiState.onNextClick, + onPreviousClick = uiState.onPreviousClick, + onAssignmentToolsClick = uiState.onAssignmentToolsClick, + onAiAssistClick = { uiState.updateShowAiAssist(true) }, + onNotebookClick = { + mainNavController.navigate( + NotebookRoute.Notebook.route( + uiState.courseId.toString(), + uiState.objectTypeAndId.first, + uiState.objectTypeAndId.second, + true, + false, + true + ) ) - ) - }, - notebookEnabled = uiState.notebookButtonEnabled, - aiAssistEnabled = uiState.aiAssistButtonEnabled, - hasUnreadComments = uiState.hasUnreadComments - ) - } - ) { contentPadding -> - Box(modifier = Modifier.padding(contentPadding)) { - if (uiState.showAiAssist) { - AiAssistantScreen( - onDismiss = { uiState.updateShowAiAssist(false) }, + }, + notebookEnabled = uiState.notebookButtonEnabled, + aiAssistEnabled = uiState.aiAssistButtonEnabled, + hasUnreadComments = uiState.hasUnreadComments ) } - ModuleItemSequenceContent(uiState = uiState, mainNavController = mainNavController, onBackPressed = { - mainNavController.popBackStack() - }) - val markAsDoneState = uiState.currentItem?.markAsDoneUiState - if (markAsDoneState != null && !uiState.currentItem.isLoading) { - MarkAsDoneButton(markAsDoneState) + ) { contentPadding -> + Box(modifier = Modifier.padding(contentPadding)) { + if (uiState.showAiAssist) { + AiAssistantScreen( + onDismiss = { uiState.updateShowAiAssist(false) }, + ) + } + ModuleItemSequenceContent( + uiState = uiState, + mainNavController = mainNavController, + onBackPressed = { + mainNavController.popBackStack() + }) + val markAsDoneState = uiState.currentItem?.markAsDoneUiState + if (markAsDoneState != null && !uiState.currentItem.isLoading) { + MarkAsDoneButton(markAsDoneState) + } } } } @@ -256,13 +268,17 @@ private fun ModuleItemSequenceContent( moduleHeaderHeight = coordinates.size.height val temp = nestedScrollConnection.appBarOffset nestedScrollConnection = - CollapsingAppBarNestedScrollConnection(moduleHeaderHeight).apply { appBarOffset = temp } + CollapsingAppBarNestedScrollConnection(moduleHeaderHeight).apply { + appBarOffset = temp + } } } ) { ModuleHeaderContainer( uiState = uiState, modifier = Modifier + .background(HorizonColors.Surface.institution()) + .windowInsetsPadding(WindowInsets.statusBars) .padding(start = 24.dp, end = 24.dp, top = 16.dp, bottom = 24.dp) .wrapContentHeight(), onBackPressed = onBackPressed @@ -274,7 +290,10 @@ private fun ModuleItemSequenceContent( containerColor = Color.Transparent, modifier = Modifier .conditional(uiState.loadingState.isLoading || uiState.loadingState.isError) { - background(color = HorizonColors.Surface.pageSecondary(), shape = HorizonCornerRadius.level5) + background( + color = HorizonColors.Surface.pageSecondary(), + shape = HorizonCornerRadius.level5 + ) } .padding(top = moduleHeaderHeight) ) { @@ -377,7 +396,7 @@ private fun ModuleItemPager(pagerState: PagerState, modifier: Modifier = Modifie state = pagerState, beyondViewportPageCount = 0, pageSize = PageSize.Fill, - modifier = modifier, + modifier = modifier.background(HorizonColors.Surface.institution()), userScrollEnabled = false ) { page -> Column( @@ -538,10 +557,14 @@ private fun ModuleItemSequenceBottomBar( onNotebookClick: () -> Unit = {}, hasUnreadComments: Boolean = false ) { - Surface(shadowElevation = HorizonElevation.level4, color = HorizonColors.Surface.pagePrimary()) { + Surface( + shadowElevation = HorizonElevation.level4, + color = HorizonColors.Surface.pagePrimary(), + ) { Box( modifier = modifier .fillMaxWidth() + .windowInsetsPadding(WindowInsets.navigationBars) .padding(horizontal = 24.dp, vertical = 16.dp) ) { if (showPreviousButton) IconButton( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt index ae23b33e01..34bc237605 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/NotebookScreen.kt @@ -21,8 +21,10 @@ import androidx.compose.foundation.clickable 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.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.add +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn @@ -66,6 +68,9 @@ import com.instructure.horizon.horizonui.molecules.IconButtonColor import com.instructure.horizon.horizonui.molecules.IconButtonSize import com.instructure.horizon.horizonui.molecules.Spinner import com.instructure.horizon.navigation.MainNavigationRoute +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars +import com.instructure.horizon.util.topBarScreenInsets +import com.instructure.horizon.util.zeroScreenInsets import com.instructure.pandautils.compose.modifiers.conditional import com.instructure.pandautils.utils.ViewStyler import com.instructure.pandautils.utils.getActivityOrNull @@ -79,109 +84,125 @@ fun NotebookScreen( ) { val activity = LocalContext.current.getActivityOrNull() LaunchedEffect(Unit) { - if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) + if (activity != null) ViewStyler.setStatusBarColor( + activity, + ContextCompat.getColor(activity, R.color.surface_pagePrimary) + ) } val scrollState = rememberLazyListState() - Scaffold( - containerColor = HorizonColors.Surface.pagePrimary(), - topBar = { - if (state.showTopBar) { - NotebookAppBar( - navigateBack = { mainNavController.popBackStack() }, - modifier = Modifier.conditional(scrollState.canScrollBackward) { - horizonShadow( - elevation = HorizonElevation.level2, + HorizonEdgeToEdgeSystemBars( + null, + HorizonColors.Surface.pagePrimary() + ) { + Scaffold( + contentWindowInsets = WindowInsets.zeroScreenInsets, + containerColor = HorizonColors.Surface.pagePrimary(), + topBar = { + if (state.showTopBar) { + NotebookAppBar( + navigateBack = { mainNavController.popBackStack() }, + modifier = Modifier.conditional(scrollState.canScrollBackward) { + horizonShadow( + elevation = HorizonElevation.level2, + ) + } + ) + } + }, + ) { padding -> + LazyColumn( + state = scrollState, + modifier = Modifier + .padding(padding), + contentPadding = WindowInsets.topBarScreenInsets.add( + WindowInsets( + 24.dp, + 24.dp, + 24.dp, + 24.dp + ) + ).asPaddingValues() + ) { + if (state.showFilters && state.notes.isNotEmpty()) { + item { + FilterContent( + state.selectedFilter, + state.onFilterSelected ) } - ) - } - }, - ) { padding -> - LazyColumn( - state = scrollState, - modifier = Modifier - .padding(padding), - contentPadding = PaddingValues(24.dp) - ) { - if (state.showFilters && state.notes.isNotEmpty()) { - item { - FilterContent( - state.selectedFilter, - state.onFilterSelected - ) } - } - if (state.notes.isNotEmpty()){ - item { - Column { - Text( - text = stringResource(R.string.notebookNotesLabel), - style = HorizonTypography.labelLargeBold, - color = HorizonColors.Text.title() - ) + if (state.notes.isNotEmpty()) { + item { + Column { + Text( + text = stringResource(R.string.notebookNotesLabel), + style = HorizonTypography.labelLargeBold, + color = HorizonColors.Text.title() + ) - HorizonSpace(SpaceSize.SPACE_12) + HorizonSpace(SpaceSize.SPACE_12) + } } } - } - if (state.isLoading) { - item { - LoadingContent() - } - } else if (state.notes.isEmpty()) { - item { - EmptyContent() - } - } else { - items(state.notes) { note -> - Column { - NoteContent(note) { - if (state.navigateToEdit) { - mainNavController.navigate( - NotebookRoute.EditNotebook( - noteId = note.id, - highlightedTextStartOffset = note.highlightedText.range.startOffset, - highlightedTextEndOffset = note.highlightedText.range.endOffset, - highlightedTextStartContainer = note.highlightedText.range.startContainer, - highlightedTextEndContainer = note.highlightedText.range.endContainer, - textSelectionStart = note.highlightedText.textPosition.start, - textSelectionEnd = note.highlightedText.textPosition.end, - highlightedText = note.highlightedText.selectedText, - noteType = note.type.name, - userComment = note.userText + if (state.isLoading) { + item { + LoadingContent() + } + } else if (state.notes.isEmpty()) { + item { + EmptyContent() + } + } else { + items(state.notes) { note -> + Column { + NoteContent(note) { + if (state.navigateToEdit) { + mainNavController.navigate( + NotebookRoute.EditNotebook( + noteId = note.id, + highlightedTextStartOffset = note.highlightedText.range.startOffset, + highlightedTextEndOffset = note.highlightedText.range.endOffset, + highlightedTextStartContainer = note.highlightedText.range.startContainer, + highlightedTextEndContainer = note.highlightedText.range.endContainer, + textSelectionStart = note.highlightedText.textPosition.start, + textSelectionEnd = note.highlightedText.textPosition.end, + highlightedText = note.highlightedText.selectedText, + noteType = note.type.name, + userComment = note.userText + ) ) - ) - } else { - mainNavController.navigate( - MainNavigationRoute.ModuleItemSequence( - courseId = note.courseId, - moduleItemAssetType = note.objectType.value, - moduleItemAssetId = note.objectId, + } else { + mainNavController.navigate( + MainNavigationRoute.ModuleItemSequence( + courseId = note.courseId, + moduleItemAssetType = note.objectType.value, + moduleItemAssetId = note.objectId, + ) ) - ) + } } - } - if (state.notes.lastOrNull() != note) { - HorizonSpace(SpaceSize.SPACE_12) + if (state.notes.lastOrNull() != note) { + HorizonSpace(SpaceSize.SPACE_12) + } } } - } - item { - Column { - HorizonSpace(SpaceSize.SPACE_24) + item { + Column { + HorizonSpace(SpaceSize.SPACE_24) - NotesPager( - canNavigateBack = state.hasPreviousPage, - canNavigateForward = state.hasNextPage, - isLoading = state.isLoading, - onNavigateBack = state.loadPreviousPage, - onNavigateForward = state.loadNextPage - ) + NotesPager( + canNavigateBack = state.hasPreviousPage, + canNavigateForward = state.hasNextPage, + isLoading = state.isLoading, + onNavigateBack = state.loadPreviousPage, + onNavigateForward = state.loadNextPage + ) + } } } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt index 4efc360ecf..3a2b1cba69 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notebook/addedit/AddEditNoteScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -35,8 +36,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.navigation.NavHostController +import androidx.navigation.compose.rememberNavController import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedData import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataRange import com.instructure.canvasapi2.managers.graphql.horizon.redwood.NoteHighlightedDataTextPosition @@ -59,8 +60,6 @@ import com.instructure.horizon.horizonui.molecules.Spinner import com.instructure.horizon.horizonui.organisms.inputs.common.InputLabelRequired import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextArea import com.instructure.horizon.horizonui.organisms.inputs.textarea.TextAreaState -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.getActivityOrNull @Composable fun AddEditNoteScreen( @@ -68,16 +67,12 @@ fun AddEditNoteScreen( state: AddEditNoteUiState, onShowSnackbar: (String?, () -> Unit) -> Unit ) { - val activity = LocalContext.current.getActivityOrNull() - LaunchedEffect(Unit) { - if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) - } - LaunchedEffect(state.snackbarMessage) { onShowSnackbar(state.snackbarMessage, state.onSnackbarDismiss) } Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), containerColor = HorizonColors.Surface.pagePrimary(), topBar = { NotebookAppBar(navigateBack = { navController.popBackStack() }) }, ) { padding -> @@ -212,7 +207,7 @@ private fun AddEditNoteScreenPreview() { ) AddEditNoteScreen( - navController = NavHostController(LocalContext.current), + navController = rememberNavController(), state = state, onShowSnackbar = { _, _ -> } ) diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt index 8c27503c8a..fee9c239b5 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/notification/NotificationScreen.kt @@ -33,7 +33,6 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -44,7 +43,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat import androidx.core.net.toUri import androidx.navigation.NavDeepLinkRequest import androidx.navigation.NavHostController @@ -66,8 +64,7 @@ import com.instructure.horizon.horizonui.molecules.StatusChipState import com.instructure.horizon.horizonui.organisms.scaffolds.HorizonScaffold import com.instructure.horizon.horizonui.platform.LoadingState import com.instructure.horizon.horizonui.platform.LoadingStateWrapper -import com.instructure.pandautils.utils.ViewStyler -import com.instructure.pandautils.utils.getActivityOrNull +import com.instructure.horizon.util.HorizonEdgeToEdgeSystemBars import com.instructure.pandautils.utils.isPreviousDay import com.instructure.pandautils.utils.isSameDay import com.instructure.pandautils.utils.isSameWeek @@ -81,27 +78,28 @@ import java.util.Locale @OptIn(ExperimentalMaterial3Api::class) @Composable fun NotificationScreen(state: NotificationUiState, mainNavController: NavHostController) { - val activity = LocalContext.current.getActivityOrNull() val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - if (activity != null) ViewStyler.setStatusBarColor(activity, ContextCompat.getColor(activity, R.color.surface_pagePrimary)) - } - HorizonScaffold( - title = stringResource(R.string.notificationsTitle), - snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, - onBackPressed = { mainNavController.popBackStack() }, - ) { modifier -> - LoadingStateWrapper(state.screenState) { - NotificationContent( - mainNavController, - state, - showSnackbar = { message -> - scope.launch { snackbarHostState.showSnackbar(message) } - }, - modifier - ) + HorizonEdgeToEdgeSystemBars( + null, + HorizonColors.Surface.cardPrimary() + ){ + HorizonScaffold( + title = stringResource(R.string.notificationsTitle), + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, + onBackPressed = { mainNavController.popBackStack() }, + ) { modifier -> + LoadingStateWrapper(state.screenState) { + NotificationContent( + mainNavController, + state, + showSnackbar = { message -> + scope.launch { snackbarHostState.showSnackbar(message) } + }, + modifier + ) + } } } } diff --git a/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceScreen.kt b/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceScreen.kt index 8bd034e0f9..3cd68af335 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceScreen.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/features/skillspace/SkillspaceScreen.kt @@ -21,7 +21,9 @@ import android.view.ViewGroup import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -32,6 +34,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.instructure.horizon.horizonui.platform.LoadingStateWrapper +import com.instructure.horizon.util.bottomNavigationScreenInsets import com.instructure.pandautils.compose.composables.ComposeCanvasWebView import com.instructure.pandautils.compose.composables.ComposeEmbeddedWebViewCallbacks import com.instructure.pandautils.utils.ThemePrefs @@ -52,7 +55,11 @@ fun SkillspaceScreen(state: SkillspaceUiState) { webView?.handleOnActivityResult(request, result.resultCode, result.data) } - Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .fillMaxSize() + .windowInsetsPadding(WindowInsets.bottomNavigationScreenInsets) + ) { LoadingStateWrapper(state.loadingState) { state.webviewUrl?.let { ComposeCanvasWebView( diff --git a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/platform/LoadingStateWrapper.kt b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/platform/LoadingStateWrapper.kt index 5796539765..6c1572c007 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/horizonui/platform/LoadingStateWrapper.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/horizonui/platform/LoadingStateWrapper.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState @@ -77,7 +78,12 @@ fun LoadingStateWrapper( } } - Scaffold(containerColor = containerColor, snackbarHost = { SnackbarHost(snackbarHostState) }, modifier = modifier) { paddingValues -> + Scaffold( + contentWindowInsets = WindowInsets(0, 0, 0, 0), + containerColor = containerColor, + snackbarHost = { SnackbarHost(snackbarHostState) }, + modifier = modifier + ) { paddingValues -> if (loadingState.isPullToRefreshEnabled) { PullToRefreshBox( isRefreshing = loadingState.isRefreshing, diff --git a/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt b/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt index b573a35e8d..2ab274f820 100644 --- a/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt +++ b/libs/horizon/src/main/java/com/instructure/horizon/navigation/HorizonNavigation.kt @@ -15,6 +15,7 @@ */ package com.instructure.horizon.navigation +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.padding import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost @@ -51,6 +52,7 @@ import com.instructure.horizon.navigation.MainNavigationRoute.Companion.ASSIGNME import com.instructure.horizon.navigation.MainNavigationRoute.Companion.COURSE_ID import com.instructure.horizon.navigation.MainNavigationRoute.Companion.PAGE_ID import com.instructure.horizon.navigation.MainNavigationRoute.Companion.QUIZ_ID +import com.instructure.horizon.util.zeroScreenInsets import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -87,6 +89,7 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } Scaffold( + contentWindowInsets = WindowInsets.zeroScreenInsets, snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, ) { innerPadding -> NavHost( @@ -136,7 +139,8 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod ), deepLinks = listOf( navDeepLink { - uriPattern = "${ApiPrefs.fullDomain}/courses/{${COURSE_ID}}/assignments/{${ASSIGNMENT_ID}}" + uriPattern = + "${ApiPrefs.fullDomain}/courses/{${COURSE_ID}}/assignments/{${ASSIGNMENT_ID}}" } ) ) { backStackEntry -> @@ -155,7 +159,9 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod moduleItemAssetId = assignmentId?.toString() ) ) { - popUpTo(MainNavigationRoute.AssignmentDetailsDeepLink.route) { inclusive = true } + popUpTo(MainNavigationRoute.AssignmentDetailsDeepLink.route) { + inclusive = true + } } } } @@ -173,7 +179,8 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod ), deepLinks = listOf( navDeepLink { - uriPattern = "${ApiPrefs.fullDomain}/courses/{${COURSE_ID}}/quizzes/{${QUIZ_ID}}" + uriPattern = + "${ApiPrefs.fullDomain}/courses/{${COURSE_ID}}/quizzes/{${QUIZ_ID}}" } ) ) { backStackEntry -> @@ -192,7 +199,9 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod moduleItemAssetId = quizId?.toString() ) ) { - popUpTo(MainNavigationRoute.QuizDetailsDeepLink.route) { inclusive = true } + popUpTo(MainNavigationRoute.QuizDetailsDeepLink.route) { + inclusive = true + } } } } @@ -210,7 +219,8 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod ), deepLinks = listOf( navDeepLink { - uriPattern = "${ApiPrefs.fullDomain}/courses/{${COURSE_ID}}/pages/{${PAGE_ID}}" + uriPattern = + "${ApiPrefs.fullDomain}/courses/{${COURSE_ID}}/pages/{${PAGE_ID}}" } ) ) { backStackEntry -> @@ -227,10 +237,12 @@ fun HorizonNavigation(navController: NavHostController, modifier: Modifier = Mod moduleItemAssetId = pageId ) ) { - popUpTo(MainNavigationRoute.PageDetailsDeepLink.route) { inclusive = true } + popUpTo(MainNavigationRoute.PageDetailsDeepLink.route) { + inclusive = true + } } } } } } -} \ No newline at end of file +} diff --git a/libs/horizon/src/main/java/com/instructure/horizon/util/HorizonEdgeToEdgeHelper.kt b/libs/horizon/src/main/java/com/instructure/horizon/util/HorizonEdgeToEdgeHelper.kt new file mode 100644 index 0000000000..ffdf77c328 --- /dev/null +++ b/libs/horizon/src/main/java/com/instructure/horizon/util/HorizonEdgeToEdgeHelper.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2025 - present Instructure, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package com.instructure.horizon.util + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.layout.windowInsetsBottomHeight +import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat +import com.instructure.horizon.horizonui.foundation.HorizonColors + +@Composable +fun HorizonEdgeToEdgeSystemBars( + statusBarColor: Color? = HorizonColors.Surface.pagePrimary(), + navigationBarColor: Color? = null, + modifier: Modifier = Modifier, + statusBarAlpha: Float = 0.8f, + navigationBarAlpha: Float = 0.8f, + content: @Composable () -> Unit +) { + val view = LocalView.current + + SideEffect { + val window = (view.context as? android.app.Activity)?.window ?: return@SideEffect + val insetsController = WindowCompat.getInsetsController(window, view) + + statusBarColor?.let { color -> + val isLight = color.luminance() > 0.5f + insetsController.isAppearanceLightStatusBars = isLight + } + + navigationBarColor?.let { color -> + val isLight = color.luminance() > 0.5f + insetsController.isAppearanceLightNavigationBars = isLight + } + } + + Box { + content() + statusBarColor?.let { + Box( + modifier = modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .windowInsetsTopHeight(WindowInsets.statusBars) + .background(statusBarColor.copy(alpha = statusBarAlpha)) + ) + } + + navigationBarColor?.let { + Box( + modifier = modifier + .align(Alignment.BottomCenter) + .fillMaxWidth() + .windowInsetsBottomHeight(WindowInsets.navigationBars) + .background(navigationBarColor.copy(alpha = navigationBarAlpha)) + ) + } + } +} + +private val WindowInsetsSides.Companion.BottomHorizontalSides: WindowInsetsSides + get() = WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom + +private val WindowInsetsSides.Companion.TopHorizontalSides: WindowInsetsSides + get() = WindowInsetsSides.Horizontal + WindowInsetsSides.Top + +val WindowInsets.Companion.zeroScreenInsets: WindowInsets + get() = WindowInsets(0, 0, 0, 0) + +val WindowInsets.Companion.bottomNavigationScreenInsets: WindowInsets + @Composable + get() = WindowInsets.safeDrawing.only(WindowInsetsSides.TopHorizontalSides) + +val WindowInsets.Companion.topBarScreenInsets: WindowInsets + @Composable + get() = WindowInsets.safeDrawing.only(WindowInsetsSides.BottomHorizontalSides) + +val WindowInsets.Companion.horizontalSafeDrawing: WindowInsets + @Composable + get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal) + +val WindowInsets.Companion.verticalSafeDrawing: WindowInsets + @Composable + get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical) + +val WindowInsets.Companion.topSafeDrawing: WindowInsets + @Composable + get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Top) + +val WindowInsets.Companion.bottomSafeDrawing: WindowInsets + @Composable + get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom) + +val WindowInsets.Companion.fullScreenInsets: WindowInsets + @Composable + get() = WindowInsets.safeDrawing