diff --git a/build.gradle.kts b/build.gradle.kts index a13e1683..19d5d0e1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -39,7 +39,7 @@ allprojects { subprojects { plugins.withId("com.vanniktech.maven.publish") { configure { - publishToMavenCentral() + publishToMavenCentral(automaticRelease = true) // Artifact ID만 각 프로젝트의 이름으로 자동 설정 group = "zone.ien.hig" @@ -73,12 +73,12 @@ subprojects { } } -// val isSnapshot = version.toString().endsWith("SNAPSHOT") -// val hasSigningKey = !(project.findProperty("signingInMemoryKeyId") as String?).isNullOrBlank() -// -// if (!isSnapshot && hasSigningKey) { - signAllPublications() -// } + val isPublishingToMavenLocal = gradle.startParameter.taskNames.any { it.contains("publishToMavenLocal", ignoreCase = true) } + val isSnapshot = version.toString().endsWith("SNAPSHOT") + + if (!isSnapshot && !isPublishingToMavenLocal) { + signAllPublications() + } } } } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/multiplatform-module-convention.gradle.kts b/buildSrc/src/main/kotlin/multiplatform-module-convention.gradle.kts index 554d9863..45d7c193 100644 --- a/buildSrc/src/main/kotlin/multiplatform-module-convention.gradle.kts +++ b/buildSrc/src/main/kotlin/multiplatform-module-convention.gradle.kts @@ -110,6 +110,13 @@ kotlin { jsMain.get().dependsOn(this) wasmJsMain.dependsOn(this) } + val nonDarwinMain by creating { + dependsOn(commonMain.get()) + androidMain.get().dependsOn(this) + desktopMain.dependsOn(this) + jsMain.get().dependsOn(this) + wasmJsMain.dependsOn(this) + } val darwinMain by creating { dependsOn(commonMain.get()) @@ -124,5 +131,5 @@ kotlin { } } - compilerOptions.freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime") + compilerOptions.freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } \ No newline at end of file diff --git a/example/composeApp/build.gradle.kts b/example/composeApp/build.gradle.kts index e273ab70..dcf7046e 100644 --- a/example/composeApp/build.gradle.kts +++ b/example/composeApp/build.gradle.kts @@ -107,7 +107,7 @@ kotlin { } } - compilerOptions.freeCompilerArgs.add("-Xopt-in=kotlin.time.ExperimentalTime") + compilerOptions.freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") } dependencies { diff --git a/example/composeApp/src/commonMain/kotlin/RootNavigationGraph.kt b/example/composeApp/src/commonMain/kotlin/RootNavigationGraph.kt index e5b90c90..eaefe741 100644 --- a/example/composeApp/src/commonMain/kotlin/RootNavigationGraph.kt +++ b/example/composeApp/src/commonMain/kotlin/RootNavigationGraph.kt @@ -45,7 +45,7 @@ fun RootNavigationGraph( modifier = modifier, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), - rememberViewModelStoreNavEntryDecorator() // 뷰모델 꺼짐 보장 + rememberViewModelStoreNavEntryDecorator() // ViewModel lifecycle guarantee ), entryProvider = entryProvider { entry { diff --git a/example/composeApp/src/commonMain/kotlin/cupertino/CupertinoWidgetsScreen.kt b/example/composeApp/src/commonMain/kotlin/cupertino/CupertinoWidgetsScreen.kt index a802a936..2b70d73b 100644 --- a/example/composeApp/src/commonMain/kotlin/cupertino/CupertinoWidgetsScreen.kt +++ b/example/composeApp/src/commonMain/kotlin/cupertino/CupertinoWidgetsScreen.kt @@ -43,8 +43,10 @@ import RootDetails import RootRoute import RootUiState import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.ScrollableState @@ -68,6 +70,11 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -77,10 +84,12 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +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.graphics.Color +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalLayoutDirection @@ -115,12 +124,18 @@ import zone.ien.hig.CupertinoDateTimePicker import zone.ien.hig.CupertinoDateTimePickerNative import zone.ien.hig.CupertinoDateTimePickerState import zone.ien.hig.CupertinoDropdownMenu +import zone.ien.hig.CupertinoDropdownMenuNative import zone.ien.hig.CupertinoIcon import zone.ien.hig.CupertinoIconButton import zone.ien.hig.CupertinoLiquidAlertDialog import zone.ien.hig.CupertinoLiquidButton import zone.ien.hig.CupertinoLiquidButtonDefaults import zone.ien.hig.CupertinoLiquidIconButton +import zone.ien.hig.CupertinoMenuItemData +import zone.ien.hig.CupertinoMenuSectionData +import zone.ien.hig.CupertinoNavigationBar +import zone.ien.hig.CupertinoNavigationBarItem +import zone.ien.hig.CupertinoNavigationBarItemData import zone.ien.hig.CupertinoNavigationTitle import zone.ien.hig.CupertinoPickerState import zone.ien.hig.CupertinoRangeSlider @@ -140,11 +155,13 @@ import zone.ien.hig.CupertinoTopAppBar import zone.ien.hig.CupertinoTriStateCheckBox import zone.ien.hig.CupertinoWheelPicker import zone.ien.hig.ExperimentalCupertinoApi +import zone.ien.hig.HigMenuOptions import zone.ien.hig.MenuAction import zone.ien.hig.MenuSection import zone.ien.hig.PresentationStyle import zone.ien.hig.adaptive.AdaptiveNavigationBar import zone.ien.hig.adaptive.AdaptiveNavigationBarItem +import zone.ien.hig.adaptive.AdaptiveNavigationBarNative import zone.ien.hig.adaptive.ExperimentalAdaptiveApi import zone.ien.hig.adaptive.Theme import zone.ien.hig.adaptive.icons.AdaptiveIcons @@ -162,6 +179,7 @@ import zone.ien.hig.icons.filled.Archivebox import zone.ien.hig.icons.filled.Banknote import zone.ien.hig.icons.filled.Pin import zone.ien.hig.icons.filled.Trash +import zone.ien.hig.icons.outlined.Book import zone.ien.hig.icons.outlined.Bookmark import zone.ien.hig.icons.outlined.FaceSmiling import zone.ien.hig.icons.outlined.Heart @@ -217,7 +235,7 @@ private enum class PickerTab { fun CupertinoWidgetsScreen( uiState: RootUiState, onItemValueChanged: (RootDetails) -> Unit, - onNavigate: (NavKey) -> Unit + onNavigate: (NavKey) -> Unit, ) { val scrollState = rememberScrollState() @@ -270,7 +288,8 @@ fun CupertinoWidgetsScreen( }, bottomBar = { BottomBarSample( - backdrop = backdrop + backdrop = backdrop, + isNative = nativePickers.value ) }, ) { pv -> @@ -322,7 +341,7 @@ private fun Body( ) { CupertinoNavigationTitle { - Text("HIG") + Text("Cupertino") } var searchValue by remember { mutableStateOf("") @@ -408,12 +427,14 @@ private fun Body( } SectionItem { DropdownExample( - backdrop = backdrop + backdrop = backdrop, + isNative = nativePickers.value ) } SectionItem { DropdownExample2( - backdrop = backdrop + backdrop = backdrop, + isNative = nativePickers.value ) } } @@ -441,7 +462,7 @@ private fun ScreenPreview() { paddingValues = PaddingValues.Zero, scrollState = rememberScrollState(), scaffoldState = rememberCupertinoBottomSheetScaffoldState(), - nativePickers = remember { mutableStateOf(false) }, + nativePickers = rememberSaveable { mutableStateOf(false) }, onNavigate = {}, backdrop = rememberDefaultBackdrop() ) @@ -835,7 +856,7 @@ private fun TopBarSample( } }, title = { - CupertinoText("HIG") + CupertinoText("Cupertino") } ) } @@ -843,37 +864,55 @@ private fun TopBarSample( @OptIn(ExperimentalAdaptiveApi::class) @Composable private fun BottomBarSample( - backdrop: LayerBackdrop + backdrop: LayerBackdrop, + isNative: Boolean ) { var tab by remember { mutableStateOf(0) } val content = listOf( - "Profile" to AdaptiveIcons.Outlined.Person, - "Menu" to AdaptiveIcons.Outlined.Menu, - "Settings" to AdaptiveIcons.Outlined.Settings, + "Profile" to Icons.Default.Delete, + "Menu" to Icons.Default.Save, + "Setting" to Icons.Default.Settings, ) - AdaptiveNavigationBar( - selectedTabIndex = { tab }, - onTabSelected = { tab = it }, - tabsCount = 3, - adaptation = { - cupertino { this.backdrop = backdrop } - }, - ) { - content.forEachIndexed { index, pair -> - AdaptiveNavigationBarItem( - index = index, - onClick = { tab = index }, - icon = { - CupertinoIcon( - imageVector = pair.second, - contentDescription = pair.first, - ) - }, - label = { - Text(pair.first) - }, - ) + if (isNative) { + AdaptiveNavigationBarNative( + selectedTabIndex = { tab }, + onTabSelected = { tab = it }, + adaptation = { + cupertino { this.backdrop = backdrop } + }, + items = content.mapIndexed { index, item -> + CupertinoNavigationBarItemData( + onClick = { tab = index }, + icon = rememberVectorPainter(item.second), + label = item.first + ) + } + ) + } else { + AdaptiveNavigationBar( + selectedTabIndex = { tab }, + onTabSelected = { tab = it }, + tabsCount = content.size, + adaptation = { + cupertino { this.backdrop = backdrop } + }, + ) { + content.forEachIndexed { index, pair -> + AdaptiveNavigationBarItem( + index = index, + onClick = { tab = index }, + icon = { + Icon( + imageVector = pair.second, + contentDescription = pair.first, + ) + }, + label = { + Text(pair.first) + }, + ) + } } } } @@ -905,7 +944,6 @@ private fun SheetSample( CupertinoText("Done") } }, -// isTransparent = sheetListState.isTopBarTransparent ) } ) { pv -> @@ -1717,7 +1755,8 @@ private fun SheetsExamples() { @Composable private fun DropdownExample( - backdrop: Backdrop + backdrop: Backdrop, + isNative: Boolean ) { var dropdownVisible by remember { mutableStateOf(false) } var pickerSheetVisible by remember { mutableStateOf(false) } @@ -1775,70 +1814,123 @@ private fun DropdownExample( Spacer(Modifier.weight(1f)) //Menu bar should be in the box with anchor to align correctly Box { - CupertinoLiquidButton( - colors = CupertinoLiquidButtonDefaults.glassProminentButtonColors(), - backdrop = layerBackdrop, - onClick = { dropdownVisible = !dropdownVisible } + androidx.compose.animation.AnimatedVisibility( + visible = !dropdownVisible ) { - CupertinoText("Menu") + CupertinoLiquidButton( + colors = CupertinoLiquidButtonDefaults.glassProminentButtonColors(), + backdrop = layerBackdrop, + onClick = { dropdownVisible = !dropdownVisible } + ) { + CupertinoText("Menu") + } } val red = CupertinoColors.systemRed - CupertinoDropdownMenu( - expanded = dropdownVisible, - onDismissRequest = { dropdownVisible = false }, - backdrop = backdrop - ) { - MenuSection( - title = { - Text("Menu") - } - ) { - MenuAction( - onClick = { - dropdownVisible = false - }, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.SquareAndArrowUp, - contentDescription = null + if (isNative) { + + CupertinoDropdownMenuNative( + expanded = dropdownVisible, + onDismissRequest = { + dropdownVisible = false + }, + backdrop = backdrop, + sections = listOf( + CupertinoMenuSectionData( + title = "Menu", + options = HigMenuOptions.SingleSelection, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.SquareAndArrowUp }, + cupertino = { "square.and.arrow.up" } + ), + items = listOf( + CupertinoMenuItemData( + title = "Share", + onClick = { dropdownVisible = false }, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.SquareAndArrowUp }, + cupertino = { "square.and.arrow.up" } + ) + ), + CupertinoMenuItemData( + title = "Add to Favorites", + enabled = false, + onClick = { dropdownVisible = false }, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.Bookmark }, + cupertino = { "bookmark" } + ) + ), + CupertinoMenuItemData( + title = "Delete", + onClick = { dropdownVisible = false }, + isDestructive = true, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.Trash }, + cupertino = { "trash" } + ) + ) ) + ) + ) + ) + } else { + CupertinoDropdownMenu( + expanded = dropdownVisible, + onDismissRequest = { dropdownVisible = false }, + backdrop = backdrop + ) { + MenuSection( + title = { + Text("Menu") } ) { - CupertinoText("Share") + MenuAction( + onClick = { + dropdownVisible = false + }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.SquareAndArrowUp, + contentDescription = null + ) + } + ) { + CupertinoText("Share") + } + MenuAction( + enabled = false, + onClick = { + dropdownVisible = false + }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.Bookmark, + contentDescription = null + ) + } + ) { + CupertinoText("Add to Favorites") + } } + MenuAction( - enabled = false, onClick = { dropdownVisible = false + }, - icon = { + contentColor = red, + leadingIcon = { CupertinoIcon( - imageVector = CupertinoIcons.Default.Bookmark, + imageVector = CupertinoIcons.Default.Trash, contentDescription = null ) } ) { - CupertinoText("Add to Favorites") + CupertinoText("Delete") } } - - MenuAction( - onClick = { - dropdownVisible = false - - }, - contentColor = red, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.Trash, - contentDescription = null - ) - } - ) { - CupertinoText("Delete") - } } } } @@ -1846,7 +1938,8 @@ private fun DropdownExample( @Composable private fun DropdownExample2( - backdrop: Backdrop + backdrop: Backdrop, + isNative: Boolean ) { var dropdownVisible by remember { mutableStateOf(false) } val layerBackdrop = rememberDefaultBackdrop() @@ -1866,60 +1959,107 @@ private fun DropdownExample2( val red = CupertinoColors.systemRed - CupertinoDropdownMenu( - expanded = dropdownVisible, - onDismissRequest = { dropdownVisible = false }, - backdrop = backdrop - ) { - MenuSection( - title = { - Text("Menu") - } - ) { - MenuAction( - onClick = { - dropdownVisible = false - }, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.SquareAndArrowUp, - contentDescription = null + if (isNative) { + CupertinoDropdownMenuNative( + expanded = dropdownVisible, + onDismissRequest = { dropdownVisible = false }, + backdrop = backdrop, + sections = listOf( + CupertinoMenuSectionData( + title = "Menu", + items = listOf( + CupertinoMenuItemData( + title = "Share", + onClick = { + dropdownVisible = false + }, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.SquareAndArrowUp }, + cupertino = { "square.and.arrow.up" } + ) + ), + CupertinoMenuItemData( + title = "Add to Favorites", + enabled = false, + onClick = { + dropdownVisible = false + }, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.Bookmark}, + cupertino = { "bookmark" } + ) + ), + CupertinoMenuItemData( + title = "Delete", + onClick = { + dropdownVisible = false + }, + isDestructive = true, + icon = AdaptiveIcons.painter( + material = { CupertinoIcons.Default.Trash}, + cupertino = { "trash" } + ) + ), ) + ) + ) + ) + } else { + CupertinoDropdownMenu( + expanded = dropdownVisible, + onDismissRequest = { dropdownVisible = false }, + backdrop = backdrop + ) { + MenuSection( + title = { + Text("Menu") } ) { - CupertinoText("Share") + MenuAction( + onClick = { + dropdownVisible = false + }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.SquareAndArrowUp, + contentDescription = null + ) + } + ) { + CupertinoText("Share") + } + MenuAction( + enabled = false, + onClick = { + dropdownVisible = false + }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.Bookmark, + contentDescription = null + ) + } + ) { + CupertinoText("Add to Favorites") + } } + MenuAction( - enabled = false, onClick = { dropdownVisible = false + }, - icon = { + contentColor = red, + leadingIcon = { CupertinoIcon( - imageVector = CupertinoIcons.Default.Bookmark, + imageVector = CupertinoIcons.Default.Trash, contentDescription = null ) } ) { - CupertinoText("Add to Favorites") + CupertinoText("Delete") } } - - MenuAction( - onClick = { - dropdownVisible = false - - }, - contentColor = red, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.Trash, - contentDescription = null - ) - } - ) { - CupertinoText("Delete") - } } } } diff --git a/example/composeApp/src/commonMain/kotlin/test/TestScreen.kt b/example/composeApp/src/commonMain/kotlin/test/TestScreen.kt index 5c50d6fb..6f82758d 100644 --- a/example/composeApp/src/commonMain/kotlin/test/TestScreen.kt +++ b/example/composeApp/src/commonMain/kotlin/test/TestScreen.kt @@ -1,5 +1,9 @@ package test +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -25,9 +29,9 @@ 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.graphics.vector.rememberVectorPainter import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop @@ -35,10 +39,13 @@ import zone.ien.hig.utils.rememberDefaultBackdrop import kotlinx.coroutines.launch import org.jetbrains.compose.resources.painterResource import zone.ien.hig.CupertinoDropdownMenu +import zone.ien.hig.CupertinoDropdownMenuNative import zone.ien.hig.CupertinoIcon import zone.ien.hig.CupertinoLiquidButton import zone.ien.hig.CupertinoLiquidButtonDefaults import zone.ien.hig.CupertinoLiquidIconButton +import zone.ien.hig.CupertinoMenuItemData +import zone.ien.hig.CupertinoMenuSectionData import zone.ien.hig.CupertinoNavigationTitle import zone.ien.hig.CupertinoScaffold import zone.ien.hig.CupertinoText @@ -81,30 +88,40 @@ fun TestScreen( CupertinoText("sub title") }, navigationIcon = { - Box { - CupertinoLiquidIconButton( - onClick = { expanded = true }, - backdrop = backdrop - ) { - CupertinoIcon( - imageVector = CupertinoIcons.Default.ChevronBackward, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - } - CupertinoDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - backdrop = rememberLayerBackdrop() + Row { + + AnimatedVisibility( + visible = enabled, + enter = slideInHorizontally(tween(300)) { -it }, + exit = slideOutHorizontally(tween(300)) { -it } ) { - MenuSection( - title = { Text(text = "Title") } - ) { - repeat(4) { - MenuAction( - onClick = {} + Box { + CupertinoLiquidIconButton( + onClick = { expanded = true }, + backdrop = backdrop, + modifier = Modifier.padding(start = 16.dp) + ) { + CupertinoIcon( + imageVector = CupertinoIcons.Default.ChevronBackward, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + CupertinoDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + backdrop = rememberLayerBackdrop() + ) { + MenuSection( + title = { Text(text = "Title") } ) { - Text(text = "Action") + repeat(4) { + MenuAction( + onClick = {} + ) { + Text(text = "Action") + } + } } } } @@ -154,12 +171,12 @@ fun TestScreen( CupertinoNavigationTitle( subtitle = { Text( - text = "87개의 메모" + text = "87 memos" ) } ) { Text( - text = "메모", + text = "Memo", ) } @@ -202,54 +219,95 @@ fun TestScreen( isInteractive = false, colors = CupertinoLiquidButtonDefaults.glassProminentButtonColors(), ) { - Text(text = "Open Menu") + Text(text = "${if (expanded) "Close" else "Open"} Menu") } // drawBackdrop is used inside Popup internally - CupertinoDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - backdrop = rememberDefaultBackdrop() - ) { - MenuSection( + if (enabled) { + CupertinoDropdownMenuNative( + expanded = expanded, + onDismissRequest = { + expanded = false + }, + backdrop = rememberDefaultBackdrop(), + items = listOf( + CupertinoMenuItemData( + title = "Message Select", + onClick = { + expanded = false + coroutineScope.launch { + snackbarState.showSnackbar("Message Select") + } + }, + icon = rememberVectorPainter(CupertinoIcons.Default.CheckmarkCircle) + ), + CupertinoMenuItemData( + title = "Pin Edit", + onClick = { + expanded = false + coroutineScope.launch { + snackbarState.showSnackbar("Pin Edit") + } + }, + icon = rememberVectorPainter(CupertinoIcons.Default.Pin) + ), + CupertinoMenuItemData( + title = "Name and Photo Setting", + onClick = { + expanded = false + coroutineScope.launch { + snackbarState.showSnackbar("Name and Photo Setting") + } + }, + icon = rememberVectorPainter(CupertinoIcons.Default.PersonCropCircle) + ), + ) + ) + } else { + CupertinoDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + backdrop = rememberDefaultBackdrop() + ) { + MenuSection( // title = { // Text("Menu") // } - ) { - MenuAction( - onClick = { expanded = false }, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.CheckmarkCircle, - contentDescription = null - ) - } ) { - CupertinoText("메시지 선택") - } - MenuAction( - onClick = { expanded = false }, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.Pin, - contentDescription = null - ) + MenuAction( + onClick = { expanded = false }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.CheckmarkCircle, + contentDescription = null + ) + } + ) { + CupertinoText("Message Select") } - ) { - CupertinoText("고정 편집") - } - MenuAction( - onClick = { expanded = false }, - icon = { - CupertinoIcon( - imageVector = CupertinoIcons.Default.PersonCropCircle, - contentDescription = null - ) + MenuAction( + onClick = { expanded = false }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.Pin, + contentDescription = null + ) + } + ) { + CupertinoText("Pin Edit") + } + MenuAction( + onClick = { expanded = false }, + leadingIcon = { + CupertinoIcon( + imageVector = CupertinoIcons.Default.PersonCropCircle, + contentDescription = null + ) + } + ) { + CupertinoText("Name and Photo Setting") } - ) { - CupertinoText("이름 및 사진 설정") } - } // MenuDivider() // MenuSection( //// title = { @@ -284,6 +342,7 @@ fun TestScreen( // CupertinoText("Add to Favorites") // } // } + } } } @@ -294,6 +353,37 @@ fun TestScreen( .fillMaxWidth() .height(200.dp) ) + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp), + modifier = Modifier.padding(horizontal = 24.dp) + ) { + AnimatedVisibility( + visible = enabled, + enter = slideInHorizontally(tween(300)), + exit = slideOutHorizontally(tween(300)) + ) { + CupertinoLiquidIconButton( + onClick = { }, + backdrop = rememberDefaultBackdrop() + ) { + CupertinoIcon( + imageVector = CupertinoIcons.Default.ChevronBackward, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } + CupertinoLiquidIconButton( + onClick = { }, + backdrop = rememberDefaultBackdrop() + ) { + CupertinoIcon( + imageVector = CupertinoIcons.Default.ChevronBackward, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + } + } Box( modifier = Modifier .background(Color.White) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a5ec2b3..2e39adb4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,17 @@ [versions] agp = "9.0.0" -kotlin = "2.3.20" -lib-version-name = "1.0.1" +kotlin = "2.3.21" +lib-version-name = "1.1.0" # plugin -compose-plugin = "1.10.3" -compose-plugin-experimental = "1.10.0-alpha05" -compose-navigation3 = "1.0.0-alpha06" +compose-plugin = "1.11.0-rc01" +compose-plugin-experimental = "1.11.0-alpha07" +compose-navigation3 = "1.1.1" # ui activity-compose = "1.13.0" lifecycle = "2.10.0" -backdrop = "2.0.0-alpha04" +backdrop = "2.0.0-alpha06" capsule = "1.2.0" haze = "1.7.2" @@ -20,10 +20,10 @@ datetime = "0.7.1" material-kolor = "4.1.1" # backend -serialization = "1.10.0" +serialization = "1.11.0" vanniktech-mavenPublish = "0.36.0" atomicfu = "0.23.2" -koin = "4.2.0" +koin = "4.2.1" [libraries] @@ -45,7 +45,8 @@ compose-navigation3 = { group = "org.jetbrains.androidx.navigation3", name = "na backdrop = { group = "zone.ien.backdrop", name = "backdrop", version.ref = "backdrop"} #backdrop = { group = "io.github.kyant0", name = "backdrop", version.ref = "backdrop"} -capsule = { group = "io.github.kyant0", name = "shapes", version.ref = "capsule"} +#capsule = { group = "io.github.kyant0", name = "shapes", version.ref = "capsule"} +capsule = { group = "zone.ien.shapes", name = "shapes", version.ref = "capsule"} haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" } # functionality diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Adaptation.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Adaptation.kt index 6264f618..f0313100 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Adaptation.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Adaptation.kt @@ -53,6 +53,15 @@ sealed interface AdaptationScope { fun material(block: @Composable M.() -> Unit) } +/** + * Base class for creating custom adaptations between Material and Cupertino implementations. + * + * This class provides a framework for defining how Material and Cupertino widgets should be adapted + * by specifying how their properties can be customized. + * + * @param C The type of the Cupertino adaptation object. + * @param M The type of the Material adaptation object. + */ @Stable @ExperimentalAdaptiveApi abstract class Adaptation: AdaptationScope { diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveAlertDialog.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveAlertDialog.kt index 5632e41c..0c4243cf 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveAlertDialog.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveAlertDialog.kt @@ -177,6 +177,12 @@ class CupertinoAlertAdaptation internal constructor( var buttonsOrientation: Orientation by mutableStateOf(buttonsOrientation) } +/** + * Adaptation class for alert dialog that manages theme-specific values for alert dialogs. + * + * This class handles the adaptation between Cupertino and Material design for alert dialogs + * by providing appropriate values for container color, shape, and button orientation. + */ @ExperimentalAdaptiveApi @Stable private class AlertDialogAdaptation: Adaptation() { diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveButton.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveButton.kt index abbb369f..53659b6e 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveButton.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveButton.kt @@ -50,8 +50,22 @@ import zone.ien.hig.ExperimentalCupertinoApi import zone.ien.hig.theme.CupertinoTheme /** - * Adaptive button that takes [Button] or borderedProminent [CupertinoButton] appearance - * */ + * An adaptive button that uses either Material [Button] or [CupertinoLiquidButton] + * depending on the current theme. + * + * Material Design uses the [Button] component, while the Cupertino theme uses + * the [CupertinoLiquidButton] component to provide a native look and feel on each platform. + * + * @param onClick called when the button is clicked + * @param modifier the [Modifier] to be applied to this button + * @param enabled controls the enabled state of this button + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * @param adaptation configuration block for [CupertinoButtonAdaptation] and [MaterialButtonAdaptation] + * @param content the content to be displayed inside the button + * @see AdaptiveWidget + * @see CupertinoLiquidButton + * @see Button + */ @OptIn(ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -101,8 +115,21 @@ fun AdaptiveButton( } /** - * Adaptive button that takes [TextButton] or borderless [CupertinoButton] appearance - * */ + * An adaptive button that uses either Material [TextButton] or borderless [CupertinoLiquidButton] appearance. + * + * Material Design uses the [TextButton] component, while the Cupertino theme uses + * the [CupertinoLiquidButton] component to provide a native look and feel on each platform. + * + * @param onClick called when the button is clicked + * @param modifier the [Modifier] to be applied to this button + * @param enabled controls the enabled state of this button + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * @param adaptation configuration block for [CupertinoButtonAdaptation] and [MaterialButtonAdaptation] + * @param content the content to be displayed inside the button + * @see AdaptiveWidget + * @see CupertinoLiquidButton + * @see TextButton + */ @OptIn(ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -152,8 +179,21 @@ fun AdaptiveTextButton( } /** - * Adaptive button that takes [FilledTonalButton] or bordered [CupertinoButton] appearance - * */ + * An adaptive button that uses either Material [FilledTonalButton] or bordered [CupertinoLiquidButton] appearance. + * + * Material Design uses the [FilledTonalButton] component, while the Cupertino theme uses + * the [CupertinoLiquidButton] component to provide a native look and feel on each platform. + * + * @param onClick called when the button is clicked + * @param modifier the [Modifier] to be applied to this button + * @param enabled controls the enabled state of this button + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * @param adaptation configuration block for [CupertinoButtonAdaptation] and [MaterialButtonAdaptation] + * @param content the content to be displayed inside the button + * @see AdaptiveWidget + * @see CupertinoLiquidButton + * @see FilledTonalButton + */ @OptIn(ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -204,6 +244,18 @@ fun AdaptiveTonalButton( @Stable +/** ++ * An adaptive adaptation class for [CupertinoButton] that manages various properties of the Cupertino button style. ++ * ++ * @param colors [CupertinoLiquidButtonColors] - The color configuration for the button ++ * @param backdrop [LayerBackdrop] - The layer backdrop configuration for the button ++ * @param isBackgroundAdaptive Whether the background is treated as adaptive ++ * @see CupertinoLiquidButtonColors ++ * @see LayerBackdrop ++ * @see CupertinoButtonSize ++ * @see Shape ++ * @see PaddingValues ++ */ class CupertinoButtonAdaptation internal constructor( colors: CupertinoLiquidButtonColors, backdrop: LayerBackdrop, @@ -217,6 +269,20 @@ class CupertinoButtonAdaptation internal constructor( var contentPadding: PaddingValues? by mutableStateOf(null) } +/** + * An adaptive adaptation class for [MaterialButton] that manages various properties of the Material button style. + * + * @param colors [ButtonColors] - The color configuration for the button + * @param elevation [ButtonElevation] - The shadow effect for the button + * @param shape [Shape] - The shape of the button + * @param contentPadding [PaddingValues] - The padding for the button content + * @param border [BorderStroke] - The border style for the button + * @see ButtonColors + * @see ButtonElevation + * @see Shape + * @see PaddingValues + * @see BorderStroke + */ @Stable class MaterialButtonAdaptation internal constructor( colors: ButtonColors, @@ -232,11 +298,28 @@ class MaterialButtonAdaptation internal constructor( var border: BorderStroke? by mutableStateOf(border) } +/** + * Enum representing the different button types that can be adapted + * + * @property Filled - Represents a filled button style + * @property Text - Represents a text button style + * @property Tonal - Represents a tonal button style + */ private enum class ButtonType { Filled, Text, Tonal } -@ExperimentalAdaptiveApi +/** + * Adaptation class for button components that manages theme-specific values for buttons. + * + * This class handles the adaptation between Cupertino and Material design for buttons, + * providing appropriate styling for different button types (Filled, Text, Tonal). + * + * @param type The type of button to adapt (Filled, Text, or Tonal) + * @see CupertinoButtonAdaptation + * @see MaterialButtonAdaptation + */ +@OptIn(ExperimentalAdaptiveApi::class) private class ButtonAdaptation( private val type: ButtonType, ): Adaptation() { diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveCheckbox.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveCheckbox.kt index 701c344d..cd3ee956 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveCheckbox.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveCheckbox.kt @@ -36,6 +36,22 @@ import zone.ien.hig.CupertinoCheckboxColors import zone.ien.hig.CupertinoCheckboxDefaults import zone.ien.hig.CupertinoTriStateCheckBox +/** + * An adaptive checkbox component that provides different checkbox styles based on Material Design and Cupertino themes. + * + * [AdaptiveCheckbox] uses the Material Design [Checkbox] component and the Cupertino [CupertinoCheckBox] component + * to provide an appropriate checkbox for each operating system. + * + * @param checked the current checked state of the checkbox + * @param onCheckedChange callback to be invoked when the checkbox state changes + * @param modifier the [Modifier] to be applied to the element + * @param enabled controls the enabled state of the checkbox + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * @param adaptation configuration block for [CupertinoCheckBoxAdaptation] and [MaterialCheckBoxAdaptation] + * @see AdaptiveWidget + * @see CupertinoCheckBox + * @see Checkbox + */ @ExperimentalAdaptiveApi @Composable fun AdaptiveCheckbox( @@ -72,6 +88,22 @@ fun AdaptiveCheckbox( ) } +/** + * An adaptive tri-state checkbox component that provides different checkbox styles based on Material Design and Cupertino themes. + * + * [AdaptiveTriStateCheckbox] uses the Material Design [TriStateCheckbox] component and the Cupertino [CupertinoTriStateCheckBox] component + * to provide an appropriate tri-state checkbox for each operating system. + * + * @param state The state of the checkbox (Unchecked, Checked, Indeterminate) + * @param onClick The function to be called when the checkbox is clicked + * @param modifier The modifier to be applied to the element + * @param enabled Whether the checkbox is enabled + * @param interactionSource The interaction source + * @param adaptation Custom settings function for [CupertinoCheckBoxAdaptation] and [MaterialCheckBoxAdaptation] + * @see AdaptiveWidget + * @see CupertinoTriStateCheckBox + * @see TriStateCheckbox + */ @ExperimentalAdaptiveApi @Composable fun AdaptiveTriStateCheckbox( @@ -109,6 +141,13 @@ fun AdaptiveTriStateCheckbox( } @Stable +/** + * An adaptive adaptation class for Material Design checkboxes that manages checkbox color properties. + * + * @param colors [CheckboxColors] - The color configuration for the checkbox + * @see CheckboxColors + * @see CheckboxDefaults + */ class MaterialCheckBoxAdaptation( colors: CheckboxColors ) { @@ -116,14 +155,34 @@ class MaterialCheckBoxAdaptation( } @Stable +/** + * Cupertino checkbox adaptation class that manages checkbox color properties. + * + * @param colors [CupertinoCheckboxColors] - Checkbox color configuration + * @see CupertinoCheckboxColors + * @see CupertinoCheckboxDefaults + */ class CupertinoCheckBoxAdaptation( colors: CupertinoCheckboxColors ){ var colors: CupertinoCheckboxColors by mutableStateOf(colors) } +/** + * Adaptation class for checkbox components that manages theme-specific values for checkboxes. + * + * This class handles the adaptation between Cupertino and Material design for checkboxes, + * providing appropriate color styling for both design systems. + */ @OptIn(ExperimentalAdaptiveApi::class) @Stable +/** + * Checkbox component adaptive adaptation class. + * + * @see Adaptation + * @see CupertinoCheckBoxAdaptation + * @see MaterialCheckBoxAdaptation + */ private class CheckBoxAdaptation: Adaptation(){ @Composable @@ -143,5 +202,4 @@ private class CheckBoxAdaptation: Adaptation adaptiveComponent( @@ -34,6 +44,18 @@ fun adaptiveComponent( } } +/** + * Creates an adaptive component using the provided [adaptation] to customize both Material and Cupertino implementations. + * + * This function allows for more complex adaptations where the Material and Cupertino implementations need different + * customization parameters. It uses the provided [adaptation] to configure both implementations. + * + * @param adaptation The adaptation object that defines how to customize both Material and Cupertino implementations. + * @param material The Material implementation of the component. This is used when the current theme is [Theme.Material3]. + * @param cupertino The Cupertino implementation of the component. This is used when the current theme is [Theme.Cupertino]. + * @param adaptationScope The scope for customizing both Material and Cupertino implementations using [AdaptationScope]. + * @return The result of the selected implementation based on the current theme. + */ @Composable @ExperimentalAdaptiveApi fun adaptiveComponent( diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDatePicker.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDatePicker.kt index b2cbc682..4fc023a1 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDatePicker.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDatePicker.kt @@ -165,6 +165,14 @@ class MaterialDatePickerAdaptation internal constructor( private val DatePickerTitlePadding = PaddingValues(start = 24.dp, end = 12.dp, top = 16.dp) private val DatePickerHeadlinePadding = PaddingValues(start = 24.dp, end = 12.dp, bottom = 12.dp) +/** + * Adaptation class for date picker components that manages theme-specific values for date pickers. + * + * This class handles the adaptation between Cupertino and Material design for date pickers, + * providing appropriate styling and behavior for both design systems. + * + * @param state The DatePickerState used for managing the state of the date picker + */ @ExperimentalAdaptiveApi @OptIn(ExperimentalMaterial3Api::class) private class DatePickerAdaptation( diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDivider.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDivider.kt index 93fb54e1..12049d11 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDivider.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveDivider.kt @@ -34,6 +34,17 @@ import zone.ien.hig.CupertinoDividerDefaults import zone.ien.hig.CupertinoHorizontalDivider import zone.ien.hig.CupertinoVerticalDivider +/** + * Adaptive divider component that provides different divider styles based on Material Design and Cupertino themes. + * + * This function is deprecated and replaced by [AdaptiveHorizontalDivider] function. + * + * @param modifier Modifier to be applied to the element + * @param adaptation Custom configuration function for [DividerAdaptation] + * @see AdaptiveHorizontalDivider + * @see HorizontalDivider + * @see CupertinoHorizontalDivider + */ @Deprecated( replaceWith = ReplaceWith( "AdaptiveHorizontalDivider(modifier,adaptation)", @@ -48,6 +59,18 @@ fun AdaptiveDivider( ) = AdaptiveHorizontalDivider(modifier, adaptation) +/** + * Adaptive horizontal divider component that provides different divider styles based on Material Design and Cupertino themes. + * + * [AdaptiveHorizontalDivider] uses the Material Design [HorizontalDivider] component and the Cupertino [CupertinoHorizontalDivider] component to + * provide an appropriate horizontal divider for each operating system. + * + * @param modifier Modifier to be applied to the element + * @param adaptation Custom configuration function for [DividerAdaptation] + * @see AdaptiveWidget + * @see HorizontalDivider + * @see CupertinoHorizontalDivider + */ @Composable @ExperimentalAdaptiveApi fun AdaptiveHorizontalDivider( @@ -76,6 +99,18 @@ fun AdaptiveHorizontalDivider( ) } +/** + * Adaptive vertical divider component that provides different divider styles based on Material Design and Cupertino themes. + * + * [AdaptiveVerticalDivider] uses the Material Design [VerticalDivider] component and the Cupertino [CupertinoVerticalDivider] component to + * provide an appropriate vertical divider for each operating system. + * + * @param modifier Modifier to be applied to the element + * @param adaptation Custom configuration function for [DividerAdaptation] + * @see AdaptiveWidget + * @see VerticalDivider + * @see CupertinoVerticalDivider + */ @Composable @ExperimentalAdaptiveApi fun AdaptiveVerticalDivider( @@ -105,6 +140,12 @@ fun AdaptiveVerticalDivider( } @ExperimentalAdaptiveApi +/** + * Implementation class for divider component adaptation. + * + * @see Adaptation + * @see DividerAdaptation + */ private class DividerAdaptationScope: Adaptation() { @Composable @@ -132,11 +173,29 @@ private class DividerAdaptationScope: Adaptation Int, val onTabSelected: (Int) -> Unit, ) internal val LocalNavigationBarState = compositionLocalOf { null } +/** + * An adaptive navigation bar that adapts between Cupertino and Material design based on the platform. + * + * This composable provides a navigation bar that automatically switches between Cupertino (iOS) and Material (Android) + * design patterns based on the target platform. It supports tab navigation with appropriate styling for each platform. + * + * @param modifier optional [Modifier] for customizing the appearance and behavior + * @param selectedTabIndex lambda that returns the currently selected tab index + * @param onTabSelected callback to be invoked when a tab is selected + * @param tabsCount the total number of tabs + * @param adaptation lambda for customizing the adaptation behavior + * @param content composable content of the navigation bar items + */ @OptIn(ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -103,6 +126,81 @@ fun AdaptiveNavigationBar( } } +@OptIn(ExperimentalAdaptiveApi::class, ExperimentalCupertinoApi::class) +@Composable +fun AdaptiveNavigationBarNative( + modifier: Modifier = Modifier, + selectedTabIndex: () -> Int, + onTabSelected: (index: Int) -> Unit, + adaptation: AdaptationScope.() -> Unit = {}, + items: List +) { + CompositionLocalProvider( + LocalNavigationBarState provides NavigationBarState( + selectedTabIndex = selectedTabIndex, + onTabSelected = onTabSelected, + ) + ) { + AdaptiveWidget( + adaptation = remember { + NavigationBarAdaptation() + }, + adaptationScope = adaptation, + cupertino = { + CupertinoNavigationBarNative( + modifier = modifier, + colors = it.colors, + windowInsets = it.windowInsets, + backdrop = it.backdrop, + selectedTabIndex = selectedTabIndex, + onTabSelected = onTabSelected, + items = items + ) + }, + material = { + NavigationBar( + modifier = modifier, + containerColor = it.containerColor, + contentColor = it.contentColor, + tonalElevation = it.tonalElevation, + windowInsets = it.windowInsets, + content = { + items.forEachIndexed { index, item -> + val selected = index == selectedTabIndex() + NavigationBarItem( + selected = selected, + onClick = item.onClick, + icon = { + Icon( + painter = (if (selected) item.selectedIcon else null) ?: item.icon, + contentDescription = item.label, + ) + }, + label = { Text(text = item.label) }, + ) + } + } + ) + } + ) + } +} + +/** + * An adaptive navigation bar item that adapts between Cupertino and Material design based on the platform. + * + * This composable provides a navigation bar item that automatically switches between Cupertino (iOS) and Material (Android) + * design patterns based on the target platform. It's designed to be used within an [AdaptiveNavigationBar]. + * + * @param index the index of this item in the navigation bar + * @param onClick callback to be invoked when the item is clicked + * @param icon composable that draws the icon for the item + * @param modifier optional [Modifier] for customizing the appearance and behavior + * @param enabled whether the item is enabled + * @param label composable that draws the label for the item + * @param interactionSource the interaction source for the item + * @param adaptation lambda for customizing the adaptation behavior + */ @OptIn(ExperimentalCupertinoApi::class, ExperimentalAdaptiveApi::class) @Composable fun RowScope.AdaptiveNavigationBarItem( @@ -153,6 +251,16 @@ fun RowScope.AdaptiveNavigationBarItem( ) } +/** + * Material navigation bar adaptation. + * + * Container class for Material navigation bar adaptation properties. + * + * @param containerColor color for the container + * @param contentColor color for the content + * @param tonalElevation elevation for the navigation bar + * @param windowInsets window insets to be used for the navigation bar + */ class MaterialNavigationBarAdaptation internal constructor( containerColor: Color, contentColor: Color, @@ -165,6 +273,15 @@ class MaterialNavigationBarAdaptation internal constructor( var windowInsets: WindowInsets by mutableStateOf(windowInsets) } +/** + * Cupertino navigation bar adaptation. + * + * Container class for Cupertino navigation bar adaptation properties. + * + * @param colors colors to be used for the navigation bar + * @param windowInsets window insets to be used for the navigation bar + * @param backdrop backdrop to use for the navigation bar + */ @OptIn(ExperimentalCupertinoApi::class) class CupertinoNavigationBarAdaptation internal constructor( colors: CupertinoNavigationBarColors, diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveProgressIndicator.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveProgressIndicator.kt index d6f5783b..609fe2f4 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveProgressIndicator.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveProgressIndicator.kt @@ -119,6 +119,12 @@ class CupertinoCircularProgressIndicatorAdaptation internal constructor( var minAlpha: Float by mutableStateOf(minAlpha) } +/** + * Adaptation class for progress indicator components that manages theme-specific values for progress indicators. + * + * This class handles the adaptation between Cupertino and Material design for progress indicators, + * providing appropriate styling for both design systems. + */ @OptIn(ExperimentalAdaptiveApi::class) private class ProgressIndicatorAdaptation : Adaptation() { diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveScaffold.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveScaffold.kt index d0730d12..5bc92ca9 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveScaffold.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveScaffold.kt @@ -40,6 +40,23 @@ import zone.ien.hig.ExperimentalCupertinoApi import zone.ien.hig.FabPosition +/** + * An adaptive scaffold that adapts between Cupertino and Material design based on the platform. + * + * This composable provides a scaffold that automatically switches between Cupertino (iOS) and Material (Android) + * design patterns based on the target platform. The content of the scaffold adapts to the appropriate design + * guidelines and styles. + * + * @param modifier optional [Modifier] for customizing the appearance and behavior + * @param topBar composable for the top app bar + * @param bottomBar composable for the bottom app bar + * @param snackbarHost composable for the snackbar host + * @param floatingActionButton composable for the floating action button + * @param floatingActionButtonPosition determines the position of the floating action button + * @param contentWindowInsets the window insets to be used for the content + * @param adaptation lambda for customizing the adaptation behavior + * @param content composable content of the scaffold + */ @OptIn(ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -93,6 +110,24 @@ fun AdaptiveScaffold( ) } +/** + * An adaptive scaffold that adapts between Cupertino and Material design based on the platform. + * + * This composable provides a scaffold that automatically switches between Cupertino (iOS) and Material (Android) + * design patterns based on the target platform. The content of the scaffold adapts to the appropriate design + * guidelines and styles. + * + * @param modifier optional [Modifier] for customizing the appearance and behavior + * @param topBar composable for the top app bar + * @param bottomBar composable for the bottom app bar + * @param snackbarHost composable for the snackbar host + * @param floatingActionButton composable for the floating action button + * @param floatingActionButtonPosition determines the position of the floating action button + * @param containerColor color for the container + * @param contentColor color for the content + * @param contentWindowInsets the window insets to be used for the content + * @param content composable content of the scaffold + */ @OptIn(ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -153,15 +188,35 @@ fun AdaptiveScaffold( } @Stable +/** + * [Scaffold] component adaptive adaptation class that manages various Scaffold style properties. + * + * @param contentColor Color for the screen content + * @param containerColor Background color for the screen container + * @see Color + */ class ScaffoldAdaptation internal constructor( contentColor: Color, containerColor: Color ) { - internal var contentColor by mutableStateOf(contentColor) - internal var containerColor by mutableStateOf(containerColor) + var contentColor by mutableStateOf(contentColor) + var containerColor by mutableStateOf(containerColor) } + +/** + * Implementation of [Adaptation] for [ScaffoldAdaptation]. + * + * This class manages the adaptation between Cupertino and Material design for scaffolds, + * providing appropriate container and content colors for both design systems. + */ @OptIn(ExperimentalAdaptiveApi::class) @Stable +/** + * Implementation of [Adaptation] for [ScaffoldAdaptation]. + * + * This class manages the adaptation between Cupertino and Material design for scaffolds, + * providing appropriate container and content colors for both design systems. + */ private class ScaffoldAdaptationImpl : Adaptation() { diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSlider.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSlider.kt index fb52eca5..283e8f69 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSlider.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSlider.kt @@ -16,8 +16,6 @@ * limitations under the License. */ - - package zone.ien.hig.adaptive import androidx.compose.foundation.interaction.Interaction @@ -42,10 +40,11 @@ import zone.ien.hig.CupertinoRangeSlider import zone.ien.hig.CupertinoSliderColors /** - * Sliders allow users to make selections from a range of values. + * An adaptive slider that adapts between Cupertino and Material design based on the platform. * - * Sliders reflect a range of values along a bar, from which users may select a single value. - * They are ideal for adjusting settings such as volume, brightness, or applying image filters. + * Sliders allow users to make selections from a range of values. + * This composable automatically switches between Cupertino (iOS) and Material (Android) design patterns + * based on the current theme. It supports both single value and range sliders. * * @param value current value of the slider. If outside of [valueRange] provided, value will be * coerced to this range. @@ -119,10 +118,11 @@ fun AdaptiveSlider( } /** - * Sliders allow users to make selections from a range of values. + * An adaptive range slider that adapts between Cupertino and Material design based on the platform. * - * Sliders reflect a range of values along a bar, from which users may select a single value. - * They are ideal for adjusting settings such as volume, brightness, or applying image filters. + * Sliders allow users to make selections from a range of values. + * This composable automatically switches between Cupertino (iOS) and Material (Android) design patterns + * based on the current theme. It supports both single value and range sliders. * * @param value current values of the RangeSlider. If either value is outside of [valueRange] * provided, it will be coerced to this range. @@ -187,6 +187,16 @@ fun AdaptiveRangeSlider( ) } +/** + * Cupertino slider adaptation. + * + * Container class for Cupertino slider adaptation properties. + * + * @param colors the colors to be used for the slider + * @param showStepIndicator whether to show step indicators + * @param backdrop backdrop to use for the slider + * @param visibilityThreshold threshold for visibility + */ @Stable class CupertinoSliderAdaptation internal constructor( colors: CupertinoSliderColors, @@ -200,6 +210,13 @@ class CupertinoSliderAdaptation internal constructor( var visibilityThreshold: Float by mutableStateOf(visibilityThreshold) } +/** + * Material slider adaptation. + * + * Container class for Material slider adaptation properties. + * + * @param colors the colors to be used for the slider + */ @Stable class MaterialSliderAdaptation internal constructor( colors: SliderColors, @@ -207,6 +224,13 @@ class MaterialSliderAdaptation internal constructor( var colors: SliderColors by mutableStateOf(colors) } +/** + * Slider adaptation implementation. + * + * Implementation of [Adaptation] for slider adaptation. + * + * @param steps The number of discrete steps for the slider, or 0 for continuous + */ @OptIn(ExperimentalAdaptiveApi::class) @Stable private class SliderAdaptation( diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSurface.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSurface.kt index d515a741..5e2cb537 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSurface.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSurface.kt @@ -32,6 +32,19 @@ import zone.ien.hig.LocalContentColor import zone.ien.hig.CupertinoSurface import zone.ien.hig.theme.CupertinoTheme +/** + * A composable that renders a surface that adapts its appearance based on the current theme. + * + * This composable provides a consistent surface implementation across both Material and Cupertino design systems. + * It automatically chooses the appropriate surface implementation based on the current theme set by [AdaptiveTheme]. + * + * @param modifier Modifier to be applied to the surface. + * @param shape The shape of the surface. Defaults to [RectangleShape]. + * @param color The background color of the surface. Defaults to [Color.Unspecified]. + * @param contentColor The color of the content within the surface. Defaults to [Color.Unspecified]. + * @param shadowElevation The elevation of the surface's shadow. Defaults to 0.dp. + * @param content The content to be displayed within the surface. + */ @ExperimentalAdaptiveApi @Composable fun AdaptiveSurface( diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSwitch.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSwitch.kt index f956745c..2a22800c 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSwitch.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveSwitch.kt @@ -16,8 +16,6 @@ * limitations under the License. */ - - package zone.ien.hig.adaptive import androidx.compose.foundation.interaction.Interaction @@ -40,20 +38,20 @@ import zone.ien.hig.CupertinoSwitchColors import zone.ien.hig.CupertinoSwitchDefaults /** - * Adaptive Switch depending on current [Theme]. + * An adaptive switch that changes its appearance based on the current [Theme]. * * Switches toggle the state of a single item on or off. * - * @param checked whether or not this switch is checked - * @param onCheckedChange called when this switch is clicked. If `null`, then this switch will not - * be interactable, unless something else handles its input events and updates its state. + * @param checked whether this switch is checked + * @param onCheckedChange called when the switch is clicked. If null, the switch is not interactable. * @param modifier the [Modifier] to be applied to this switch - * @param thumbContent content that will be drawn inside the thumb - * @param enabled controls the enabled state of this switch. When `false`, this component will not - * respond to user input, and it will appear visually disabled and disabled to accessibility - * services. + * @param thumbContent content to be drawn inside the thumb + * @param enabled controls the enabled state of this switch * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s - * @param adaptation configuration block for theme-dependent properties for this switch + * @param adaptation configuration block for theme-dependent properties + * @see AdaptiveWidget + * @see CupertinoSwitch + * @see Switch */ @ExperimentalAdaptiveApi @Composable @@ -98,6 +96,16 @@ fun AdaptiveSwitch( ) } +/** + * Cupertino switch adaptation. + * + * Adaptive adaptation class for Cupertino switch that manages various switch properties. + * + * @param colors [CupertinoSwitchColors] - Switch color configuration + * @param backdrop [Backdrop] - Switch backdrop + * @see CupertinoSwitchColors + * @see Backdrop + */ @Stable class CupertinoSwitchAdaptation internal constructor( colors: CupertinoSwitchColors, @@ -107,15 +115,41 @@ class CupertinoSwitchAdaptation internal constructor( var backdrop by mutableStateOf(backdrop) } +/** + * Material switch adaptation. + * + * Container class for Material switch adaptation properties. + * + * @param colors the colors to be used for the switch + */ @Stable +/** + * Material switch adaptation class that manages various switch properties. + * + * @param colors [SwitchColors] - Switch color configuration + * @see SwitchColors + * @see SwitchDefaults + */ class MaterialSwitchAdaptation internal constructor( colors: SwitchColors, ) { var colors by mutableStateOf(colors) } +/** + * Switch adaptation implementation. + * + * Implementation of [Adaptation] for switch adaptation. + */ @OptIn(ExperimentalAdaptiveApi::class) @Stable +/** + * Class that provides adaptive adaptation for switch type. + * + * @see Adaptation + * @see CupertinoSwitchAdaptation + * @see MaterialSwitchAdaptation + */ private class SwitchAdaptation: Adaptation() { @Composable override fun rememberCupertinoAdaptation(): CupertinoSwitchAdaptation { @@ -138,4 +172,4 @@ private class SwitchAdaptation: Adaptation [CupertinoText] and [Icon] <-> [CupertinoIcon] behave identically + * This theme provides a way to seamlessly use Material and Cupertino widgets together in the same application + * by adapting based on the target theme. It also makes [Text] and [CupertinoText], as well as [Icon] and [CupertinoIcon] + * behave identically in both design systems. * - * Current theme target can be accessed inside the [content] using [currentTheme] property + * The current theme target can be accessed inside the [content] using [currentTheme] property. * - * @param target theme for adaptive widgets - * @param material [MaterialTheme] specification - * @param cupertino [CupertinoTheme] specification - * @param content themed content - * */ + * @param target theme for adaptive widgets. Defaults to [Theme.Cupertino] for iOS and [Theme.Material3] for other platforms. + * @param material [MaterialThemeSpec] specification. + * @param cupertino [CupertinoThemeSpec] specification. + * @param content themed content. + */ @ExperimentalAdaptiveApi @Composable fun AdaptiveTheme( @@ -123,10 +129,10 @@ fun AdaptiveTheme( * @param material [MaterialTheme] specification. NOTE: You must use lambda parameter as a content * @param cupertino [CupertinoTheme] specification. NOTE: You must use lambda parameter as a content * @param content themed content - * */ + */ @ExperimentalAdaptiveApi @Deprecated( - message = "Use variant with theme specs instead of lambdas", + message = "Use the version that takes theme specifications as parameters", replaceWith = ReplaceWith( "AdaptiveTheme(target, MaterialThemeSpec.Default(), CupertinoThemeSpec.Default(), content)", "import zone.ien.hig.adaptive.MaterialThemeSpec", @@ -165,6 +171,15 @@ fun AdaptiveTheme( } } +/** + * Material theme specification. + * + * This class holds the specification for Material themes including color scheme, shapes, and typography. + * + * @param colorScheme the color scheme to use for Material design + * @param shapes the shapes to use for Material design + * @param typography the typography to use for Material design + */ @Immutable @ExperimentalAdaptiveApi class MaterialThemeSpec( @@ -172,6 +187,14 @@ class MaterialThemeSpec( val shapes: MaterialShapes = MaterialShapes(), val typography: MaterialTypography = MaterialTypography(), ) { + /** + * Creates a copy of this MaterialThemeSpec with the specified values replaced. + * + * @param colorScheme the new color scheme + * @param shapes the new shapes + * @param typography the new typography + * @return a copy of this MaterialThemeSpec + */ fun copy( colorScheme: MaterialColorScheme = this.colorScheme, shapes: MaterialShapes = this.shapes, @@ -187,6 +210,14 @@ class MaterialThemeSpec( } companion object { + /** + * Creates a default Material theme specification based on the current MaterialTheme. + * + * @param colorScheme the color scheme to use for Material design + * @param shapes the shapes to use for Material design + * @param typography the typography to use for Material design + * @return a MaterialThemeSpec with the default values + */ @Composable fun Default( colorScheme: MaterialColorScheme = MaterialTheme.colorScheme, @@ -196,13 +227,37 @@ class MaterialThemeSpec( } } +/** + * Cupertino theme specification. + * + * This class holds the specification for Cupertino themes including color scheme, shapes, and typography. + * + * @param colorScheme the color scheme to use for Cupertino design + * @param shapes the shapes to use for Cupertino design + * @param typography the typography to use for Cupertino design + */ @Immutable @ExperimentalAdaptiveApi +/** + * Cupertino theme specification class that defines color, shapes, and typography for Cupertino theme. + * + * @param colorScheme [CupertinoColorScheme] - Theme color scheme + * @param shapes [CupertinoShapes] - Theme shapes + * @param typography [CupertinoTypography] - Theme typography + */ class CupertinoThemeSpec( val colorScheme: CupertinoColorScheme = cupertinoLightColorScheme(), val shapes: CupertinoShapes = CupertinoShapes(), val typography: CupertinoTypography = CupertinoTypography() ) { + /** + * Creates a copy of this CupertinoThemeSpec with the specified values replaced. + * + * @param colorScheme the new color scheme + * @param shapes the new shapes + * @param typography the new typography + * @return a copy of this CupertinoThemeSpec + */ fun copy( colorScheme: CupertinoColorScheme = this.colorScheme, shapes: CupertinoShapes = this.shapes, @@ -217,6 +272,14 @@ class CupertinoThemeSpec( return "CupertinoThemeSpec(colorScheme=$colorScheme, shapes=$shapes, typography=$typography)" } companion object { + /** + * Creates a default Cupertino theme specification based on the current CupertinoTheme. + * + * @param colorScheme the color scheme to use for Cupertino design + * @param shapes the shapes to use for Cupertino design + * @param typography the typography to use for Cupertino design + * @return a CupertinoThemeSpec with the default values + */ @Composable fun Default( colorScheme: CupertinoColorScheme = CupertinoTheme.colorScheme, @@ -228,8 +291,8 @@ class CupertinoThemeSpec( /** - * Theme declared as a target in [AdaptiveTheme] - * */ + * The theme declared as a target in [AdaptiveTheme], allowing you to check the current theme. + */ @ExperimentalAdaptiveApi val currentTheme: Theme @Composable diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveTopAppBar.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveTopAppBar.kt index 883c099a..78ba17fa 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveTopAppBar.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/AdaptiveTopAppBar.kt @@ -16,8 +16,6 @@ * limitations under the License. */ - - package zone.ien.hig.adaptive import androidx.compose.foundation.layout.RowScope @@ -43,6 +41,19 @@ import zone.ien.hig.CupertinoTopAppBarColors import zone.ien.hig.CupertinoTopAppBarDefaults import zone.ien.hig.ExperimentalCupertinoApi +/** + * An adaptive top app bar that adapts between Cupertino and Material design based on the platform. + * + * This composable provides a top app bar that automatically switches between Cupertino (iOS) and Material (Android) + * design patterns based on the target platform. The content adapts to appropriate design guidelines and styles. + * + * @param title composable for the title + * @param modifier optional [Modifier] for customizing the appearance and behavior + * @param navigationIcon composable for the navigation icon + * @param actions composable for the actions + * @param windowInsets the window insets to be used for the content + * @param adaptation lambda for customizing the adaptation behavior + */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalCupertinoApi::class) @ExperimentalAdaptiveApi @Composable @@ -86,6 +97,20 @@ fun AdaptiveTopAppBar( ) } +/** + * A single row top app bar. + * + * This private composable renders either a CenterAlignedTopAppBar or a TopAppBar based on the isCenterAligned parameter. + * + * @param title composable for the title + * @param isCenterAligned whether the title should be centered + * @param colors the colors to be used for the top app bar + * @param modifier optional [Modifier] for customizing the appearance and behavior + * @param navigationIcon composable for the navigation icon + * @param actions composable for the actions + * @param windowInsets the window insets to be used for the content + * @param scrollBehavior the scroll behavior for the top app bar + */ @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SingleRowTopAppBar( @@ -121,8 +146,27 @@ private fun SingleRowTopAppBar( } } +/** + * Material top app bar adaptation. + * + * Container class for Material top app bar adaptation properties. + * + * @param colors the colors to be used for the top app bar + * @param isCenterAligned whether the title should be centered + * @param scrollBehavior the scroll behavior for the top app bar + */ @Stable @OptIn(ExperimentalMaterial3Api::class) +/** + * Adaptive adaptation class for Material top app bar that manages various top app bar properties. + * + * @param colors [TopAppBarColors] - Top app bar color configuration + * @param isCenterAligned Whether the title is centered + * @param scrollBehavior Scroll behavior configuration + * @see TopAppBarColors + * @see TopAppBarDefaults + * @see TopAppBarScrollBehavior + */ class MaterialTopAppBarAdaptation internal constructor( colors: TopAppBarColors, isCenterAligned: Boolean = false, @@ -133,7 +177,23 @@ class MaterialTopAppBarAdaptation internal constructor( var scrollBehavior: TopAppBarScrollBehavior? by mutableStateOf(scrollBehavior) } +/** + * Cupertino top app bar adaptation. + * + * Container class for Cupertino top app bar adaptation properties. + * + * @param colors the colors to be used for the top app bar + * @param backdrop backdrop to use for the bar + */ @Stable +/** + * Adaptive adaptation class for Cupertino top app bar that manages various top app bar properties. + * + * @param colors [CupertinoTopAppBarColors] - Top app bar color configuration + * @param backdrop [LayerBackdrop] - Top app bar layer backdrop + * @see CupertinoTopAppBarColors + * @see LayerBackdrop + */ class CupertinoTopAppBarAdaptation internal constructor( colors: CupertinoTopAppBarColors, backdrop: LayerBackdrop @@ -142,8 +202,20 @@ class CupertinoTopAppBarAdaptation internal constructor( var backdrop: LayerBackdrop by mutableStateOf(backdrop) } +/** + * Top app bar adaptation. + * + * Implementation of [Adaptation] for top app bar adaptation. + */ @OptIn(ExperimentalAdaptiveApi::class) @Stable +/** + * Class that provides adaptive adaptation based on top app bar type. + * + * @see Adaptation + * @see CupertinoTopAppBarAdaptation + * @see MaterialTopAppBarAdaptation + */ private class TopAppBarAdaptation: Adaptation() { @Composable override fun rememberCupertinoAdaptation(): CupertinoTopAppBarAdaptation { @@ -169,4 +241,4 @@ private class TopAppBarAdaptation: Adaptation AdaptiveWidget( diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Shapes.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Shapes.kt index 24ab0119..22263cc7 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Shapes.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Shapes.kt @@ -28,6 +28,24 @@ import com.kyant.shapes.RoundedRectangle import com.kyant.shapes.UnevenRoundedRectangle import zone.ien.hig.theme.Shapes as CupertinoShapes import androidx.compose.material3.Shapes as MaterialShapes + +/** + * A class that holds the shapes for different design systems. + * + * This class provides a consistent set of shapes that can be used across both Material and Cupertino design systems. + * It includes shapes for different sizes and also provides HIG-specific shapes. + * + * @param extraSmall The extra small shape, typically used for small components. + * @param small The small shape, typically used for small components. + * @param medium The medium shape, typically used for medium components. + * @param large The large shape, typically used for large components. + * @param extraLarge The extra large shape, typically used for large components. + * @param higExtraSmall The extra small HIG shape, typically used for small components. + * @param higSmall The small HIG shape, typically used for small components. + * @param higMedium The medium HIG shape, typically used for medium components. + * @param higLarge The large HIG shape, typically used for large components. + * @param higExtraLarge The extra large HIG shape, typically used for large components. + */ @Immutable class Shapes( val extraSmall: CornerBasedShape = RoundedCornerShape(4.dp), @@ -41,6 +59,21 @@ class Shapes( val higLarge: RoundedRectangle = RoundedRectangle(16.dp), val higExtraLarge: RoundedRectangle = RoundedRectangle(24.dp) ) { + /** + * Creates a copy of this Shapes object with the specified values replaced. + * + * @param extraSmall The extra small shape, typically used for small components. + * @param small The small shape, typically used for small components. + * @param medium The medium shape, typically used for medium components. + * @param large The large shape, typically used for large components. + * @param extraLarge The extra large shape, typically used for large components. + * @param higExtraSmall The extra small HIG shape, typically used for small components. + * @param higSmall The small HIG shape, typically used for small components. + * @param higMedium The medium HIG shape, typically used for medium components. + * @param higLarge The large HIG shape, typically used for large components. + * @param higExtraLarge The extra large HIG shape, typically used for large components. + * @return A new Shapes object with the specified values replaced. + */ fun copy( extraSmall: CornerBasedShape = this.extraSmall, small: CornerBasedShape = this.small, diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Util.adaptive.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Util.adaptive.kt index 25bf2ea3..2bedc886 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Util.adaptive.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/Util.adaptive.kt @@ -23,4 +23,13 @@ package zone.ien.hig.adaptive import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.isSpecified +/** + * Returns this Color if it is specified, otherwise returns the result of calling [block]. + * + * This utility function provides a clean way to handle optional Colors that may be null or unspecified. + * It's commonly used in adaptive components where default colors need to be provided. + * + * @param block The block that provides a default Color value if this Color is null or unspecified. + * @return This Color if it is specified, otherwise the result of [block]. + */ inline fun Color?.takeOrElse(block: () -> Color): Color = if (this != null && isSpecified) this else block() diff --git a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/icons/AdaptiveIcons.kt b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/icons/AdaptiveIcons.kt index 8aee685d..c921355f 100644 --- a/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/icons/AdaptiveIcons.kt +++ b/hig-adaptive/src/commonMain/kotlin/zone/ien/hig/adaptive/icons/AdaptiveIcons.kt @@ -16,8 +16,6 @@ * limitations under the License. */ - - package zone.ien.hig.adaptive.icons import androidx.compose.material.icons.Icons @@ -76,6 +74,12 @@ import zone.ien.hig.icons.outlined.WrenchAndScrewdriver import zone.ien.hig.icons.outlined.Xmark import zone.ien.hig.icons.outlined.XmarkCircle +/** + * Provides adaptive icons for both Cupertino and Material design. + * + * This object contains a collection of adaptive icons that switch between Material and Cupertino design based on + * the current theme. Icons are defined for both filled and outlined variants. + */ object AdaptiveIcons { object Filled @@ -85,7 +89,11 @@ object AdaptiveIcons { * Adaptive vector icon based on current theme. * * To retrieve system images on iOS use [painter] - * */ + * + * @param material composable that returns the Material design icon + * @param cupertino composable that returns the Cupertino icon + * @return the appropriate icon based on the current theme + */ @Composable fun vector( material: @Composable () -> ImageVector, @@ -101,7 +109,11 @@ object AdaptiveIcons { * * - For [Theme.Material3] and [Theme.Cupertino] on non-iOS it return painter from [material] vector. * - For [Theme.Cupertino] on iOS it returns SF Symbol with system name [cupertino] - * */ + * + * @param material composable that returns the Material design icon + * @param cupertino composable that returns the Cupertino system name + * @return the appropriate painter based on the current theme + */ @Composable fun painter( material: @Composable () -> ImageVector, diff --git a/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.ios.kt b/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.ios.kt index 08a297d3..1bf79302 100644 --- a/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.ios.kt +++ b/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.ios.kt @@ -18,5 +18,15 @@ package zone.ien.hig.adaptive +/** + * Platform-specific default theme for iOS platforms. + * + * This file defines the default theme for iOS platforms to use Cupertino design. + * It ensures consistent theming across iOS applications using this library. + * + * On iOS platforms, the default theme is set to [Theme.Cupertino] to provide a native Cupertino experience. + * + * @return The default theme for iOS platforms, which is [Theme.Cupertino]. + */ internal actual val DefaultTheme: Theme get() = Theme.Cupertino \ No newline at end of file diff --git a/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt b/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt index 2ad3b1ff..0c9359be 100644 --- a/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt +++ b/hig-adaptive/src/iosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt @@ -25,5 +25,14 @@ import androidx.compose.ui.graphics.painter.Painter import zone.ien.hig.icons.CupertinoIcons import zone.ien.hig.named +/** + * Platform-specific implementation for retrieving system icons on iOS platforms. + * + * This function provides a way to retrieve system icons for iOS platforms. + * It delegates to the Cupertino icon implementation using the [CupertinoIcons.named] function. + * + * @param name The name of the system icon to retrieve. + * @return A [Painter] for the specified system icon, or null if the icon is not found. + */ @Composable internal actual fun systemImage(name: String): Painter? = CupertinoIcons.named(name) diff --git a/hig-adaptive/src/nonAndroidMain/kotlin/zone/ien/hig/adaptive/SystemTheme.kt b/hig-adaptive/src/nonAndroidMain/kotlin/zone/ien/hig/adaptive/SystemTheme.kt index b4575b0e..921d2cd9 100644 --- a/hig-adaptive/src/nonAndroidMain/kotlin/zone/ien/hig/adaptive/SystemTheme.kt +++ b/hig-adaptive/src/nonAndroidMain/kotlin/zone/ien/hig/adaptive/SystemTheme.kt @@ -19,6 +19,15 @@ package zone.ien.hig.adaptive import androidx.compose.material3.ColorScheme import androidx.compose.runtime.Composable +/** + * Returns the system Material Color Scheme. + * + * This function returns the system color scheme used on the Android platform, + * and currently has an implementation that returns null. + * + * @param dark whether dark mode is enabled + * @return the system color scheme or null + */ @Composable internal actual fun systemMaterialColorScheme(dark: Boolean): ColorScheme? { val r: ColorScheme? = null // https://github.com/JetBrains/compose-multiplatform/issues/3900 diff --git a/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.nonIos.kt b/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.nonIos.kt index aedd1e03..c2ec058b 100644 --- a/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.nonIos.kt +++ b/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/DefaultTheme.nonIos.kt @@ -18,5 +18,11 @@ package zone.ien.hig.adaptive +/** + * Platform-specific default theme for non-iOS platforms. + * + * This file defines the default theme for non-iOS platforms (such as Android) to use Material 3 design. + * It ensures consistent theming across Android applications using this library. + */ internal actual val DefaultTheme: Theme get() = Theme.Material3 \ No newline at end of file diff --git a/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt b/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt index c7ebc344..8a3c6467 100644 --- a/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt +++ b/hig-adaptive/src/nonIosMain/kotlin/zone/ien/hig/adaptive/icons/SystemImage.kt @@ -21,5 +21,14 @@ package zone.ien.hig.adaptive.icons import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.painter.Painter +/** + * Returns a system image for non-iOS platforms. + * + * This function is used on platforms other than iOS (e.g., Android, Desktop, etc.) + * and always returns null to prevent the use of system images. + * + * @param name the name of the system image + * @return null (system images are not supported) + */ @Composable internal actual fun systemImage(name: String): Painter? = null diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/Accessibility.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/Accessibility.kt index 581d7277..98f3f4e0 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/Accessibility.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/Accessibility.kt @@ -16,22 +16,7 @@ * limitations under the License. */ -@file:Suppress("unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", "unused", - "unused", "unused", "unused", "unused", "unused", "unused" -) +@file:Suppress("unused") package zone.ien.hig @@ -44,23 +29,37 @@ import zone.ien.hig.theme.isDark /** * This object provides access to native accessibility preferences - * */ + * + * It allows for checking system accessibility settings such as high contrast mode and + * reduced transparency. + */ object Accessibility +/** + * Checks if high contrast mode is enabled in the system. + */ expect val Accessibility.isHighContrastEnabled: Boolean +/** + * Checks if reduced transparency is enabled in the system. + */ expect val Accessibility.isReduceTransparencyEnabled: Boolean /** * Adjust color contrast if corresponding OS accessibility system preference is enabled - * */ + * + * @return the accessible color version based on system settings + */ val Color.accessible: Color @Composable get() = accessible(isDark()) /** * Adjust color contrast if corresponding accessibility system preference is enabled - * */ + * + * @param isDark whether the theme is dark or light + * @return the accessible color version based on system settings + */ fun Color.accessible(isDark: Boolean): Color = if (Accessibility.isHighContrastEnabled) { lerp(this, if (isDark) CupertinoColors.White else Color.Black, .2f) diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoHapticFeedback.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoHapticFeedback.kt index ab5e14bb..8b6af0fe 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoHapticFeedback.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoHapticFeedback.kt @@ -24,24 +24,56 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.hapticfeedback.HapticFeedback import androidx.compose.ui.hapticfeedback.HapticFeedbackType +/** + * Creates a haptic feedback instance for Cupertino styling. + * + * @return a [HapticFeedback] instance for Cupertino styling + */ @Composable expect fun rememberCupertinoHapticFeedback(): HapticFeedback /** - * This haptic feedback types work only on iOS. They are available for public usage in iosMain as + * Cupertino haptic feedback types that work only on iOS. + * + * These haptic feedback types are available for public usage in iosMain as * extension properties of [HapticFeedbackType.Companion] - * */ + */ @InternalCupertinoApi object CupertinoHapticFeedback { + /** + * Selection changed haptic feedback type. + */ val SelectionChanged: HapticFeedbackType = HapticFeedbackType(1001) - + /** + * Success haptic feedback type. + */ val Success: HapticFeedbackType = HapticFeedbackType(2001) + /** + * Warning haptic feedback type. + */ val Warning: HapticFeedbackType = HapticFeedbackType(2002) + /** + * Error haptic feedback type. + */ val Error: HapticFeedbackType = HapticFeedbackType(2003) - + /** + * Impact light haptic feedback type. + */ val ImpactLight: HapticFeedbackType = HapticFeedbackType(3001) + /** + * Impact medium haptic feedback type. + */ val ImpactMedium: HapticFeedbackType = HapticFeedbackType(3002) + /** + * Impact heavy haptic feedback type. + */ val ImpactHeavy: HapticFeedbackType = HapticFeedbackType(3003) + /** + * Impact rigid haptic feedback type. + */ val ImpactRigid: HapticFeedbackType = HapticFeedbackType(3004) + /** + * Impact soft haptic feedback type. + */ val ImpactSoft: HapticFeedbackType = HapticFeedbackType(3005) } diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoTween.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoTween.kt index 78007d03..94c8a969 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoTween.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/CupertinoTween.kt @@ -30,7 +30,12 @@ import androidx.compose.animation.core.tween * * Default values are used for iOS view transitions such as * UINavigationController, UIAlertController - * */ + * + * @param durationMillis the duration of the animation in milliseconds + * @param delayMillis the delay before the animation starts in milliseconds + * @param easing the easing function to use for the animation + * @return a TweenSpec for the Cupertino transition + */ fun cupertinoTween( durationMillis: Int = CupertinoTransitionDuration, delayMillis: Int = 0, @@ -42,5 +47,11 @@ fun cupertinoTween( delayMillis = delayMillis, ) +/** + * The easing function used for Cupertino transitions. + */ val CupertinoEasing = CubicBezierEasing(0.2833f, 0.99f, 0.31833f, 0.99f) +/** + * The default transition duration for Cupertino transitions. + */ private val CupertinoTransitionDuration = 400 diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/SystemBarAppearance.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/SystemBarAppearance.kt index ecc653a9..961d9e07 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/SystemBarAppearance.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/SystemBarAppearance.kt @@ -20,6 +20,11 @@ package zone.ien.hig import androidx.compose.runtime.Composable +/** + * Applies the system bar appearance to the current composable. + * + * @param dark whether the status bar should be dark (true) or light (false) + */ @Composable @InternalCupertinoApi expect fun SystemBarAppearance(dark: Boolean) diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt index 3c3f621c..6b92a8b2 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt @@ -1,3 +1,6 @@ package zone.ien.hig.section +/** + * A scope for section components in the Cupertino design system. + */ interface SectionScope \ No newline at end of file diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/ColorScheme.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/ColorScheme.kt index 77765cd5..94e86bbf 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/ColorScheme.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/ColorScheme.kt @@ -29,6 +29,32 @@ import zone.ien.hig.Accessibility import zone.ien.hig.InternalCupertinoApi import zone.ien.hig.isHighContrastEnabled +/** + * A color scheme for the Cupertino design system. + * + * @param isDark whether the theme is dark or light + * @param accent the accent color used for interactive elements + * @param label the primary label color + * @param secondaryLabel the secondary label color + * @param tertiaryLabel the tertiary label color + * @param quaternaryLabel the quaternary label color + * @param link the link color + * @param placeholderText the placeholder text color + * @param separator the separator color + * @param opaqueSeparator the opaque separator color + * @param systemBackground the system background color + * @param secondarySystemBackground the secondary system background color + * @param tertiarySystemBackground the tertiary system background color + * @param systemGroupedBackground the system grouped background color + * @param secondarySystemGroupedBackground the secondary system grouped background color + * @param tertiarySystemGroupedBackground the tertiary system grouped background color + * @param systemFill the system fill color + * @param secondarySystemFill the secondary system fill color + * @param tertiarySystemFill the tertiary system fill color + * @param quaternarySystemFill the quaternary system fill color + * @param lightText the light text color + * @param darkText the dark text color + */ @Immutable class ColorScheme internal constructor( val isDark: Boolean, @@ -54,6 +80,31 @@ class ColorScheme internal constructor( val lightText: Color, val darkText: Color ) { + /** + * Creates a copy of this ColorScheme with the specified values replaced. + * + * @param accent the new accent color + * @param label the new label color + * @param secondaryLabel the new secondary label color + * @param tertiaryLabel the new tertiary label color + * @param quaternaryLabel the new quaternary label color + * @param link the new link color + * @param placeholderText the new placeholder text color + * @param separator the new separator color + * @param opaqueSeparator the new opaque separator color + * @param systemBackground the new system background color + * @param secondarySystemBackground the new secondary system background color + * @param tertiarySystemBackground the new tertiary system background color + * @param systemGroupedBackground the new system grouped background color + * @param secondarySystemGroupedBackground the new secondary system grouped background color + * @param tertiarySystemGroupedBackground the new tertiary system grouped background color + * @param systemFill the new system fill color + * @param secondarySystemFill the new secondary system fill color + * @param tertiarySystemFill the new tertiary system fill color + * @param quaternarySystemFill the new quaternary system fill color + * @param lightText the new light text color + * @param darkText the new dark text color + */ fun copy( accent: Color = this.accent, label: Color = this.label, diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Shapes.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Shapes.kt index 8ecbb0fb..6fc7dcdd 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Shapes.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Shapes.kt @@ -30,6 +30,15 @@ import com.kyant.shapes.RoundedRectangle import com.kyant.shapes.UnevenRoundedRectangle import zone.ien.hig.InternalCupertinoApi +/** + * Shapes for the Cupertino design system. + * + * @param extraSmall the extra small corner shape + * @param small the small corner shape + * @param medium the medium corner shape + * @param large the large corner shape + * @param extraLarge the extra large corner shape + */ @Stable class Shapes( val extraSmall: RoundedRectangle = ShapeDefaults.ExtraSmall, @@ -38,6 +47,15 @@ class Shapes( val large: RoundedRectangle = ShapeDefaults.Large, val extraLarge: RoundedRectangle = ShapeDefaults.ExtraLarge, ) { + /** + * Creates a copy of this Shapes with the specified values replaced. + * + * @param extraSmall the new extra small corner shape + * @param small the new small corner shape + * @param medium the new medium corner shape + * @param large the new large corner shape + * @param extraLarge the new extra large corner shape + */ fun copy( extraSmall: RoundedRectangle = this.extraSmall, small: RoundedRectangle = this.small, @@ -56,6 +74,9 @@ class Shapes( @InternalCupertinoApi val LocalShapes = staticCompositionLocalOf { Shapes() } +/** + * Default shapes for the Cupertino design system. + */ @Immutable object ShapeDefaults { /** Extra small sized corner shape */ diff --git a/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Typography.kt b/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Typography.kt index af53d0ce..89046cf5 100644 --- a/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Typography.kt +++ b/hig-core/src/commonMain/kotlin/zone/ien/hig/theme/Typography.kt @@ -27,6 +27,21 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import zone.ien.hig.InternalCupertinoApi +/** + * Typography for the Cupertino design system. + * + * @param largeTitle the large title text style + * @param title1 the title 1 text style + * @param title2 the title 2 text style + * @param title3 the title 3 text style + * @param headline the headline text style + * @param body the body text style + * @param callout the callout text style + * @param subhead the subhead text style + * @param footnote the footnote text style + * @param caption1 the caption 1 text style + * @param caption2 the caption 2 text style + */ @Immutable class Typography( val largeTitle: TextStyle = @@ -86,6 +101,21 @@ class Typography( lineHeight = 13.sp, ), ) { + /** + * Creates a copy of this Typography with the specified values replaced. + * + * @param largeTitle the new large title text style + * @param title1 the new title 1 text style + * @param title2 the new title 2 text style + * @param title3 the new title 3 text style + * @param headline the new headline text style + * @param body the new body text style + * @param callout the new callout text style + * @param subhead the new subhead text style + * @param footnote the new footnote text style + * @param caption1 the new caption 1 text style + * @param caption2 the new caption 2 text style + */ fun copy( largeTitle: TextStyle = this.largeTitle, title1: TextStyle = this.title1, diff --git a/hig-core/src/iosMain/kotlin/zone/ien/hig/SystemBarAppearance.ios.kt b/hig-core/src/iosMain/kotlin/zone/ien/hig/SystemBarAppearance.ios.kt index b34505a5..5c9565c7 100644 --- a/hig-core/src/iosMain/kotlin/zone/ien/hig/SystemBarAppearance.ios.kt +++ b/hig-core/src/iosMain/kotlin/zone/ien/hig/SystemBarAppearance.ios.kt @@ -20,7 +20,7 @@ package zone.ien.hig import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.ui.interop.LocalUIViewController +import androidx.compose.ui.uikit.LocalUIViewController import org.jetbrains.skiko.OS import org.jetbrains.skiko.OSVersion import org.jetbrains.skiko.available diff --git a/hig-native/build.gradle.kts b/hig-native/build.gradle.kts index e76ce420..f3c45144 100644 --- a/hig-native/build.gradle.kts +++ b/hig-native/build.gradle.kts @@ -32,7 +32,9 @@ kotlin { implementation(projects.hig) implementation(libs.compose.runtime) implementation(libs.compose.foundation) + implementation(libs.compose.material3) implementation(libs.capsule) + implementation(libs.backdrop) } } } diff --git a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.kt b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.kt index abf49257..8e30c26b 100644 --- a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.kt +++ b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.kt @@ -26,6 +26,15 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import zone.ien.hig.theme.CupertinoTheme +/** + * A date picker that is implemented with native platform widgets on iOS and Compose widgets on + * non-iOS platforms. + * + * @param state the state of the date picker + * @param modifier the [Modifier] to be applied to this date picker + * @param style the date picker style, either wheel or pager + * @param containerColor the color to apply to the background of the date picker + */ @Composable @ExperimentalCupertinoApi expect fun CupertinoDatePickerNative( diff --git a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDialogsNative.kt b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDialogsNative.kt index 159f27c3..4c9f3c0f 100644 --- a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDialogsNative.kt +++ b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDialogsNative.kt @@ -42,7 +42,7 @@ import zone.ien.hig.theme.systemGray7 * @param buttonsOrientation not used. iOS automatically picks most suitable layout * based on buttons width and count * @param buttons actions builder block - * */ + */ @Composable expect fun CupertinoAlertDialogNative( onDismissRequest: () -> Unit, @@ -58,6 +58,7 @@ expect fun CupertinoAlertDialogNative( /** * Native analog for the compose [CupertinoActionSheet]. * + * @param visible whether the action sheet is visible * @param onDismissRequest called when dialog is already dismissed. Must not be ignored * @param title alert dialog title * @param message alert dialog message @@ -66,7 +67,7 @@ expect fun CupertinoAlertDialogNative( * @param properties not used. To enable dismissOnClickOutside behavior * add an action with [AlertActionStyle.Cancel] that would receive a cancel callback. * @param buttons actions builder block - * */ + */ @Composable expect fun CupertinoActionSheetNative( visible: Boolean, @@ -79,10 +80,18 @@ expect fun CupertinoActionSheetNative( buttons: NativeAlertDialogActionsScope.() -> Unit, ) +/** + * Scope for building native alert dialog actions. + */ interface NativeAlertDialogActionsScope { /** * Alert controller button - * */ + * + * @param onClick callback when button is clicked + * @param style the style of the action button + * @param enabled whether the button is enabled + * @param title the title of the button + */ fun action( onClick: () -> Unit, style: AlertActionStyle = AlertActionStyle.Default, @@ -93,7 +102,11 @@ interface NativeAlertDialogActionsScope { /** * Alert controller button with default style - * */ + * + * @param onClick callback when button is clicked + * @param enabled whether the button is enabled + * @param title the title of the button + */ fun NativeAlertDialogActionsScope.default( onClick: () -> Unit, enabled: Boolean = true, @@ -107,7 +120,11 @@ fun NativeAlertDialogActionsScope.default( /** * Alert controller button with destructive style - * */ + * + * @param onClick callback when button is clicked + * @param enabled whether the button is enabled + * @param title the title of the button + */ fun NativeAlertDialogActionsScope.destructive( onClick: () -> Unit, enabled: Boolean = true, @@ -121,7 +138,11 @@ fun NativeAlertDialogActionsScope.destructive( /** * Alert controller button with cancel style - * */ + * + * @param onClick callback when button is clicked + * @param enabled whether the button is enabled + * @param title the title of the button + */ fun NativeAlertDialogActionsScope.cancel( onClick: () -> Unit, enabled: Boolean = true, @@ -133,6 +154,14 @@ fun NativeAlertDialogActionsScope.cancel( title = title, ) +/** + * A button for a native alert dialog. + * + * @param onClick callback when the button is clicked + * @param style the style of the action button + * @param enabled whether the button is enabled + * @param title the title of the button + */ internal class CupertinoAlertDialogButtonNative( val onClick: () -> Unit, val style: AlertActionStyle, @@ -140,6 +169,11 @@ internal class CupertinoAlertDialogButtonNative( val title: String, ) +/** + * Converts native alert dialog actions to regular dialog actions. + * + * @param native the native dialog actions to convert + */ internal fun AlertDialogActionsScope.fromNative(native: NativeAlertDialogActionsScope.() -> Unit) { val buttons = mutableListOf() diff --git a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.kt b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.kt new file mode 100644 index 00000000..5f1b63bc --- /dev/null +++ b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.kt @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2023-2024. Compose Cupertino project and open source contributors. + * Copyright (c) 2025. Scott Lanoue. + * Copyright (c) 2026. IENGROUND of IENLAB. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + + +package zone.ien.hig + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.rememberScrollState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.PopupProperties +import com.kyant.backdrop.Backdrop + +/** + * Data class representing a menu item in a dropdown menu. + * + * @param title the text title of the menu item + * @param enabled whether the menu item is enabled + * @param icon an optional icon painter for the menu item + * @param isDestructive whether this is a destructive menu action + * @param onClick callback when the menu item is clicked + */ +@Immutable +data class CupertinoMenuItemData( + val title: String, + val enabled: Boolean = true, + val icon: Painter? = null, + val isDestructive: Boolean = false, + val onClick: () -> Unit, +) + +/** + * Data class representing a section in a dropdown menu. + * + * @param title the title of the section + * @param icon an optional icon for the section + * @param options display options for the section + * @param items the list of menu items in this section + */ +@Immutable +data class CupertinoMenuSectionData( + val title: String, + val icon: Painter? = null, + val options: HigMenuOptions = HigMenuOptions.DisplayInline, + val items: List, +) + +/** + * Enum representing display options for dropdown menu sections. + */ +enum class HigMenuOptions { + DisplayInline, SingleSelection, DisplayAsPalette, Destructive +} + +/** + * A dropdown menu that is implemented with native platform widgets on iOS and Compose widgets on + * non-iOS platforms. + * + * @param expanded whether the menu is currently expanded + * @param onDismissRequest callback when the menu should be dismissed + * @param modifier the [Modifier] to be applied to this menu + * @param offset the offset for positioning the menu + * @param paddingValues the padding values for the menu + * @param containerColor the color of the menu container + * @param width the width of the menu + * @param scrollState the scroll state for the menu + * @param properties popup properties for the menu + * @param backdrop the backdrop for the menu + * @param items list of menu items + * @param sections list of menu sections + */ +@OptIn(ExperimentalComposeUiApi::class) +@ExperimentalCupertinoApi +@Composable +expect fun CupertinoDropdownMenuNative( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + offset: DpOffset = DpOffset.Zero, + paddingValues: PaddingValues = CupertinoDropdownMenuDefaults.PaddingValues, + containerColor: Color = CupertinoDropdownMenuDefaults.ContainerColor, + width: Dp = CupertinoDropdownMenuDefaults.DefaultWidth, + scrollState: ScrollState = rememberScrollState(), + properties: PopupProperties = PopupProperties(focusable = true, clippingEnabled = false), + backdrop: Backdrop, + items: List = emptyList(), + sections: List = emptyList(), +) \ No newline at end of file diff --git a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.kt b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.kt new file mode 100644 index 00000000..9b323d70 --- /dev/null +++ b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.kt @@ -0,0 +1,26 @@ +package zone.ien.hig + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import com.kyant.backdrop.backdrops.LayerBackdrop + +data class CupertinoNavigationBarItemData( + val onClick: () -> Unit, + val icon: Painter, + val selectedIcon: Painter? = null, + val label: String +) + +@OptIn(ExperimentalCupertinoApi::class) +@Composable +expect fun CupertinoNavigationBarNative( + modifier: Modifier = Modifier, + colors: CupertinoNavigationBarColors = CupertinoNavigationBarDefaults.colors(), + windowInsets: WindowInsets = CupertinoNavigationBarDefaults.windowInsets, + backdrop: LayerBackdrop, + selectedTabIndex: () -> Int, + onTabSelected: (index: Int) -> Unit, + items: List +) \ No newline at end of file diff --git a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoTimePickerNative.kt b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoTimePickerNative.kt index 09aa127a..0afb87d2 100644 --- a/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoTimePickerNative.kt +++ b/hig-native/src/commonMain/kotlin/zone/ien/hig/CupertinoTimePickerNative.kt @@ -27,6 +27,15 @@ import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.Dp import zone.ien.hig.theme.CupertinoTheme +/** + * A time picker that is implemented with native platform widgets on iOS and Compose widgets on + * non-iOS platforms. + * + * @param state the state of the time picker + * @param modifier the [Modifier] to be applied to this time picker + * @param height the height of the time picker + * @param containerColor the color to apply to the background of the time picker + */ @Composable @ExperimentalCupertinoApi expect fun CupertinoTimePickerNative( diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.ios.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.ios.kt index 15168857..f3b4e4c7 100644 --- a/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.ios.kt +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.ios.kt @@ -56,6 +56,14 @@ import platform.UIKit.UIDatePickerMode import platform.UIKit.UIDatePickerStyle import platform.UIKit.UIView +/** + * An iOS implementation of the date picker native component. + * + * @param state the date picker state + * @param modifier the [Modifier] to be applied to this date picker + * @param style the date picker style, either wheel or pager + * @param containerColor the color to apply to the background of the date picker + */ @OptIn(InternalCupertinoApi::class) @Composable @ExperimentalCupertinoApi @@ -81,6 +89,16 @@ actual fun CupertinoDatePickerNative( ) } +/** + * Composable that creates a native iOS UIDatePicker for use inside a [UIKitView]. + * + * @param millis the initial date in milliseconds since epoch + * @param mode the UIDatePickerMode + * @param onChange callback when the date changed + * @param modifier the [Modifier] to be applied to this date picker + * @param style the date picker style, either wheel or pager + * @param containerColor the color to apply to the background of the date picker + */ @OptIn(ExperimentalForeignApi::class, ExperimentalComposeUiApi::class) @Composable internal fun CupertinoDatePickerNativeImpl( @@ -140,6 +158,14 @@ internal fun CupertinoDatePickerNativeImpl( ) } +/** + * A UIDatePicker subclass that handles date selection and UI configuration. + * + * @param millis the initial date in milliseconds since epoch + * @param mode the UIDatePickerMode + * @param style the date picker style, either wheel or pager + * @param onChange callback when the date changed + */ @OptIn(ExperimentalForeignApi::class) private class DatePicker( millis: Long, diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.ios.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.ios.kt new file mode 100644 index 00000000..b8fb5f81 --- /dev/null +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.ios.kt @@ -0,0 +1,212 @@ +package zone.ien.hig + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.uikit.LocalUIViewController +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.window.PopupProperties +import com.kyant.backdrop.Backdrop +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.CValue +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import platform.CoreGraphics.CGPoint +import platform.CoreGraphics.CGRectMake +import platform.UIKit.UIAction +import platform.UIKit.UIButton +import platform.UIKit.UIButtonTypePlain +import platform.UIKit.UIColor +import platform.UIKit.UIContextMenuConfiguration +import platform.UIKit.UIContextMenuConfigurationElementOrderFixed +import platform.UIKit.UIContextMenuInteraction +import platform.UIKit.UIContextMenuInteractionAnimatingProtocol +import platform.UIKit.UIContextMenuInteractionDelegateProtocol +import platform.UIKit.UIControlStateNormal +import platform.UIKit.UIDevice +import platform.UIKit.UIEvent +import platform.UIKit.UIGestureRecognizer +import platform.UIKit.UIMenu +import platform.UIKit.UIMenuElement +import platform.UIKit.UIMenuElementAttributesDestructive +import platform.UIKit.UIMenuElementAttributesDisabled +import platform.UIKit.UIMenuOptionsDestructive +import platform.UIKit.UIMenuOptionsDisplayAsPalette +import platform.UIKit.UIMenuOptionsDisplayInline +import platform.UIKit.UIMenuOptionsSingleSelection +import platform.UIKit.addInteraction +import platform.UIKit.touchesBegan +import platform.darwin.NSObject + +fun HigMenuOptions.toUIMenuOptions() = when (this) { + HigMenuOptions.DisplayInline -> UIMenuOptionsDisplayInline + HigMenuOptions.SingleSelection -> UIMenuOptionsSingleSelection + HigMenuOptions.DisplayAsPalette -> UIMenuOptionsDisplayAsPalette + HigMenuOptions.Destructive -> UIMenuOptionsDestructive +} + +@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) +@ExperimentalCupertinoApi +@Composable +actual fun CupertinoDropdownMenuNative( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier, + offset: DpOffset, + paddingValues: PaddingValues, + containerColor: Color, + width: Dp, + scrollState: ScrollState, + properties: PopupProperties, + backdrop: Backdrop, + items: List, + sections: List +) { + val density = LocalDensity.current + val viewController = LocalUIViewController.current + + val delegate = remember { CupertinoDropdownMenuDelegate() } + delegate.items = items + delegate.sections = sections + delegate.onDismissRequest = onDismissRequest + + // Insert UIButton at the bottom of the view hierarchy + DisposableEffect(viewController) { + val button = UIButton.buttonWithType(UIButtonTypePlain).apply { + backgroundColor = UIColor.clearColor + setTitle("", forState = UIControlStateNormal) + showsMenuAsPrimaryAction = true + preferredMenuElementOrder = UIContextMenuConfigurationElementOrderFixed + addInteraction(UIContextMenuInteraction(delegate)) + } + viewController.view.insertSubview(button, atIndex = 0) + delegate.button = button + + onDispose { + button.removeFromSuperview() + delegate.button = null + } + } + + // Refresh menu when items/sections change + LaunchedEffect(items, sections) { + delegate.button?.menu = delegate.buildMenu() + } + + // Show menu when expanded changes + LaunchedEffect(expanded) { + val button = delegate.button ?: return@LaunchedEffect + if (!expanded) return@LaunchedEffect + + delegate.isMenuVisible = true // Prevent frame updates while menu is visible + if (isPerformPrimaryActionAvailable()) { + button.performPrimaryAction() + } else { + val gesture = button.gestureRecognizers + .orEmpty() + .filterIsInstance() + .firstOrNull { + it.`class`()?.toString() + ?.contains("UITouchDownGestureRecognizer", ignoreCase = true) == true + } + gesture?.touchesBegan(emptySet(), UIEvent()) + } + } + + Box( + modifier = modifier + .onGloballyPositioned { coords -> + if (delegate.isMenuVisible) return@onGloballyPositioned + + val d = density.density + val pos = coords.positionInRoot() + + val newX = kotlin.math.round(pos.x.toDouble() / d) + val newY = kotlin.math.round(pos.y.toDouble() / d) + val newW = kotlin.math.round(coords.size.width.toDouble() / d) + val newH = kotlin.math.round(coords.size.height.toDouble() / d) + + delegate.button?.let { btn -> + btn.frame.useContents { + if (kotlin.math.abs(origin.x - newX) >= 1.0 || + kotlin.math.abs(origin.y - newY) >= 1.0 || + kotlin.math.abs(size.width - newW) >= 1.0 || + kotlin.math.abs(size.height - newH) >= 1.0) { + btn.setFrame(CGRectMake(newX, newY, newW, newH)) + } + } + } + } + ) +} + +@OptIn(ExperimentalForeignApi::class) +internal class CupertinoDropdownMenuDelegate : NSObject(), UIContextMenuInteractionDelegateProtocol { + var items: List = emptyList() + var sections: List = emptyList() + var onDismissRequest: () -> Unit = {} + var button: UIButton? = null + var isMenuVisible: Boolean = false + + override fun contextMenuInteraction( + interaction: UIContextMenuInteraction, + configurationForMenuAtLocation: CValue + ): UIContextMenuConfiguration? = null + + override fun contextMenuInteraction( + interaction: UIContextMenuInteraction, + willEndForConfiguration: UIContextMenuConfiguration, + animator: UIContextMenuInteractionAnimatingProtocol? + ) { + isMenuVisible = false + onDismissRequest() + } + + fun buildMenu(): UIMenu { + val topActions: List = items.map { it.toUIAction() } + val sectionMenus: List = sections.map { section -> + UIMenu.menuWithTitle( + title = section.title, + image = section.icon?.toUIImage()?.resized(20.0), + identifier = null, + options = section.options.toUIMenuOptions(), + children = section.items.map { it.toUIAction() } + ) + } + + return UIMenu.menuWithTitle( + title = "", + children = topActions + sectionMenus, + ) + } +} + +@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class) +private fun CupertinoMenuItemData.toUIAction(): UIAction { + val action = UIAction.actionWithTitle( + title = title, + image = icon?.toUIImage()?.resized(20.0), + identifier = null, + ) { _ -> onClick() } + + var attributes: ULong = 0u + if (isDestructive) attributes = attributes or UIMenuElementAttributesDestructive + if (!enabled) attributes = attributes or UIMenuElementAttributesDisabled + action.attributes = attributes + return action +} + +@OptIn(ExperimentalForeignApi::class) +private fun isPerformPrimaryActionAvailable(): Boolean = platform.Foundation.NSProcessInfo.processInfo.operatingSystemVersion.useContents { + majorVersion > 17 || (majorVersion == 17L && minorVersion >= 4L) +} \ No newline at end of file diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.ios.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.ios.kt new file mode 100644 index 00000000..bfe720e4 --- /dev/null +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.ios.kt @@ -0,0 +1,155 @@ +package zone.ien.hig + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.uikit.LocalUIViewController +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.kyant.backdrop.backdrops.LayerBackdrop +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.useContents +import platform.UIKit.NSLayoutConstraint +import platform.UIKit.UITabBar +import zone.ien.hig.navigation.IosBarHeightCache +import zone.ien.hig.navigation.LocalIosTabBarPadding +import zone.ien.hig.navigation.TabBarManager + +@OptIn(ExperimentalCupertinoApi::class, ExperimentalForeignApi::class) +@Composable +actual fun CupertinoNavigationBarNative( + modifier: Modifier, + colors: CupertinoNavigationBarColors, + windowInsets: WindowInsets, + backdrop: LayerBackdrop, + selectedTabIndex: () -> Int, + onTabSelected: (index: Int) -> Unit, + items: List +) { + val density = LocalDensity.current + val viewController = LocalUIViewController.current + + // Write the measured tab bar height to the CompositionLocal so AdaptiveScaffold + // can automatically adjust content padding on iOS. + val iosTabBarPaddingState = LocalIosTabBarPadding.current + + val tabBarView = remember { + UITabBar().apply { + translatesAutoresizingMaskIntoConstraints = false + } + } + val isLiquidGlassEnabled = true // TODO: consider parameterize + + val onItemSelectedState by rememberUpdatedState(onTabSelected) + + val tabBarManager = remember(tabBarView) { + TabBarManager( + tabBar = tabBarView, + onItemSelected = { onItemSelectedState(it) }, + ) + } + + LaunchedEffect(items, selectedTabIndex) { + tabBarManager.setItems(items, selectedTabIndex()) + } + + val initialSafeAreaBottom = remember { 0.dp } + val cachedTabBarHeight = remember { IosBarHeightCache.lastTabBarHeight } + + var topLeft by remember { mutableStateOf(DpOffset.Zero) } + var positionInRoot by remember { mutableStateOf(DpOffset.Zero) } + var tabBarWidth by remember { mutableStateOf(0.dp) } + var tabBarHeight by remember { + val tabBarHeight = initialSafeAreaBottom + cachedTabBarHeight + if (tabBarHeight.value > 0f) { + iosTabBarPaddingState.value = PaddingValues(bottom = tabBarHeight) + } + mutableStateOf(tabBarHeight) + } + + DisposableEffect(tabBarView, viewController) { + viewController.view.addSubview(tabBarView) + + NSLayoutConstraint.activateConstraints( + listOf( + tabBarView.leadingAnchor.constraintEqualToAnchor(viewController.view.leadingAnchor), + tabBarView.trailingAnchor.constraintEqualToAnchor(viewController.view.trailingAnchor), + tabBarView.bottomAnchor.constraintEqualToAnchor( + if (isLiquidGlassEnabled) + viewController.view.bottomAnchor + else + viewController.view.safeAreaLayoutGuide.bottomAnchor + ), + ) + ) + + onDispose { + tabBarView.removeFromSuperview() + } + } + + LaunchedEffect(Unit) { + repeat(15) { // 최대 15프레임 동안 안정화 시도 + tabBarView.frame.useContents { + topLeft = DpOffset(x = origin.x.dp, y = origin.y.dp) + tabBarWidth = size.width.dp + + val safeAreaBottom = if (isLiquidGlassEnabled) 0.dp else viewController.view.safeAreaInsets.useContents { bottom.dp } + val newTabBarHeight = size.height.dp + safeAreaBottom + + IosBarHeightCache.updateTabBarHeight(size.height.dp) + + if (tabBarHeight != newTabBarHeight) { + tabBarHeight = newTabBarHeight + + if (newTabBarHeight.value > 0f) { + iosTabBarPaddingState.value = PaddingValues(bottom = newTabBarHeight) + } + } + } + withFrameMillis { } + } + } + + // Clean up the padding when this composable leaves the composition + DisposableEffect(Unit) { + onDispose { + iosTabBarPaddingState.value = PaddingValues() + } + } + + Box( + modifier = Modifier + .onPlaced { + val positionInRootPx = it.positionInRoot() + positionInRoot = with(density) { + DpOffset( + x = positionInRootPx.x.toDp(), + y = positionInRootPx.y.toDp(), + ) + } + } + .graphicsLayer { + translationX = (topLeft.x - positionInRoot.x).toPx() + translationY = (topLeft.y - positionInRoot.y).toPx() + } + .width(tabBarWidth) + .height(tabBarHeight) + ) +} \ No newline at end of file diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/IosBarHeightCache.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/IosBarHeightCache.kt new file mode 100644 index 00000000..fb3bc2cb --- /dev/null +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/IosBarHeightCache.kt @@ -0,0 +1,44 @@ +package zone.ien.hig.navigation + +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Simple in-memory cache for the last measured iOS native bar heights. + * + * When [AdaptiveTopBar] or [AdaptiveNavigationBar] measures the actual native bar height, + * it writes the value here. On subsequent compositions (e.g. navigating to a new screen), + * the cached value is used as the initial estimate instead of the hardcoded default, + * reducing the chance of a visible layout jump. + */ +internal object IosBarHeightCache { + private val isLiquidGlassEnabled = true + private val DEFAULT_NAV_BAR_HEIGHT = + if (isLiquidGlassEnabled) + 54.dp + else + 44.dp + private val DEFAULT_TAB_BAR_HEIGHT = + if (isLiquidGlassEnabled) + 83.dp + else + 49.dp + + var lastNavBarHeight: Dp = DEFAULT_NAV_BAR_HEIGHT + private set + + var lastTabBarHeight: Dp = DEFAULT_TAB_BAR_HEIGHT + private set + + fun updateNavBarHeight(height: Dp) { + if (height.value > 0f) { + lastNavBarHeight = height + } + } + + fun updateTabBarHeight(height: Dp) { + if (height.value > 0f) { + lastTabBarHeight = height + } + } +} \ No newline at end of file diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/LocalIosTabBarPadding.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/LocalIosTabBarPadding.kt new file mode 100644 index 00000000..343158b2 --- /dev/null +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/LocalIosTabBarPadding.kt @@ -0,0 +1,20 @@ +package zone.ien.hig.navigation + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.mutableStateOf + +/** + * Internal CompositionLocal used to communicate the native iOS UITabBar height + * from [AdaptiveNavigationBar] to [AdaptiveScaffold]. + * + * When [AdaptiveNavigationBar] is used inside an [AdaptiveScaffold]'s `bottomBar`, + * it measures the native UITabBar height and writes it to this local. + * The [AdaptiveScaffold] reads this value and adds it to the bottom padding + * passed to the `content` lambda, so consumers don't need to handle iOS tab bar + * insets manually. + */ +internal val LocalIosTabBarPadding = compositionLocalOf> { + mutableStateOf(PaddingValues()) +} diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/TabBarManager.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/TabBarManager.kt new file mode 100644 index 00000000..e521cf98 --- /dev/null +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/navigation/TabBarManager.kt @@ -0,0 +1,76 @@ +package zone.ien.hig.navigation + +import kotlinx.cinterop.ExperimentalForeignApi +import platform.UIKit.UITabBar +import platform.UIKit.UITabBarDelegateProtocol +import platform.UIKit.UITabBarItem +import platform.darwin.NSObject +import zone.ien.hig.CupertinoNavigationBarItemData +import zone.ien.hig.resized +import zone.ien.hig.toUIImage + +/** + * Reference: com.mohamedrejeb.calf.ui.navigation + */ + +/** + * A helper class that manages a native iOS UITabBar, handling item setup and selection. + * + * @param tabBar The native UITabBar instance to manage. + * @param onItemSelected Callback invoked when a tab bar item is selected, with the item index. + */ +@OptIn(ExperimentalForeignApi::class) +internal class TabBarManager( + private val tabBar: UITabBar, + onItemSelected: (Int) -> Unit, +) { + private val uiTabBarDelegate = UITabBarDelegate( + onItemSelected = onItemSelected, + ) + + private var currentItems = emptyList() + + init { + tabBar.delegate = uiTabBarDelegate + } + + fun setItems( + items: List, + selectedIndex: Int, + ) { + if (items.isEmpty()) return + + if (items != currentItems) { + val uiKitItems = items.mapIndexed { index, item -> + item.toUITabBarItem(index.toLong()) + } + tabBar.setItems(uiKitItems) + currentItems = items + val safeSelectedIndex = selectedIndex.coerceIn(uiKitItems.indices) + tabBar.selectedItem = uiKitItems[safeSelectedIndex] + } else { + val uiKitItems = tabBar.items?.filterIsInstance() ?: return + val safeSelectedIndex = selectedIndex.coerceIn(uiKitItems.indices) + tabBar.selectedItem = uiKitItems[safeSelectedIndex] + } + } +} + +private fun CupertinoNavigationBarItemData.toUITabBarItem(tag: Long): UITabBarItem { + val item = UITabBarItem( + title = label, + image = icon.toUIImage().resized(20.0), + tag = tag, + ) + selectedIcon?.toUIImage()?.resized(20.0)?.let { item.selectedImage = it } + return item +} + +internal class UITabBarDelegate( + private val onItemSelected: (Int) -> Unit, +) : NSObject(), UITabBarDelegateProtocol { + override fun tabBar(tabBar: UITabBar, didSelectItem: UITabBarItem) { + tabBar.selectedItem = didSelectItem + onItemSelected(didSelectItem.tag.toInt()) + } +} diff --git a/hig-native/src/iosMain/kotlin/zone/ien/hig/util.ios.kt b/hig-native/src/iosMain/kotlin/zone/ien/hig/util.ios.kt index 79adf456..7ed882e5 100644 --- a/hig-native/src/iosMain/kotlin/zone/ien/hig/util.ios.kt +++ b/hig-native/src/iosMain/kotlin/zone/ien/hig/util.ios.kt @@ -20,9 +20,35 @@ package zone.ien.hig +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asSkiaBitmap +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.addressOf +import kotlinx.cinterop.usePinned +import org.jetbrains.skia.Data +import org.jetbrains.skia.EncodedImageFormat +import org.jetbrains.skia.Image +import platform.Foundation.NSData +import platform.Foundation.dataWithBytes +import platform.UIKit.UIImage import platform.UIKit.UIUserInterfaceStyle import platform.UIKit.UIView import platform.UIKit.UIViewController +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.drawscope.CanvasDrawScope +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import kotlinx.cinterop.useContents +import platform.CoreGraphics.CGRectMake +import platform.CoreGraphics.CGSizeMake +import platform.UIKit.UIGraphicsBeginImageContextWithOptions +import platform.UIKit.UIGraphicsEndImageContext +import platform.UIKit.UIGraphicsGetImageFromCurrentImageContext +import platform.UIKit.UIImageRenderingMode +import kotlin.math.roundToInt internal fun UIViewController.applyTheme(dark: Boolean) { overrideUserInterfaceStyle = @@ -43,3 +69,79 @@ internal fun UIView.applyTheme(dark: Boolean) { } } } + +@OptIn(ExperimentalForeignApi::class) +fun ImageBitmap.toUIImage(): UIImage { + val skiaImage = Image.makeFromBitmap(this.asSkiaBitmap()) + val pngData = skiaImage.encodeToData(EncodedImageFormat.PNG) + ?: return UIImage() + val pngBytes = pngData.bytes + val nsData = pngBytes.usePinned { pinned -> + NSData.dataWithBytes(pinned.addressOf(0), pngBytes.size.toULong()) + } + return UIImage(data = nsData).imageWithRenderingMode(UIImageRenderingMode.UIImageRenderingModeAlwaysOriginal) +} + +fun Painter.toImageBitmap( + size: Size = intrinsicSize, + density: Density = Density(1f), + layoutDirection: LayoutDirection = LayoutDirection.Ltr, +): ImageBitmap { + val width = size.width.toInt().takeIf { it > 0 } ?: 64 + val height = size.height.toInt().takeIf { it > 0 } ?: 64 + val bitmap = ImageBitmap(width, height) + val canvas = Canvas(bitmap) + CanvasDrawScope().draw( + density = density, + layoutDirection = layoutDirection, + canvas = canvas, + size = Size(width.toFloat(), height.toFloat()) + ) { + // Force render as white -> so AlwaysTemplate can multiply tint correctly + with(this) { + drawContext.canvas.let { c -> + val paint = androidx.compose.ui.graphics.Paint().apply { + colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( + androidx.compose.ui.graphics.Color.White + ) + } + c.saveLayer( + bounds = androidx.compose.ui.geometry.Rect( + 0f, 0f, width.toFloat(), height.toFloat() + ), + paint = paint + ) + } + } + draw(Size(width.toFloat(), height.toFloat())) + drawContext.canvas.restore() + } + return bitmap +} + +@OptIn(ExperimentalForeignApi::class) +fun Painter.toUIImage(size: Size = intrinsicSize): UIImage { + return this.toImageBitmap(size).toUIImage() +} + +@OptIn(ExperimentalForeignApi::class) +fun UIImage.resized(maxSize: Double): UIImage { + val originalWidth = this.size.useContents { width } + val originalHeight = this.size.useContents { height } + + if (originalWidth <= 0.0 || originalHeight <= 0.0) return this + + val scale = maxSize / maxOf(originalWidth, originalHeight) + val targetWidth = originalWidth * scale + val targetHeight = originalHeight * scale + + val targetSize = CGSizeMake(targetWidth, targetHeight) + UIGraphicsBeginImageContextWithOptions(targetSize, false, 0.0) + this.imageWithRenderingMode(UIImageRenderingMode.UIImageRenderingModeAlwaysOriginal) + .drawInRect(CGRectMake(0.0, 0.0, targetWidth, targetHeight)) + val resizedImage = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return resizedImage + ?.imageWithRenderingMode(UIImageRenderingMode.UIImageRenderingModeAlwaysTemplate) + ?: this +} \ No newline at end of file diff --git a/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.nonIos.kt b/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.nonIos.kt index 86874ac6..9e636fb6 100644 --- a/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.nonIos.kt +++ b/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDatePickerNative.nonIos.kt @@ -24,6 +24,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +/** + * A date picker that is implemented with Compose widgets on non-iOS platforms. + * + * @param state the state of the date picker + * @param modifier the [Modifier] to be applied to this date picker + * @param style the date picker style, either wheel or pager + * @param containerColor the color to apply to the background of the date picker + */ @Composable @ExperimentalCupertinoApi actual fun CupertinoDatePickerNative( diff --git a/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.nonIos.kt b/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.nonIos.kt new file mode 100644 index 00000000..f6361b04 --- /dev/null +++ b/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoDropdownMenuNative.nonIos.kt @@ -0,0 +1,89 @@ +package zone.ien.hig + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.window.PopupProperties +import com.kyant.backdrop.Backdrop +import zone.ien.hig.MenuSection +import zone.ien.hig.theme.CupertinoColors +import zone.ien.hig.theme.systemRed + +@OptIn(markerClass = [ExperimentalComposeUiApi::class]) +@ExperimentalCupertinoApi +@Composable +actual fun CupertinoDropdownMenuNative( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier, + offset: DpOffset, + paddingValues: PaddingValues, + containerColor: Color, + width: Dp, + scrollState: ScrollState, + properties: PopupProperties, + backdrop: Backdrop, + items: List, + sections: List +) { + CupertinoDropdownMenu( + expanded = expanded, + onDismissRequest = onDismissRequest, + modifier = modifier, + offset = offset, + paddingValues = paddingValues, + containerColor = containerColor, + width = width, + scrollState = scrollState, + properties = properties, + backdrop = backdrop + ) { + items.fastForEach { + MenuAction( + onClick = it.onClick, + enabled = it.enabled, + leadingIcon = { + it.icon?.let { icon -> + CupertinoIcon( + painter = icon, + contentDescription = null + ) + } + }, + title = { CupertinoText(text = it.title) }, + contentColor = if (it.isDestructive) CupertinoColors.systemRed else CupertinoDropdownMenuDefaults.ContentColor + ) + } + if (items.isNotEmpty() && sections.isNotEmpty()) { + MenuDivider() + } + sections.fastForEach { section -> + MenuSection( + title = { CupertinoText(text = section.title) } + ) { + section.items.fastForEach { + MenuAction( + onClick = it.onClick, + enabled = it.enabled, + leadingIcon = { + it.icon?.let { icon -> + CupertinoIcon( + painter = icon, + contentDescription = null + ) + } + }, + title = { CupertinoText(text = it.title) }, + contentColor = if (it.isDestructive) CupertinoColors.systemRed else CupertinoDropdownMenuDefaults.ContentColor + ) + } + } + } + } +} \ No newline at end of file diff --git a/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.nonIos.kt b/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.nonIos.kt new file mode 100644 index 00000000..4e169b2b --- /dev/null +++ b/hig-native/src/nonIosMain/kotlin/zone/ien/hig/CupertinoNavigationBarNative.nonIos.kt @@ -0,0 +1,46 @@ +package zone.ien.hig + +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.kyant.backdrop.backdrops.LayerBackdrop + +@OptIn(markerClass = [ExperimentalCupertinoApi::class]) +@Composable +actual fun CupertinoNavigationBarNative( + modifier: Modifier, + colors: CupertinoNavigationBarColors, + windowInsets: WindowInsets, + backdrop: LayerBackdrop, + selectedTabIndex: () -> Int, + onTabSelected: (index: Int) -> Unit, + items: List +) { + CupertinoNavigationBar( + modifier = modifier, + colors = colors, + windowInsets = windowInsets, + backdrop = backdrop, + selectedTabIndex = selectedTabIndex, + onTabSelected = onTabSelected, + tabsCount = items.size + ) { + items.forEachIndexed { index, item -> + CupertinoNavigationBarItem( + onClick = item.onClick, + icon = { + Icon( + painter = + if (index == selectedTabIndex() && item.selectedIcon != null) item.selectedIcon + else item.icon + , + contentDescription = item.label, + ) + }, + label = { Text(text = item.label) }, + ) + } + } +} \ No newline at end of file diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/CalendarModelImpl.android.kt b/hig/src/androidMain/kotlin/zone/ien/hig/CalendarModelImpl.android.kt index 52853ddb..6a995b6f 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/CalendarModelImpl.android.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/CalendarModelImpl.android.kt @@ -16,8 +16,6 @@ * limitations under the License. */ - - package zone.ien.hig import android.os.Build @@ -40,22 +38,44 @@ import java.time.format.FormatStyle import java.time.format.TextStyle import java.time.temporal.WeekFields import java.util.Locale +import androidx.compose.ui.platform.LocalLocale +/** + * Gets the default locale for the device. + * + * @return The default [CalendarLocale] for the device + */ @Composable @ReadOnlyComposable internal actual fun defaultLocale(): CalendarLocale = LocalConfiguration.current.run { val locales = locales - if (locales.isEmpty) Locale.getDefault() else locales[0] + if (locales.isEmpty) LocalLocale.current.platformLocale else locales[0] } +/** + * Gets the current locale for the device. + * + * @return The current [CalendarLocale] for the device + */ internal actual fun currentLocale(): CalendarLocale = Locale.getDefault() /** * A [CalendarModel] implementation for API >= 26. + * + * This implementation uses Java 8 Time APIs available on Android API level 26 and above. + * + * @property today The current date + * @property firstDayOfWeek The first day of the week for the default locale + * @property weekdayNames The weekday names for the default locale */ @RequiresApi(Build.VERSION_CODES.O) internal class AndroidCalendarModelImpl: CalendarModel { + /** + * Gets the current date. + * + * @return The [CalendarDate] representing today + */ override val today get(): CalendarDate { val systemLocalDate = LocalDate.now() @@ -72,10 +92,26 @@ internal class AndroidCalendarModelImpl: CalendarModel { ) } + /** + * Gets the first day of the week for the default locale. + * + * @return The first day of the week (1 = Sunday, 2 = Monday, etc.) + */ override val firstDayOfWeek: Int = WeekFields.of(Locale.getDefault()).firstDayOfWeek.value + /** + * Gets the weekday names for the default locale. + * + * @return A list of pairs containing full and short weekday names + */ override val weekdayNames: List> = weekdayNames(Locale.getDefault()) + /** + * Gets the weekday names for a specific locale. + * + * @param locale The [Locale] to get weekday names for + * @return A list of pairs containing full and short weekday names + */ fun weekdayNames(locale: Locale): List> = // This will start with Monday as the first day, according to ISO-8601. with(locale) { @@ -93,6 +129,12 @@ internal class AndroidCalendarModelImpl: CalendarModel { } } + /** + * Gets the date input format for the given locale. + * + * @param locale The [Locale] to get the input format for + * @return The [DateInputFormat] for the specified locale + */ override fun getDateInputFormat(locale: Locale): DateInputFormat = datePatternAsInputFormat( DateTimeFormatterBuilder.getLocalizedDateTimePattern( @@ -107,6 +149,12 @@ internal class AndroidCalendarModelImpl: CalendarModel { ), ) + /** + * Gets the canonical date from a timestamp. + * + * @param timeInMillis The timestamp in milliseconds + * @return The [CalendarDate] for the given timestamp + */ override fun getCanonicalDate(timeInMillis: Long): CalendarDate { val localDate = Instant.ofEpochMilli(timeInMillis).atZone(utcTimeZoneId).toLocalDate() @@ -118,6 +166,12 @@ internal class AndroidCalendarModelImpl: CalendarModel { ) } + /** + * Gets the month from a timestamp. + * + * @param timeInMillis The timestamp in milliseconds + * @return The [CalendarMonth] for the given timestamp + */ override fun getMonth(timeInMillis: Long): CalendarMonth = getMonth( Instant @@ -127,21 +181,55 @@ internal class AndroidCalendarModelImpl: CalendarModel { .toLocalDate(), ) + /** + * Gets the month from a [CalendarDate]. + * + * @param date The [CalendarDate] to get the month for + * @return The [CalendarMonth] for the given date + */ override fun getMonth(date: CalendarDate): CalendarMonth = getMonth(LocalDate.of(date.year, date.month, 1)) + /** + * Gets the month from year and month values. + * + * @param year The year + * @param month The month (1-12) + * @return The [CalendarMonth] for the given year and month + */ override fun getMonth( year: Int, month: Int, ): CalendarMonth = getMonth(LocalDate.of(year, month, 1)) + /** + * Gets a date with the specified year, month, and day. + * + * @param year The year + * @param month The month (1-12) + * @param day The day of month (1-31) + * @return The [CalendarDate] for the given year, month, and day + */ override fun getDate( year: Int, month: Int, day: Int, ): CalendarDate = CalendarDate(year, month, day, 0) + /** + * Gets the day of week for the given date. + * + * @param date The [CalendarDate] to get the day of week for + * @return The day of week (1 = Sunday, 2 = Monday, etc.) + */ override fun getDayOfWeek(date: CalendarDate): Int = date.toLocalDate().dayOfWeek.value + /** + * Adds months to a month. + * + * @param from The [CalendarMonth] to add months to + * @param addedMonthsCount The number of months to add + * @return The [CalendarMonth] with added months + */ override fun plusMonths( from: CalendarMonth, addedMonthsCount: Int, @@ -153,6 +241,13 @@ internal class AndroidCalendarModelImpl: CalendarModel { return getMonth(laterMonth) } + /** + * Subtracts months from a month. + * + * @param from The [CalendarMonth] to subtract months from + * @param subtractedMonthsCount The number of months to subtract + * @return The [CalendarMonth] with subtracted months + */ override fun minusMonths( from: CalendarMonth, subtractedMonthsCount: Int, @@ -164,12 +259,27 @@ internal class AndroidCalendarModelImpl: CalendarModel { return getMonth(earlierMonth) } + /** + * Formats a date using a custom pattern. + * + * @param utcTimeMillis The UTC timestamp to format (milliseconds from epoch) + * @param pattern The date format pattern to use + * @param locale The [Locale] to use when formatting the date + * @return The formatted date string + */ override fun formatWithPattern( utcTimeMillis: Long, pattern: String, locale: Locale, ): String = Companion.formatWithPattern(utcTimeMillis, pattern, locale) + /** + * Parses a date string using the provided pattern. + * + * @param date The date string to parse + * @param pattern The date format pattern to use + * @return The parsed [CalendarDate] or null if parsing fails + */ override fun parse( date: String, pattern: String, @@ -194,6 +304,11 @@ internal class AndroidCalendarModelImpl: CalendarModel { } } + /** + * Returns a string representation of this calendar model. + * + * @return The string "CalendarModel" + */ override fun toString(): String = "CalendarModel" companion object { @@ -203,6 +318,7 @@ internal class AndroidCalendarModelImpl: CalendarModel { * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch) * @param pattern a date format pattern * @param locale the [Locale] to use when formatting the given timestamp + * @return The formatted date string */ fun formatWithPattern( utcTimeMillis: Long, @@ -222,10 +338,18 @@ internal class AndroidCalendarModelImpl: CalendarModel { /** * Holds a UTC [ZoneId]. + * + * @return The UTC [ZoneId] for time zone operations */ internal val utcTimeZoneId: ZoneId = ZoneId.of("UTC") } + /** + * Gets a calendar month from a LocalDate. + * + * @param firstDayLocalDate The first day of the month as a LocalDate + * @return The [CalendarMonth] for the given LocalDate + */ private fun getMonth(firstDayLocalDate: LocalDate): CalendarMonth { val difference = firstDayLocalDate.dayOfWeek.value - firstDayOfWeek val daysFromStartOfWeekToFirstOfMonth = @@ -249,14 +373,24 @@ internal class AndroidCalendarModelImpl: CalendarModel { ) } + /** + * Converts a CalendarMonth to a LocalDate. + * + * @return The [LocalDate] representation of the calendar month + */ private fun CalendarMonth.toLocalDate(): LocalDate = Instant.ofEpochMilli(startUtcTimeMillis).atZone( utcTimeZoneId ).toLocalDate() + /** + * Converts a CalendarDate to a LocalDate. + * + * @return The [LocalDate] representation of the calendar date + */ private fun CalendarDate.toLocalDate(): LocalDate = LocalDate.of( this.year, this.month, this.dayOfMonth, ) -} +} \ No newline at end of file diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/CupertinoDialogs.android.kt b/hig/src/androidMain/kotlin/zone/ien/hig/CupertinoDialogs.android.kt index 62d6b2df..ba28b7b0 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/CupertinoDialogs.android.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/CupertinoDialogs.android.kt @@ -17,13 +17,20 @@ */ - package zone.ien.hig import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.window.DialogProperties +/** + * Creates platform-specific dialog properties for fullscreen popups on Android. + * + * @param dismissOnBackPress Whether to dismiss the dialog when the back button is pressed + * @param dismissOnClickOutside Whether to dismiss the dialog when clicking outside + * @param usePlatformDefaultWidth Whether to use the platform default width + * @return The [DialogProperties] for the dialog + */ @Composable @ReadOnlyComposable internal actual fun FullscreenPopupProperties( @@ -38,4 +45,11 @@ internal actual fun FullscreenPopupProperties( usePlatformDefaultWidth = usePlatformDefaultWidth, ) +/** + * Gets the platform-specific insets for dialog properties. + * + * For Android, this returns true, indicating that platform insets should be used. + * + * @return true, indicating platform insets are used on Android + */ actual val DialogProperties.platformInsets: Boolean get() = true diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/HazePlatform.kt b/hig/src/androidMain/kotlin/zone/ien/hig/HazePlatform.kt index 35acb479..4b008cfb 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/HazePlatform.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/HazePlatform.kt @@ -29,9 +29,13 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp /** - * With CMP + Android, we can't do much other than display a transparent scrim. - * See `:haze-jetpack-compose` for a working blur on Android, but we need Compose 1.6.0 APIs, - * which are not available in CMP (yet). + * Android implementation of the HazeNode for drawing effects. + * + * @param areas The list of rectangles to draw the effect on + * @param backgroundColor The background color for the effect + * @param tint The tint color for the effect + * @param blurRadius The blur radius for the effect + * @param density The density for drawing operations */ internal actual class HazeNode actual constructor( private var areas: List, @@ -41,6 +45,15 @@ internal actual class HazeNode actual constructor( private val density: Density, ): Modifier.Node(), DrawModifierNode { + + /** + * Updates the HazeNode with new parameters. + * + * @param areas The list of rectangles to draw the effect on + * @param backgroundColor The background color for the effect + * @param tint The tint color for the effect + * @param blurRadius The blur radius for the effect + */ actual fun update( areas: List, backgroundColor: Color, @@ -53,6 +66,11 @@ internal actual class HazeNode actual constructor( this.blurRadius = blurRadius } + /** + * Draws the haze effect on the content. + * + * @param drawContent The content to draw + */ override fun ContentDrawScope.draw() { drawContent() diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/Luminance.android.kt b/hig/src/androidMain/kotlin/zone/ien/hig/Luminance.android.kt index 229295f5..aa31f16a 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/Luminance.android.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/Luminance.android.kt @@ -1,3 +1,21 @@ +/* + * Copyright (c) 2023-2024. Compose Cupertino project and open source contributors. + * Copyright (c) 2025. Scott Lanoue. + * Copyright (c) 2026. IENGROUND of IENLAB. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package zone.ien.hig import android.graphics.Bitmap @@ -5,12 +23,28 @@ import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asAndroidBitmap import androidx.compose.ui.graphics.asImageBitmap +/** + * Scales an image bitmap to the specified dimensions. + * + * @param width The target width + * @param height The target height + * @return The scaled [ImageBitmap] + */ actual fun ImageBitmap.scale(width: Int, height: Int): ImageBitmap { return Bitmap.createScaledBitmap(this.asAndroidBitmap(), 5, 5, false) .copy(Bitmap.Config.ARGB_8888, false) .asImageBitmap() } +/** + * Crops an image bitmap to the specified region. + * + * @param x The x-coordinate of the top-left corner of the crop region + * @param y The y-coordinate of the top-left corner of the crop region + * @param width The width of the crop region + * @param height The height of the crop region + * @return The cropped [ImageBitmap] + */ actual fun ImageBitmap.crop(x: Int, y: Int, width: Int, height: Int): ImageBitmap { val endX = (x + width).coerceAtMost(this.width) val endY = (y + height).coerceAtMost(this.height) diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/Modifier.android.kt b/hig/src/androidMain/kotlin/zone/ien/hig/Modifier.android.kt index a27ee33e..df69fbe6 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/Modifier.android.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/Modifier.android.kt @@ -21,4 +21,11 @@ package zone.ien.hig import androidx.compose.ui.Modifier import androidx.compose.foundation.systemGestureExclusion as androidSystemGestureExclusion +/** + * Applies system gesture exclusion to the modifier. + * + * This modifier is used to exclude areas from system gestures on Android devices. + * + * @return The modifier with system gesture exclusion applied + */ internal actual fun Modifier.systemGestureExclusion() = androidSystemGestureExclusion() diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/PlatformDateFormat.android.kt b/hig/src/androidMain/kotlin/zone/ien/hig/PlatformDateFormat.android.kt index f756e61f..2e4f7a46 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/PlatformDateFormat.android.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/PlatformDateFormat.android.kt @@ -16,8 +16,6 @@ * limitations under the License. */ - - package zone.ien.hig import android.os.Build @@ -27,17 +25,35 @@ import androidx.annotation.ChecksSdkIntAtLeast internal actual object PlatformDateFormat { + /** + * The delegate for date formatting operations based on API level. + * For API level 26 and above, uses AndroidCalendarModelImpl; otherwise, throws an error. + */ private val delegate by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { AndroidCalendarModelImpl() } else error("should not be used for api < 26") } + + /** + * Gets the first day of the week for the current locale. + * + * @return the first day of the week (1 = Sunday, 2 = Monday, etc.) + */ actual val firstDayOfWeek: Int get() = apiCheck( old = { LegacyDateFormat.firstDayOfWeek }, new = { delegate.firstDayOfWeek } ) + /** + * Formats a date using a custom pattern. + * + * @param utcTimeMillis The UTC timestamp to format (milliseconds from epoch) + * @param pattern The date format pattern to use + * @param locale The [CalendarLocale] to use when formatting the date + * @return The formatted date string + */ actual fun formatWithPattern( utcTimeMillis: Long, pattern: String, @@ -51,6 +67,14 @@ internal actual object PlatformDateFormat { } ) + /** + * Formats a date using a skeleton pattern. + * + * @param utcTimeMillis The UTC timestamp to format (milliseconds from epoch) + * @param skeleton The date format skeleton to use + * @param locale The [CalendarLocale] to use when formatting the date + * @return The formatted date string + */ actual fun formatWithSkeleton( utcTimeMillis: Long, skeleton: String, @@ -65,6 +89,13 @@ internal actual object PlatformDateFormat { } ) + /** + * Parses a date string using the provided pattern. + * + * @param date The date string to parse + * @param pattern The date format pattern to use + * @return The parsed [CalendarDate] or null if parsing fails + */ actual fun parse( date: String, pattern: String @@ -77,6 +108,12 @@ internal actual object PlatformDateFormat { } ) + /** + * Gets the date input format for the given locale. + * + * @param locale The [CalendarLocale] to get the input format for + * @return The [DateInputFormat] for the specified locale + */ actual fun getDateInputFormat(locale: CalendarLocale): DateInputFormat { return apiCheck( old = { LegacyDateFormat.getDateInputFormat(locale) }, @@ -84,27 +121,55 @@ internal actual object PlatformDateFormat { ) } - // From CalendarModelImpl.android.kt weekdayNames. - // - // Legacy model returns short ('Mon') format while newer version returns narrow ('M') format + /** + * Gets the weekday names for the given locale. + * + * From CalendarModelImpl.android.kt weekdayNames. + * + * Legacy model returns short ('Mon') format while newer version returns narrow ('M') format + * + * @param locale The [CalendarLocale] to get weekday names for + * @return A list of pairs containing full and short weekday names + */ actual fun weekdayNames(locale: CalendarLocale): List> { return apiCheck( old = { LegacyDateFormat.weekdayNames(locale) }, new = { delegate.weekdayNames(locale) } ) } + + /** + * Gets the month names for the given locale. + * + * @param locale The [CalendarLocale] to get month names for + * @return A list of month names + */ actual fun monthsNames(locale: CalendarLocale): List { return LegacyDateFormat.monthsNames(locale) } - // https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/text/format/DateFormat.java - // - // public static boolean is24HourFormat(Context context) -- used by Android date format + /** + * Checks if the time format is 24-hour for the given locale. + * + * https://android.googlesource.com/platform/frameworks/base/+/jb-release/core/java/android/text/format/DateFormat.java + * + * public static boolean is24HourFormat(Context context) -- used by Android date format + * + * @param locale The [CalendarLocale] to check + * @return true if the locale uses 24-hour format, false otherwise + */ actual fun is24HourFormat(locale: CalendarLocale): Boolean { return LegacyDateFormat.is24HourFormat(locale) } } +/** + * Checks API level and chooses the appropriate implementation. + * + * @param old The function to execute on older API versions + * @param new The function to execute on newer API versions + * @return The result of the appropriate function + */ @ChecksSdkIntAtLeast(api = Build.VERSION_CODES.O, lambda = 2) private fun apiCheck(old: () -> T, new: () -> T): T { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) diff --git a/hig/src/androidMain/kotlin/zone/ien/hig/utils/Coroutines.android.kt b/hig/src/androidMain/kotlin/zone/ien/hig/utils/Coroutines.android.kt index d9ba2bf2..085e55c1 100644 --- a/hig/src/androidMain/kotlin/zone/ien/hig/utils/Coroutines.android.kt +++ b/hig/src/androidMain/kotlin/zone/ien/hig/utils/Coroutines.android.kt @@ -1,7 +1,31 @@ +/* + * Copyright (c) 2023-2024. Compose Cupertino project and open source contributors. + * Copyright (c) 2025. Scott Lanoue. + * Copyright (c) 2026. IENGROUND of IENLAB. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package zone.ien.hig.utils import kotlinx.coroutines.android.awaitFrame +/** + * Suspends the current coroutine until the next frame is ready. + * + * This function is used to await the next frame in Android applications, + * typically for animation or UI updates. + */ actual suspend fun awaitFrame() { awaitFrame() } \ No newline at end of file diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModel.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModel.kt index 4f6a3c9e..1f943fb5 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModel.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModel.kt @@ -25,17 +25,29 @@ import androidx.compose.runtime.Immutable import androidx.compose.runtime.ReadOnlyComposable +/** + * Represents a calendar locale for formatting dates. + * + * This class is used to represent the locale used when formatting dates in the calendar. + */ expect class CalendarLocale /** * A composable function that returns the default [CalendarLocale]. * * When running on an Android platform, it will be recomposed when the `Configuration` gets updated. + * + * @return The default calendar locale */ @Composable @ReadOnlyComposable internal expect fun defaultLocale(): CalendarLocale +/** + * Returns the current [CalendarLocale]. + * + * @return The current calendar locale + */ internal expect fun currentLocale(): CalendarLocale /** @@ -47,9 +59,10 @@ internal expect fun currentLocale(): CalendarLocale * One difference is that order is irrelevant. For example, "MMMMd" will return "MMMM d" in the * en_US locale, but "d. MMMM" in the de_CH locale. * - * @param utcTimeMillis a UTC timestamp to format (milliseconds from epoch) - * @param skeleton a date format skeleton - * @param locale the [CalendarLocale] to use when formatting the given timestamp + * @param utcTimeMillis A UTC timestamp to format (milliseconds from epoch) + * @param skeleton A date format skeleton + * @param locale The [CalendarLocale] to use when formatting the given timestamp + * @return The formatted date string */ internal fun formatWithSkeleton( utcTimeMillis: Long, @@ -58,16 +71,26 @@ internal fun formatWithSkeleton( ): String = PlatformDateFormat.formatWithSkeleton(utcTimeMillis, skeleton, locale) +/** + * Represents a calendar model interface for date calculations and formatting. + * + * This interface provides methods for working with calendar dates and months, including + * formatting dates and retrieving calendar information like first day of week and weekday names. + */ internal interface CalendarModel { /** * A [CalendarDate] representing the current day. + * + * @return The current day as a [CalendarDate] */ val today: CalendarDate /** * Hold the first day of the week at the current `Locale` as an integer. The integer value * follows the ISO-8601 standard and refer to Monday as 1, and Sunday as 7. + * + * @return The first day of the week as an integer (1-7) */ val firstDayOfWeek: Int @@ -80,6 +103,8 @@ internal interface CalendarModel { * Newer APIs (i.e. API 26+), a [Pair] will hold a full name and the first letter of the * day. * Older APIs that predate API 26 will hold a full name and the first three letters of the day. + * + * @return A list of weekday names and abbreviations as [Pair] objects */ val weekdayNames: List> @@ -98,6 +123,9 @@ internal interface CalendarModel { * - dd-MM-yyyy * - dd.MM.yyyy * - MM/dd/yyyy + * + * @param locale The [CalendarLocale] to use when formatting the given timestamp + * @return The date input format for the given locale */ fun getDateInputFormat(locale: CalendarLocale): DateInputFormat @@ -108,6 +136,7 @@ internal interface CalendarModel { * be different than the one provided to this function. * * @param timeInMillis UTC milliseconds from the epoch + * @return The [CalendarDate] for the given time */ fun getCanonicalDate(timeInMillis: Long): CalendarDate @@ -115,6 +144,7 @@ internal interface CalendarModel { * Returns a [CalendarMonth] from a given _UTC_ time in milliseconds. * * @param timeInMillis UTC milliseconds from the epoch for the first day the month + * @return The [CalendarMonth] for the given time */ fun getMonth(timeInMillis: Long): CalendarMonth @@ -124,7 +154,8 @@ internal interface CalendarModel { * Note: This function ignores the [CalendarDate.dayOfMonth] value and just uses the date's * year and month to resolve a [CalendarMonth]. * - * @param date a [CalendarDate] to resolve into a month + * @param date A [CalendarDate] to resolve into a month + * @return The [CalendarMonth] for the given date */ fun getMonth(date: CalendarDate): CalendarMonth diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModelImpl.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModelImpl.kt index 9494c73a..a9c1f265 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModelImpl.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CalendarModelImpl.kt @@ -35,6 +35,14 @@ import kotlinx.datetime.toLocalDateTime import kotlin.time.Clock import kotlin.time.Instant + +/** + * A composable function that returns the default [CalendarLocale]. + * + * When running on an Android platform, it will be recomposed when the `Configuration` gets updated. + * + * @return The default calendar locale + */ internal class CalendarModelImpl: CalendarModel { override val today: CalendarDate diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoActivityIndicator.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoActivityIndicator.kt index 2c6cbaae..23963da6 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoActivityIndicator.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoActivityIndicator.kt @@ -50,16 +50,19 @@ import zone.ien.hig.theme.Gray import kotlin.math.roundToInt /** + * iOS-like segmented circle progress indicator. * - * iOS-like segmented circle progress indicator + * This composable displays an activity indicator similar to iOS's UIActivityIndicatorView. * - * @param modifier indicator modifier - * @param color color of the indicator - * @param count number of paths of the activity indicator - * @param innerRadius radius of the inner circle relative to indicator radius - * @param animationSpec infinite repeatable animation spec - * @param minAlpha opacity of the most translucent item - * */ + * @param modifier The modifier to be applied to the indicator + * @param size The size of the indicator + * @param color The color of the indicator + * @param count The number of paths of the activity indicator + * @param innerRadius The radius of the inner circle relative to indicator radius (0-1) + * @param strokeWidth The stroke width of the indicator, if unspecified uses default value + * @param animationSpec The infinite repeatable animation spec + * @param minAlpha The opacity of the most translucent item (0-1) + */ @Composable @ExperimentalCupertinoApi fun CupertinoActivityIndicator( @@ -160,16 +163,20 @@ fun CupertinoActivityIndicator( } /** + * iOS-like segmented circle progress indicator with progress tracking. * - * iOS-like segmented circle progress indicator + * This composable displays an activity indicator similar to iOS's UIActivityIndicatorView with progress tracking. * - * @param modifier indicator modifier - * @param color color of the indicator - * @param count number of paths of the activity indicator - * @param innerRadius radius of the inner circle relative to indicator radius - * @param animationSpec infinite repeatable animation spec - * @param minAlpha opacity of the most translucent item - * */ + * @param modifier The modifier to be applied to the indicator + * @param progress The progress value (0-1) to indicate completion + * @param size The size of the indicator + * @param color The color of the indicator + * @param count The number of paths of the activity indicator + * @param innerRadius The radius of the inner circle relative to indicator radius (0-1) + * @param strokeWidth The stroke width of the indicator, if unspecified uses default value + * @param animationSpec The infinite repeatable animation spec + * @param minAlpha The opacity of the most translucent item (0-1) + */ @Composable @ExperimentalCupertinoApi fun CupertinoActivityIndicator( diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomAppBar.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomAppBar.kt index cfb4bb39..1caf424a 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomAppBar.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomAppBar.kt @@ -41,7 +41,9 @@ import zone.ien.hig.theme.CupertinoTheme /** * Return true if container can't scroll forward - * */ + * + * @return True if the container cannot scroll forward + */ inline val ScrollableState.isNavigationBarTransparent: Boolean get() = !canScrollForward @@ -56,7 +58,8 @@ inline val ScrollableState.isNavigationBarTransparent: Boolean * @param color navigation bar container color. Alpha is controlled by the [CupertinoScaffold] * @param isTransparent if navigation bar currently should be transparent. See [CupertinoNavigationBar] * for use cases example. - * */ + * @return The appropriate color for the navigation bar + */ @Composable @ExperimentalCupertinoApi fun cupertinoTranslucentBottomBarColor( @@ -86,6 +89,20 @@ fun cupertinoTranslucentBottomBarColor( return Color.Transparent } +/** + * Composable function that creates a Cupertino-style bottom app bar. + * + * This composable displays a bottom app bar with Cupertino styling, similar to iOS's bottom navigation bar. + * + * @param modifier The modifier to be applied to the bottom app bar + * @param isTranslucent Whether the bottom app bar should be translucent + * @param isTransparent Whether the bottom app bar should be transparent + * @param containerColor The color of the bottom app bar container + * @param contentColor The color of the content in the bottom app bar + * @param contentPadding The padding around the content in the bottom app bar + * @param windowInsets The window insets to be applied to the bottom app bar + * @param content The content to be displayed in the bottom app bar + */ @ExperimentalCupertinoApi @Composable fun CupertinoBottomAppBar( diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheet.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheet.kt index ec76da31..6cbe5e11 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheet.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheet.kt @@ -66,7 +66,19 @@ import kotlin.jvm.JvmName /** * Content of the Cupertino modal bottom sheet. - * */ + * + * This composable creates the content area for a Cupertino modal bottom sheet. + * + * @param modifier The modifier to be applied to the bottom sheet + * @param containerColor The color of the bottom sheet container + * @param contentColor The color of the content in the bottom sheet + * @param appBarsAlpha The alpha value for app bars + * @param appBarsBlurRadius The blur radius for app bars + * @param hasNavigationTitle Whether the sheet has a navigation title + * @param topBar The top bar content of the bottom sheet + * @param bottomBar The bottom bar content of the bottom sheet + * @param content The main content of the bottom sheet + */ @Composable @ExperimentalCupertinoApi fun CupertinoBottomSheetContent( diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheetScaffold.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheetScaffold.kt index b74b712f..7ea5fb73 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheetScaffold.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoBottomSheetScaffold.kt @@ -77,7 +77,7 @@ import kotlin.math.max import kotlin.math.roundToInt /** - * Standard bottom sheets co-exist with the screen’s main UI region and allow for simultaneously + * Standard bottom sheets co-exist with the screen's main UI region and allow for simultaneously * viewing and interacting with both regions. They are commonly used to keep a feature or * secondary content visible on screen when content in main UI region is frequently scrolled or * panned. @@ -86,18 +86,23 @@ import kotlin.math.roundToInt * screen, by ensuring proper layout strategy for them and collecting necessary data so these * components will work together correctly. * - * @param sheetContent the content of the bottom sheet - * @param modifier the [Modifier] to be applied to this scaffold - * @param scaffoldState the state of the bottom sheet scaffold - * @param colors color of the scaffold, sheet, scrim and background. - * @param sheetShape the shape of the bottom sheet children - * @param sheetShadowElevation the shadow elevation of the bottom sheet - * @param sheetDragHandle optional visual marker to pull the scaffold's bottom sheet - * @param sheetSwipeEnabled whether the sheet swiping is enabled and should react to the user's + * @param sheetContent The content of the bottom sheet + * @param modifier The [Modifier] to be applied to this scaffold + * @param windowInsets The window insets to be applied to the scaffold + * @param scaffoldState The state of the bottom sheet scaffold + * @param colors Color of the scaffold, sheet, scrim and background. + * @param sheetShape The shape of the bottom sheet children + * @param sheetShadowElevation The shadow elevation of the bottom sheet + * @param sheetDragHandle Optional visual marker to pull the scaffold's bottom sheet + * @param sheetSwipeEnabled Whether the sheet swiping is enabled and should react to the user's * input - * @param topBar top app bar of the screen, typically a [CupertinoTopAppBar] - * to have no color. - * @param content content of the screen. The lambda receives a [PaddingValues] that should be + * @param topBar Top app bar of the screen, typically a [CupertinoTopAppBar] + * @param bottomBar Bottom app bar of the screen + * @param appBarsBlurAlpha The alpha value for app bars + * @param appBarsBlurRadius The blur radius for app bars + * @param hasNavigationTitle Whether the screen has a navigation title + * @param applyContentScaling Whether to apply content scaling based on sheet position + * @param content Content of the screen. The lambda receives a [PaddingValues] that should be * applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to * properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to * the child of the scroll, and not on the scroll itself. diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoButton.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoButton.kt index c3a0d39e..f577a261 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoButton.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoButton.kt @@ -61,6 +61,16 @@ import zone.ien.hig.theme.DefaultAlpha import zone.ien.hig.theme.Shapes import zone.ien.hig.theme.Typography +/** + * Represents the size configuration for Cupertino buttons. + * + * This enum defines different button sizes including Small, Regular, Large, and ExtraLarge. + * Each size has associated shape, text style, and content padding values. + * + * @property shape The shape function that returns the appropriate shape for the button size + * @property textStyle The text style function that returns an appropriate text style for the button size + * @property contentPadding The padding values for the button content + */ enum class CupertinoButtonSize( val shape: (Shapes) -> Shape, val textStyle: (Typography) -> TextStyle = { it.body }, @@ -88,6 +98,30 @@ enum class CupertinoButtonSize( ), } +/** + * A composable function that creates a Cupertino-styled button. + * + * This button has Cupertino styling and can be customized with various sizes, colors, and appearances. + * It handles press animations and interaction states. + * + * @param onClick The action to perform when the button is clicked + * @param modifier Modifier to be applied to the button + * @param enabled Whether the button is enabled and interactive + * @param size The size of the button (Small, Regular, Large, ExtraLarge) + * @param colors The colors to use for the button appearance + * @param border The border stroke to apply to the button, or null for no border + * @param shape The shape of the button + * @param contentPadding The padding to apply to the button content + * @param interactionSource The interaction source to track button interactions + * @param content The content to display inside the button + * + * Sample usage: + * ``` + * CupertinoButton(onClick = { /* handle click */ }) { + * Text("Button") + * } + * ``` + */ @Composable @ExperimentalCupertinoApi fun CupertinoButton( @@ -145,6 +179,27 @@ fun CupertinoButton( } } +/** + * Creates a composable icon button with Cupertino styling. + * + * This function creates a button that displays only an icon. It uses the [CupertinoButton] internally + * and applies icon-specific sizing and layout. + * + * @param onClick The action to perform when the button is clicked + * @param modifier Modifier to be applied to the button + * @param enabled Whether the button is enabled and interactive + * @param colors The colors to use for the button appearance + * @param border The border stroke to apply to the button, or null for no border + * @param interactionSource The interaction source to track button interactions + * @param content The icon content to display inside the button + * + * Sample usage: + * ``` + * CupertinoIconButton(onClick = { /* handle click */ }) { + * CupertinoIcon(CupertinoIcons.star) + * } + * ``` + */ @ExperimentalCupertinoApi @Composable fun CupertinoIconButton( @@ -181,6 +236,19 @@ fun CupertinoIconButton( } +/** + * Represents the color configuration for Cupertino buttons. + * + * This immutable class holds color values for different button states (enabled, disabled) + * and different visual styles (filled, plain). + * + * @property containerColor The container color when the button is enabled + * @property contentColor The content color when the button is enabled + * @property disabledContainerColor The container color when the button is disabled + * @property disabledContentColor The content color when the button is disabled + * @property indicationColor The color used for visual feedback during interactions + * @property isPlain Indicates if this is a plain button style (no background) + */ @Immutable class CupertinoButtonColors internal constructor( private val containerColor: Color, @@ -190,23 +258,25 @@ class CupertinoButtonColors internal constructor( internal val indicationColor: Color, internal val isPlain: Boolean = false ) { - /** - * Represents the container color for this button, depending on [enabled]. - * - * @param enabled whether the button is enabled - */ - @Composable - fun containerColor(enabled: Boolean): State { +/** + * Represents the container color for this button, depending on [enabled]. + * + * @param enabled whether the button is enabled + * @return The appropriate container color based on enabled state + */ +@Composable +fun containerColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) containerColor else disabledContainerColor) } - /** - * Represents the content color for this button, depending on [enabled]. - * - * @param enabled whether the button is enabled - */ - @Composable - fun contentColor(enabled: Boolean): State { +/** + * Represents the content color for this button, depending on [enabled]. + * + * @param enabled whether the button is enabled + * @return The appropriate content color based on enabled state + */ +@Composable +fun contentColor(enabled: Boolean): State { return rememberUpdatedState(if (enabled) contentColor else disabledContentColor) } @@ -232,15 +302,32 @@ class CupertinoButtonColors internal constructor( } +/** + * The default values and color configurations for Cupertino buttons. + * + * This object provides factory methods to create different button color schemes + * that match Apple's Cupertino design guidelines, including filled, plain, + * tinted, and gray button styles. + */ @Immutable object CupertinoButtonDefaults { - /** - * Tinted button with .bordered SwiftUI style with default gray tint - * */ - @Composable - @ReadOnlyComposable - fun grayButtonColors( +/** + * Tinted button with .bordered SwiftUI style with default gray tint + * + * Creates a button with a gray background and accent text color. + * + * @param contentColor The color of the button's text/content + * @param containerColor The background color of the button + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + */ +@Composable +@ReadOnlyComposable +fun grayButtonColors( contentColor: Color = CupertinoTheme.colorScheme.accent, containerColor: Color = CupertinoTheme.colorScheme.quaternarySystemFill, disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -254,12 +341,22 @@ object CupertinoButtonDefaults { indicationColor = indicationColor, ) - /** - * SwiftUI .borderless button - * */ - @Composable - @ReadOnlyComposable - fun plainButtonColors( +/** + * SwiftUI .borderless button + * + * Creates a button with no background that uses transparent container colors. + * + * @param contentColor The color of the button's text/content + * @param containerColor The background color of the button (default transparent) + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + */ +@Composable +@ReadOnlyComposable +fun plainButtonColors( contentColor: Color = CupertinoTheme.colorScheme.accent, containerColor: Color = Color.Transparent, disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -274,12 +371,22 @@ object CupertinoButtonDefaults { indicationColor = indicationColor ) - /** - * Filled button with .borderedProminent SwiftUI style - * */ - @Composable - @ReadOnlyComposable - fun filledButtonColors( +/** + * Filled button with .borderedProminent SwiftUI style + * + * Creates a button with a prominent filled appearance with white text. + * + * @param contentColor The color of the button's text/content (default white) + * @param containerColor The background color of the button (default accent color) + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + */ +@Composable +@ReadOnlyComposable +fun filledButtonColors( contentColor: Color = Color.White, containerColor: Color = CupertinoTheme.colorScheme.accent, disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -293,12 +400,22 @@ object CupertinoButtonDefaults { indicationColor = indicationColor ) - /** - * Tinted button with .bordered SwiftUI style and [contentColor] tint - * */ - @Composable - @ReadOnlyComposable - fun tintedButtonColors( +/** + * Tinted button with .bordered SwiftUI style and [contentColor] tint + * + * Creates a button with a tinted background based on the content color. + * + * @param contentColor The color of the button's text/content + * @param containerColor The background color of the button (computed from contentColor) + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + */ +@Composable +@ReadOnlyComposable +fun tintedButtonColors( contentColor: Color = CupertinoTheme.colorScheme.accent, containerColor: Color = contentColor.copy(alpha = CupertinoButtonTokens.BorderedButtonAlpha), disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -312,19 +429,29 @@ object CupertinoButtonDefaults { indicationColor = indicationColor, ) - /** - * Filled button with .borderedProminent SwiftUI style - * */ - @Composable - @ReadOnlyComposable - @Deprecated( - "Use filledButtonColors instead", - replaceWith = ReplaceWith( - "filledButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", - "import zone.ien.hig.filledButtonColors" - ) +/** + * Filled button with .borderedProminent SwiftUI style + * + * @param contentColor The color of the button's text/content (default white) + * @param containerColor The background color of the button (default accent color) + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + * + * @deprecated Use [filledButtonColors] instead + */ +@Composable +@ReadOnlyComposable +@Deprecated( + "Use filledButtonColors instead", + replaceWith = ReplaceWith( + "filledButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", + "import zone.ien.hig.filledButtonColors" ) - fun borderedProminentButtonColors( +) +fun borderedProminentButtonColors( contentColor: Color = Color.White, containerColor: Color = CupertinoTheme.colorScheme.accent, disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -338,19 +465,29 @@ object CupertinoButtonDefaults { indicationColor = indicationColor ) - /** - * Tinted button with .bordered SwiftUI style and [contentColor] tint - * */ - @Deprecated( - "Use tintedButtonColors instead", - replaceWith = ReplaceWith( - "tintedButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", - "import zone.ien.hig.tintedButtonColors" - ) +/** + * Tinted button with .bordered SwiftUI style and [contentColor] tint + * + * @param contentColor The color of the button's text/content + * @param containerColor The background color of the button (computed from contentColor) + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + * + * @deprecated Use [tintedButtonColors] instead + */ +@Composable +@ReadOnlyComposable +@Deprecated( + "Use tintedButtonColors instead", + replaceWith = ReplaceWith( + "tintedButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", + "import zone.ien.hig.tintedButtonColors" ) - @Composable - @ReadOnlyComposable - fun borderedButtonColors( +) +fun borderedButtonColors( contentColor: Color = CupertinoTheme.colorScheme.accent, containerColor: Color = contentColor.copy(alpha = CupertinoButtonTokens.BorderedButtonAlpha), disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -364,19 +501,29 @@ object CupertinoButtonDefaults { indicationColor = indicationColor, ) - /** - * SwiftUI .borderless button - * */ - @Deprecated( - "Use plainButtonColors instead", - replaceWith = ReplaceWith( - "plainButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", - "import zone.ien.hig.plainButtonColors" - ) +/** + * SwiftUI .borderless button + * + * @param contentColor The color of the button's text/content + * @param containerColor The background color of the button (default transparent) + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + * + * @deprecated Use [plainButtonColors] instead + */ +@Composable +@ReadOnlyComposable +@Deprecated( + "Use plainButtonColors instead", + replaceWith = ReplaceWith( + "plainButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", + "import zone.ien.hig.plainButtonColors" ) - @Composable - @ReadOnlyComposable - fun borderlessButtonColors( +) +fun borderlessButtonColors( contentColor: Color = CupertinoTheme.colorScheme.accent, containerColor: Color = Color.Transparent, disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -390,19 +537,29 @@ object CupertinoButtonDefaults { indicationColor = indicationColor ) - /** - * Tinted button with .bordered SwiftUI with default tint - * */ - @Deprecated( - "Use grayButtonColors instead", - replaceWith = ReplaceWith( - "grayButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", - "import zone.ien.hig.grayButtonColors" - ) +/** + * Tinted button with .bordered SwiftUI with default tint + * + * @param contentColor The color of the button's text/content + * @param containerColor The background color of the button + * @param disabledContentColor The color of the button's text/content when disabled + * @param disabledContainerColor The background color of the button when disabled + * @param indicationColor The color used for visual feedback during interactions + * + * @return A [CupertinoButtonColors] object with the specified configuration + * + * @deprecated Use [grayButtonColors] instead + */ +@Composable +@ReadOnlyComposable +@Deprecated( + "Use grayButtonColors instead", + replaceWith = ReplaceWith( + "grayButtonColors(contentColor,containerColor,disabledContentColor,disabledContainerColor,indicationColor)", + "import zone.ien.hig.grayButtonColors" ) - @Composable - @ReadOnlyComposable - fun borderedGrayButtonColors( +) +fun borderedGrayButtonColors( contentColor: Color = CupertinoTheme.colorScheme.accent, containerColor: Color = CupertinoTheme.colorScheme.quaternarySystemFill, disabledContentColor: Color = CupertinoTheme.colorScheme.tertiaryLabel, @@ -417,6 +574,11 @@ object CupertinoButtonDefaults { ) } +/** + * Internal token values used for button styling and behavior. + * + * This object contains constants used for button animations, sizing, and other styling properties. + */ internal object CupertinoButtonTokens { const val PressedPlainButonAlpha = .33f val IconButtonSize = 42.dp diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoCheckbox.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoCheckbox.kt index 7b866a60..d26bae1b 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoCheckbox.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoCheckbox.kt @@ -49,6 +49,18 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.util.lerp import zone.ien.hig.theme.CupertinoTheme +/** + * Cupertino-style checkbox composable. + * + * This composable creates a checkbox that follows Cupertino design guidelines. + * + * @param checked Whether the checkbox is currently checked + * @param onCheckedChange Called when the user clicks the checkbox to change its state + * @param modifier The modifier to be applied to the checkbox + * @param enabled Whether the checkbox is enabled or not + * @param colors The colors to be used for the checkbox in different states + * @param interactionSource The interaction source for the checkbox + */ @Composable fun CupertinoCheckBox( checked: Boolean, diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenu.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenu.kt index c53ea7ba..1286d0ed 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenu.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoDropdownMenu.kt @@ -1,4 +1,4 @@ -/* +/** * Copyright (c) 2023-2024. Compose Cupertino project and open source contributors. * Copyright (c) 2025. Scott Lanoue. * Copyright (c) 2026. IENGROUND of IENLAB. @@ -16,8 +16,9 @@ * limitations under the License. */ - - +/** + * Contains the implementation of Cupertino-style dropdown menus. + */ package zone.ien.hig import androidx.compose.animation.core.LinearOutSlowInEasing @@ -41,7 +42,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CornerBasedShape import androidx.compose.foundation.verticalScroll @@ -58,11 +59,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.drawscope.translate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.layout.SubcomposeLayout @@ -82,7 +81,6 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastForEach -import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastSumBy import androidx.compose.ui.util.lerp @@ -105,7 +103,6 @@ import zone.ien.hig.theme.BrightSeparatorColor import zone.ien.hig.theme.CupertinoColors import zone.ien.hig.theme.CupertinoTheme import zone.ien.hig.theme.systemGray5 -import zone.ien.hig.theme.systemRed import zone.ien.hig.utils.InteractiveHighlight import kotlin.math.abs import kotlin.math.atan2 @@ -115,17 +112,26 @@ import kotlin.math.min import kotlin.math.sin import kotlin.math.tanh +/** + * A scope for defining menu items within a dropdown menu. + */ sealed interface CupertinoMenuScope /** - * Cupertino elevated dropdown menu. Usually used for top bar actions. + * Creates a Cupertino-style dropdown menu. * - * @see MenuSection - * @see MenuTitle - * @see MenuAction - * @see MenuPickerAction - * @see MenuDivider - * */ + * @param expanded Whether the dropdown menu is currently visible. + * @param onDismissRequest Called when the user tries to dismiss the menu. + * @param modifier Modifier to be applied to the menu. + * @param offset Offset for positioning the menu. + * @param paddingValues Padding values for the menu content. + * @param containerColor Color for the menu background. + * @param width Width of the dropdown menu. + * @param scrollState Scroll state for the menu content. + * @param properties Popup properties for the menu. + * @param backdrop Backdrop effect to apply to the menu. + * @param content The content of the dropdown menu. + */ @Composable @ExperimentalCupertinoApi fun CupertinoDropdownMenu( @@ -144,12 +150,9 @@ fun CupertinoDropdownMenu( val expandedStates = remember { MutableTransitionState(false) } expandedStates.targetState = expanded val safePadding = 32.dp -// val density = LocalDensity.current -// val safePaddingPx = with(density) { safePadding.roundToPx() } if (expandedStates.currentState || expandedStates.targetState) { var transformOrigin by remember { mutableStateOf(TransformOrigin.Center) } -// var menuOffset by remember { mutableStateOf(Offset.Zero) } val density = LocalDensity.current val popupPositionProvider = DropdownMenuPositionProvider( contentOffset = offset, @@ -158,10 +161,6 @@ fun CupertinoDropdownMenu( density = density ) { parentBounds, menuBounds -> transformOrigin = calculateTransformOrigin(parentBounds, menuBounds) -// menuOffset = Offset( -// menuBounds.left.toFloat() + safePaddingPx, -// menuBounds.top.toFloat() + safePaddingPx -// ) } Popup( @@ -176,29 +175,23 @@ fun CupertinoDropdownMenu( expandedStates = expandedStates, transformOriginState = transformOrigin, scrollState = scrollState, - modifier = modifier.padding(safePadding), content = { scope.run { content() } }, width = width, paddingValue = paddingValues, backdrop = backdrop, + modifier = modifier.padding(safePadding) ) } } } /** - * Plain menu item with manual padding control. - * Usually shouldn't be used directly. - * - * @param minHeight minimum item height - * @param content item content + * Creates a menu item within the dropdown menu. * - * @see MenuSection - * @see MenuTitle - * @see MenuAction - * @see MenuPickerAction - * @see MenuDivider - * */ + * @param modifier Modifier to be applied to the menu item. + * @param minHeight Minimum height for the menu item. + * @param content Content to be displayed within the menu item. + */ @Composable fun CupertinoMenuScope.MenuItem( modifier: Modifier = Modifier, @@ -208,81 +201,73 @@ fun CupertinoMenuScope.MenuItem( this as CupertinoMenuScopeImpl Box( - modifier = modifier.heightIn(minHeight), contentAlignment = Alignment.CenterStart, + modifier = modifier.heightIn(minHeight) ) { content( MenuPaddingValues.let { - if (!hasPicker) { - it - } else { - it.copy( - start = it.calculateStartPadding(LocalLayoutDirection.current) + SelectorSize, - ) - } + if (!hasPicker) it + else it.copy( + start = it.calculateStartPadding(LocalLayoutDirection.current) + SelectorSize, + ) }, ) } } /** - * Group of buttons with top [MenuTitle] and bottom [MenuDivider] + * Creates a menu section with an optional title. * - * @see MenuTitle - * @see MenuDivider - * */ + * @param title Optional title for the menu section. + * @param content Content to be displayed within the menu section. + */ @Composable inline fun CupertinoMenuScope.MenuSection( noinline title: (@Composable () -> Unit)? = null, content: @Composable CupertinoMenuScope.() -> Unit, ) { - if (title != null) { - MenuTitle(title = title) - } + if (title != null) MenuTitle(title = title) content() } /** - * Title of the [MenuSection] - * */ + * Creates a menu title within the dropdown menu. + * + * @param modifier Modifier to be applied to the menu title. + * @param title The title content to be displayed. + */ @Composable fun CupertinoMenuScope.MenuTitle( modifier: Modifier = Modifier, title: @Composable () -> Unit, ) { - MenuItem( - modifier = modifier, - minHeight = MinTitleHeight, - ) { + MenuItem(modifier = modifier, minHeight = MinTitleHeight) { CompositionLocalProvider( LocalContentColor provides CupertinoTheme.colorScheme.secondaryLabel, ) { - ProvideTextStyle( - CupertinoTheme.typography.footnote, - ) { + ProvideTextStyle(CupertinoTheme.typography.footnote) { Box( - Modifier - .padding(it), - ) { - title() - } + modifier = Modifier + .padding(it) + .padding(horizontal = 8.dp) + ) { title() } } } } } /** - * Default menu button - - * @param onClick block performed on action click - * @param modifier item modifier - * @param onClickLabel semantics label for the [onClick] action. Should be the same text as in [title] - * @param contentColor color of the item contend. - * Usually [CupertinoColors.systemRed] is used for destructive actions. - * @param icon action trailing icon - * @param caption content before [icon] - * @param title action title - * */ + * Creates a menu action within the dropdown menu. + * + * @param onClick Called when the menu action is clicked. + * @param modifier Modifier to be applied to the menu action. + * @param onClickLabel Optional label for accessibility when the action is clicked. + * @param enabled Whether the menu action is enabled. + * @param contentColor Color for the menu action content. + * @param leadingIcon Optional leading icon for the menu action. + * @param trailingIcon Optional trailing icon for the menu action. + * @param title The title content to be displayed. + */ @Composable fun CupertinoMenuScope.MenuAction( onClick: () -> Unit, @@ -290,42 +275,48 @@ fun CupertinoMenuScope.MenuAction( onClickLabel: String? = null, enabled: Boolean = true, contentColor: Color = CupertinoDropdownMenuDefaults.ContentColor, - icon: (@Composable () -> Unit) = {}, - caption: @Composable () -> Unit = {}, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, title: @Composable () -> Unit, -) = ActionWithoutPadding( - onClickLabel = onClickLabel, - modifier = modifier, - onClick = onClick, - enabled = enabled, - contentColor = contentColor, - icon = icon, - caption = caption, -) -{ - Box( - modifier = Modifier.padding(it), +) { + this as CupertinoMenuScopeImpl + + // If at least one item has an icon, set hasIcon to true + if (leadingIcon != null) { + DisposableEffect(this) { + val prev = hasIcon + hasIcon = true + onDispose { hasIcon = prev } + } + } + + ActionWithoutPadding( + onClickLabel = onClickLabel, + modifier = modifier, + onClick = onClick, + enabled = enabled, + contentColor = contentColor, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, ) { - title() + Box(modifier = Modifier.padding(it)) { title() } } } /** - * Picker action with leading icon ([Checkmark] by default) if selected. - * - * If menu has at least one picker action (no matter selected or not) - * then all menu items will have additional start padding + * Creates a menu picker action within the dropdown menu. * - * @param isSelected selection flag. If item is selected, it will have a [selectionIcon] - * @param onClick block performed on action click - * @param modifier item modifier - * @param onClickLabel semantics label for the [onClick] action. Should be the same text as in [title] - * @param contentColor color of the item contend. - * Usually [CupertinoColors.systemRed] is used for destructive actions. - * @param icon action trailing icon - * @param caption content before [icon] - * @param title action title - * */ + * @param isSelected Whether the menu picker action is currently selected. + * @param onClick Called when the menu picker action is clicked. + * @param modifier Modifier to be applied to the menu picker action. + * @param onClickLabel Optional label for accessibility when the action is clicked. + * @param enabled Whether the menu picker action is enabled. + * @param contentColor Color for the menu picker action content. + * @param selectionIcon Icon to show when the item is selected. + * @param leadingIcon Optional leading icon for the menu picker action. + * @param trailingIcon Optional trailing icon for the menu picker action. + * @param title The title content to be displayed. + */ @Composable fun CupertinoMenuScope.MenuPickerAction( isSelected: Boolean, @@ -335,8 +326,8 @@ fun CupertinoMenuScope.MenuPickerAction( enabled: Boolean = true, contentColor: Color = CupertinoDropdownMenuDefaults.ContentColor, selectionIcon: (@Composable () -> Unit) = { CupertinoDropdownMenuDefaults.PickerLeadingIcon() }, - icon: (@Composable () -> Unit) = {}, - caption: @Composable () -> Unit = {}, + leadingIcon: (@Composable () -> Unit) = {}, + trailingIcon: @Composable () -> Unit = {}, title: @Composable () -> Unit, ) { this as CupertinoMenuScopeImpl @@ -344,9 +335,7 @@ fun CupertinoMenuScope.MenuPickerAction( DisposableEffect(this) { val prev = hasPicker hasPicker = true - onDispose { - hasPicker = prev - } + onDispose { hasPicker = prev } } ActionWithoutPadding( @@ -358,41 +347,35 @@ fun CupertinoMenuScope.MenuPickerAction( onClick = onClick, enabled = enabled, contentColor = contentColor, - icon = icon, - caption = caption, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, title = { pv -> - Box( - contentAlignment = Alignment.CenterStart, - ) { + Box(contentAlignment = Alignment.CenterStart) { Box( modifier = Modifier.size(MinItemHeight), contentAlignment = Alignment.Center, ) { - if (isSelected) { - selectionIcon() - } - } - Box( - modifier = Modifier.padding(pv), - ) { - title() + if (isSelected) selectionIcon() } + Box(modifier = Modifier.padding(pv)) { title() } } }, ) } /** - * Separator for the menu actions groups - * */ + * Creates a menu divider within the dropdown menu. + * + * @param modifier Modifier to be applied to the menu divider. + * @param color Color for the menu divider. + * @param height Height of the menu divider. + */ @Composable fun CupertinoMenuScope.MenuDivider( modifier: Modifier = Modifier, color: Color? = null, height: Dp = DividerHeight, -) = MenuItem( - minHeight = DividerHeight, -) { +) = MenuItem(minHeight = DividerHeight) { Spacer( modifier = modifier .height(height) @@ -402,6 +385,18 @@ fun CupertinoMenuScope.MenuDivider( ) } +/** + * Creates a menu action without padding, for internal use. + * + * @param onClick Called when the menu action is clicked. + * @param modifier Modifier to be applied to the menu action. + * @param onClickLabel Optional label for accessibility when the action is clicked. + * @param enabled Whether the menu action is enabled. + * @param contentColor Color for the menu action content. + * @param leadingIcon Optional leading icon for the menu action. + * @param trailingIcon Optional trailing icon for the menu action. + * @param title The title content to be displayed. + */ @OptIn(ExperimentalCupertinoApi::class) @Composable private fun CupertinoMenuScope.ActionWithoutPadding( @@ -410,11 +405,14 @@ private fun CupertinoMenuScope.ActionWithoutPadding( onClickLabel: String? = null, enabled: Boolean = true, contentColor: Color = Color.Unspecified, - icon: @Composable () -> Unit = {}, - caption: @Composable () -> Unit = {}, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, title: @Composable (PaddingValues) -> Unit, ) = MenuItem { - val color = contentColor.takeOrElse { LocalContentColor.current }.let { if (enabled) it else it.copy(alpha = it.alpha / 4f) } + this as CupertinoMenuScopeImpl + + val color = contentColor.takeOrElse { LocalContentColor.current } + .let { if (enabled) it else it.copy(alpha = it.alpha / 4f) } ProvideTextStyle(CupertinoTheme.typography.callout) { Row( @@ -423,7 +421,7 @@ private fun CupertinoMenuScope.ActionWithoutPadding( modifier = modifier .heightIn(min = CupertinoSectionTokens.MinHeight) .fillMaxWidth() - .padding(8.dp) + .padding(horizontal = 8.dp) .clip(RoundedRectangle(24.dp)) .clickable( enabled = enabled, @@ -436,53 +434,62 @@ private fun CupertinoMenuScope.ActionWithoutPadding( Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(CupertinoSectionTokens.SplitPadding), - modifier = Modifier.padding(it.copy(end = 0.dp)) + modifier = Modifier.padding(it) ) { - caption.invoke() + when { + // Has icon → display icon + leadingIcon != null -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(MinItemHeight / 2), + ) { leadingIcon() } + } + // No icon but other items have icons → reserve space + hasIcon -> { + Spacer(modifier = Modifier.size(MinItemHeight / 2)) + } + } + + Box(modifier = Modifier.weight(1f)) { + title(it.copy(start = 0.dp)) + } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.size(MinItemHeight / 3), - ) { - icon.invoke() + trailingIcon?.let { icon -> + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.size(MinItemHeight / 2), + ) { icon() } } } - title(it.copy(start = 0.dp)) } } } } /** - * Contains default values used for [CupertinoDropdownMenu]. + * Default values for Cupertino dropdown menu. */ @Immutable object CupertinoDropdownMenuDefaults { val DefaultWidth = 260.dp val SmallWidth = 160.dp - val Elevation = 16.dp - val PaddingValues = PaddingValues(0.dp) val Shape: Shape - @Composable - @ReadOnlyComposable + @Composable @ReadOnlyComposable get() = CupertinoSectionDefaults.shape(SectionStyle.InsetGrouped) val ContainerColor: Color - @Composable - @ReadOnlyComposable + @Composable @ReadOnlyComposable get() = CupertinoTheme.colorScheme.tertiarySystemBackground val ContentColor: Color - @Composable - @ReadOnlyComposable + @Composable @ReadOnlyComposable get() = CupertinoTheme.colorScheme.label val DividerColor: Color - @Composable - @ReadOnlyComposable + @Composable @ReadOnlyComposable get() = CupertinoColors.systemGray5 @Composable @@ -495,6 +502,19 @@ object CupertinoDropdownMenuDefaults { } } +/** + * Creates the content for the dropdown menu. + * + * @param width Width of the menu. + * @param containerColor Color for the menu background. + * @param expandedStates State for the menu expansion animation. + * @param transformOriginState Transform origin for the menu animation. + * @param scrollState Scroll state for the menu content. + * @param paddingValue Padding values for the menu content. + * @param modifier Modifier to be applied to the menu. + * @param backdrop Backdrop effect to apply to the menu. + * @param content The content to be displayed in the menu. + */ @Composable private fun DropdownMenuContent( width: Dp, @@ -507,47 +527,22 @@ private fun DropdownMenuContent( backdrop: Backdrop, content: @Composable () -> Unit, ) { - // Menu open/close animation. val transition = rememberTransition(expandedStates, "DropDownMenu") val animationScope = rememberCoroutineScope() + val density = LocalDensity.current val scale by transition.animateFloat( transitionSpec = { - if (false isTransitioningTo true) { - // Dismissed to expanded - MenuEnterTransition - } else { - // Expanded to dismissed. - MenuExitTransition - } + if (false isTransitioningTo true) MenuEnterTransition else MenuExitTransition }, - ) { - if (it) { - // Menu is expanded. - 1f - } else { - // Menu is dismissed. - .1f - } - } + ) { if (it) 1f else .1f } + val alpha by transition.animateFloat( transitionSpec = { - if (false isTransitioningTo true) { - // Dismissed to expanded - MenuEnterTransition - } else { - MenuExitTransition - } + if (false isTransitioningTo true) MenuEnterTransition else MenuExitTransition }, - ) { - if (it) { - // Menu is expanded. - 1f - } else { - // Menu is dismissed. - 0f - } - } + ) { if (it) 1f else 0f } + val shape = CupertinoDropdownMenuDefaults.Shape val interactiveHighlight = remember(animationScope) { InteractiveHighlight(animationScope = animationScope) } @@ -562,14 +557,10 @@ private fun DropdownMenuContent( transformOrigin = transformOriginState clip = false } - .width(width) + .widthIn(min = width) ) { - CompositionLocalProvider( - LocalSeparatorColor provides BrightSeparatorColor, - ) { - ProvideTextStyle( - CupertinoTheme.typography.body - ) { + CompositionLocalProvider(LocalSeparatorColor provides BrightSeparatorColor) { + ProvideTextStyle(CupertinoTheme.typography.body) { SubcomposeLayout( modifier = modifier .drawBackdrop( @@ -583,12 +574,10 @@ private fun DropdownMenuContent( } }, layerBlock = { - val width = this.size.width - val height = this.size.height - + val w = this.size.width + val h = this.size.height val progress = interactiveHighlight.pressProgress - val scale = lerp(1f, 1f + 4.dp.toPx() / height, progress) - + val s = lerp(1f, 1f + 4.dp.toPx() / h, progress) val maxOffset = this.size.minDimension val initialDerivative = 0.05f val offset = interactiveHighlight.offset @@ -596,26 +585,36 @@ private fun DropdownMenuContent( translationX = maxOffset * tanh(initialDerivative * offset.x / maxOffset) translationY = maxOffset * tanh(initialDerivative * offset.y / maxOffset) - val maxDragScale = 4.dp.toPx() / height + val maxDragScale = 4.dp.toPx() / h val offsetAngle = atan2(offset.y, offset.x) - scaleX = scale + maxDragScale * abs(cos(offsetAngle) * offset.x / this.size.maxDimension) * (width / height).fastCoerceAtMost(1f) - scaleY = scale + maxDragScale * abs(sin(offsetAngle) * offset.y / this.size.maxDimension) * (height / width).fastCoerceAtMost(1f) + scaleX = s + maxDragScale * abs(cos(offsetAngle) * offset.x / this.size.maxDimension) * (w / h).fastCoerceAtMost(1f) + scaleY = s + maxDragScale * abs(sin(offsetAngle) * offset.y / this.size.maxDimension) * (h / w).fastCoerceAtMost(1f) }, onDrawSurface = { drawRect(containerColor.copy(alpha = 0.95f)) }, ) - .fillMaxWidth() + .padding(vertical = 8.dp) .heightIn(max = MenuMaxHeight) .verticalScroll(scrollState), ) { constraints -> - val layoutWidth = constraints.maxWidth - val itemPlaceables = subcompose(CupertinoDropdownMenuSlots.Item, content).fastMap { it.measure(constraints) } - val allPlacements = buildList(itemPlaceables.size * 2) { itemPlaceables.fastForEach { placeable -> add(placeable) } } - val height = allPlacements.fastSumBy { it.height } + val minWidth = with(density) { width.roundToPx() } + val itemConstraints = constraints.copy( + minWidth = minWidth, + maxWidth = constraints.maxWidth + ) + val itemPlaceables = subcompose(CupertinoDropdownMenuSlots.Item, content) + .fastMap { it.measure(itemConstraints) } + val allPlacements = buildList(itemPlaceables.size * 2) { + itemPlaceables.fastForEach { add(it) } + } + + val layoutWidth = allPlacements.maxOfOrNull { it.width } + ?.coerceAtLeast(minWidth) ?: minWidth + val layoutHeight = allPlacements.fastSumBy { it.height } - layout(layoutWidth, height) { + layout(layoutWidth, layoutHeight) { var y = 0 allPlacements.fastForEach { it.placeRelative(0, y) @@ -628,47 +627,34 @@ private fun DropdownMenuContent( } } -private enum class CupertinoDropdownMenuSlots { - Section, - Item, - Separator, -} +private enum class CupertinoDropdownMenuSlots { Section, Item, Separator } -internal fun calculateTransformOrigin( - parentBounds: IntRect, - menuBounds: IntRect, -): TransformOrigin { - val pivotX = - when { - menuBounds.left >= parentBounds.right -> 0f - menuBounds.right <= parentBounds.left -> 1f - menuBounds.width == 0 -> 0f - else -> { - val intersectionCenter = - ( - max(parentBounds.left, menuBounds.left) + - min( - parentBounds.right, - menuBounds.right, - ) - ) / 2 - (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width - } +/** + * Calculates the transform origin for the dropdown menu animation. + * + * @param parentBounds Bounds of the parent element. + * @param menuBounds Bounds of the menu. + * @return The calculated transform origin. + */ +internal fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin { + val pivotX = when { + menuBounds.left >= parentBounds.right -> 0f + menuBounds.right <= parentBounds.left -> 1f + menuBounds.width == 0 -> 0f + else -> { + val intersectionCenter = (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2 + (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width } - val pivotY = - when { - menuBounds.top >= parentBounds.bottom -> 0f - menuBounds.bottom <= parentBounds.top -> 1f - menuBounds.height == 0 -> 0f - else -> { - val intersectionCenter = - ( - max(parentBounds.top, menuBounds.top) + - min(parentBounds.bottom, menuBounds.bottom) - ) / 2 - (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height - } + } + val pivotY = when { + menuBounds.top >= parentBounds.bottom -> 0f + menuBounds.bottom <= parentBounds.top -> 1f + menuBounds.height == 0 -> 0f + else -> { + val intersectionCenter = (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2 + (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height } + } return TransformOrigin(pivotX, pivotY) } @@ -693,55 +679,27 @@ internal data class DropdownMenuPositionProvider( } val contentOffsetY = with(density) { contentOffset.y.roundToPx() } - // popupContentSize는 이미 safePadding * 2를 포함한 크기 - // 실제 메뉴 크기 = popupContentSize - safePadding * 2 - // Popup 자체는 safePadding만큼 앞당겨서 배치해야 내부 컨텐츠가 anchor에 붙음 val leftToAnchorLeft = anchorBounds.left + contentOffsetX - safePaddingPx val rightToAnchorRight = anchorBounds.right - popupContentSize.width + contentOffsetX + safePaddingPx val rightToWindowRight = windowSize.width - popupContentSize.width val leftToWindowLeft = 0 val x = if (layoutDirection == LayoutDirection.Ltr) { - sequenceOf( - leftToAnchorLeft, - rightToAnchorRight, - if (anchorBounds.left >= 0) rightToWindowRight else leftToWindowLeft, - ) + sequenceOf(leftToAnchorLeft, rightToAnchorRight, if (anchorBounds.left >= 0) rightToWindowRight else leftToWindowLeft) } else { - sequenceOf( - rightToAnchorRight, - leftToAnchorLeft, - if (anchorBounds.right <= windowSize.width) leftToWindowLeft else rightToWindowRight, - ) + sequenceOf(rightToAnchorRight, leftToAnchorLeft, if (anchorBounds.right <= windowSize.width) leftToWindowLeft else rightToWindowRight) }.firstOrNull { it >= -safePaddingPx && it + popupContentSize.width <= windowSize.width + safePaddingPx } ?: rightToAnchorRight -// val topToAnchorTop = maxOf( -// anchorBounds.top + contentOffsetY - safePaddingPx, -// verticalMargin - safePaddingPx -// ) val topToAnchorTop = anchorBounds.top + contentOffsetY - safePaddingPx val bottomToAnchorBottom = anchorBounds.bottom - popupContentSize.height + contentOffsetY + safePaddingPx val bottomToWindowBottom = windowSize.height - popupContentSize.height - verticalMargin + safePaddingPx -// val y = sequenceOf( -// topToAnchorTop, -// bottomToAnchorBottom, -// bottomToWindowBottom, -// ).firstOrNull { -// it + safePaddingPx >= verticalMargin && -// it + popupContentSize.height - safePaddingPx <= windowSize.height - verticalMargin -// } ?: bottomToAnchorBottom - - val y = sequenceOf( - topToAnchorTop, - bottomToAnchorBottom, - bottomToWindowBottom, - ).firstOrNull { - it + popupContentSize.height - safePaddingPx <= windowSize.height - verticalMargin - // 👆 상단 조건(verticalMargin 체크) 제거 - } ?: bottomToAnchorBottom + val y = sequenceOf(topToAnchorTop, bottomToAnchorBottom, bottomToWindowBottom) + .firstOrNull { + it + popupContentSize.height - safePaddingPx <= windowSize.height - verticalMargin + } ?: bottomToAnchorBottom onPositionCalculated( anchorBounds, @@ -751,13 +709,14 @@ internal data class DropdownMenuPositionProvider( } } +// ✅ Added hasIcon internal class CupertinoMenuScopeImpl : CupertinoMenuScope { var hasPicker: Boolean by mutableStateOf(false) + var hasIcon: Boolean by mutableStateOf(false) } private val MenuMaxHeight: Dp = 600.dp private val SelectorSize = 20.dp - private val MenuHorizontalMargin = 24.dp private val MenuVerticalMargin = 24.dp private val MinItemHeight = 48.dp @@ -766,14 +725,12 @@ private val MinTitleHeight = 32.dp private val SplitPadding = 16.dp private val MenuPaddingValues = PaddingValues(16.dp, 8.dp) -private val MenuEnterTransition = - spring( - dampingRatio = .825f, - stiffness = Spring.StiffnessMediumLow, - ) +private val MenuEnterTransition = spring( + dampingRatio = .825f, + stiffness = Spring.StiffnessMediumLow, +) -private val MenuExitTransition = - tween( - durationMillis = 350, - easing = LinearOutSlowInEasing, - ) +private val MenuExitTransition = tween( + durationMillis = 350, + easing = LinearOutSlowInEasing, +) \ No newline at end of file diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoLiquidButton.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoLiquidButton.kt index 1eabeb31..dbb1bd95 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoLiquidButton.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoLiquidButton.kt @@ -61,6 +61,32 @@ import kotlin.math.tanh import androidx.compose.animation.Animatable as ColorAnimatable import androidx.compose.animation.core.Animatable as FloatAnimatable +/** + * A composable function that creates a Cupertino-styled liquid button with backdrop effects. + * + * This button features liquid glass-like effects with adaptive tinting and surface colors that + * respond to the background theme and content luminance. + * + * @param onClick The action to perform when the button is clicked + * @param modifier Modifier to be applied to the button + * @param enabled Whether the button is enabled and interactive + * @param size The size of the button (Small, Regular, Large, ExtraLarge) + * @param colors The colors to use for the button appearance + * @param shape The shape of the button + * @param contentPadding The padding to apply to the button content + * @param interactionSource The interaction source to track button interactions + * @param backdrop The backdrop effect configuration for the button's visual appearance + * @param isBackgroundAdaptive Whether the button should adapt to the background color + * @param isInteractive Whether the button responds to interactive gestures + * @param content The content to display inside the button + * + * Sample usage: + * ``` + * CupertinoLiquidButton(onClick = { /* handle click */ }, backdrop = backdrop) { + * Text("Button") + * } + * ``` + */ @Composable @ExperimentalCupertinoApi fun CupertinoLiquidButton( @@ -119,7 +145,7 @@ fun CupertinoLiquidButton( tween(300) ) } catch (e: RuntimeException) { - // layer가 dispose된 경우 → 루프 종료 + // When layer is disposed → exit loop break } } @@ -243,7 +269,7 @@ fun CupertinoLiquidIconButton( ) { CupertinoLiquidButton( onClick = onClick, - modifier = modifier.size(CupertinoButtonTokens.IconButtonSize), + modifier = modifier.size(CupertinoLiquidButtonTokens.IconButtonSize), enabled = enabled, colors = colors, size = CupertinoButtonSize.Regular, @@ -445,7 +471,7 @@ object CupertinoLiquidButtonDefaults { internal object CupertinoLiquidButtonTokens { const val PressedPlainButonAlpha = .33f - val IconButtonSize = 42.dp + val IconButtonSize = 48.dp const val BorderedButtonAlpha = .2f } diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBar.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBar.kt index 5f7791e0..9532ef5f 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBar.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoNavigationBar.kt @@ -16,15 +16,16 @@ * limitations under the License. */ - - +/** + * Contains the implementation of the Cupertino navigation bar. + */ package zone.ien.hig import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.EaseOut import androidx.compose.animation.core.spring +import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement @@ -37,10 +38,13 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Immutable @@ -68,6 +72,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -78,7 +84,6 @@ import com.kyant.backdrop.backdrops.LayerBackdrop import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberCombinedBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import zone.ien.hig.utils.rememberDefaultBackdrop import com.kyant.backdrop.drawBackdrop import com.kyant.backdrop.effects.blur import com.kyant.backdrop.effects.lens @@ -93,13 +98,18 @@ import kotlinx.coroutines.launch import zone.ien.hig.theme.CupertinoTheme import zone.ien.hig.utils.DampedDragAnimation import zone.ien.hig.utils.InteractiveHighlight +import zone.ien.hig.utils.rememberDefaultBackdrop import kotlin.math.abs import kotlin.math.sign +private val NavBarPadding = 4.dp +private val NavBarItemGap = 0.dp +private val NavBarItemMinWidth = 90.dp // Fixed width when items are few + /** - * Cupertino bottom navigation tab bar + * Cupertino bottom navigation tab bar. * - * [CupertinoNavigationBarItem]s should be used as navigation bar content + * [CupertinoNavigationBarItem]s should be used as navigation bar content. * * Note: navigation bar itself does not produce cupertino thin material glass effect. * This effect works only inside [CupertinoScaffold], [CupertinoBottomSheetScaffold], [CupertinoBottomSheetContent]. @@ -107,18 +117,6 @@ import kotlin.math.sign * function that will communicate with scaffold and return either * [Color.Transparent] if color was successfully applied to scaffold (and top bar itself * should be transparent) or passed color if scaffold wasn't found. - * - * @param modifier the [Modifier] to be applied to this top app bar. - * @param windowInsets a window insets that app bar will respect. - * @param containerColor color of the navigation bar background. - * @param isTransparent navigation bar is usually transparent if scroll container reached bottom. - * [ScrollableState.isNavigationBarTransparent] and [LazyListState.isNavigationBarTransparent] can be used to track it - * @param isTranslucent works only inside [CupertinoScaffold]. Blurred content behind navigation bar will be - * visible if navigation bar is translucent. Simulates iOS app bars material. - * @param divider top divider when [isTransparent] is false - * @param content navigation bar content, usually [CupertinoNavigationBarItem] - * - * @see CupertinoNavigationBarItem */ @Composable @ExperimentalCupertinoApi @@ -137,303 +135,270 @@ fun CupertinoNavigationBar( val accentColor = colors.accentColor val containerColor = colors.containerColor.copy(0.6f) - BoxWithConstraints( - contentAlignment = Alignment.CenterStart, + Box( modifier = modifier - .navigationBarsPadding() + .fillMaxWidth() + .padding(bottom = CupertinoNavigationBarDefaults.BottomPadding) + .wrapContentWidth() .windowInsetsPadding(windowInsets) ) { - val density = LocalDensity.current - val tabWidth = with(density) { - (constraints.maxWidth.toFloat() - 8.dp.toPx()) / tabsCount - } + // Calculate actual available width after applying windowInsets + BoxWithConstraints( + contentAlignment = Alignment.CenterStart, + ) { + val density = LocalDensity.current - val offsetAnimation = remember { Animatable(0f) } - val panelOffset by remember(density) { - derivedStateOf { - val fraction = (offsetAnimation.value / constraints.maxWidth).fastCoerceIn(-1f, 1f) - with(density) { - 4.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction)) - } + val paddingPx = with(density) { NavBarPadding.toPx() } + val gapPx = with(density) { NavBarItemGap.toPx() } + val minItemWidthPx = with(density) { NavBarItemMinWidth.toPx() } + + // Calculate item width based on available width + // NavBar total width = padding*2 + itemWidth*n + gap*(n-1) + // → itemWidth = (availableWidth - padding*2 - gap*(n-1)) / n + val availableWidthPx = constraints.maxWidth.toFloat() + val calculatedItemWidthPx = if (tabsCount > 0) { + (availableWidthPx - paddingPx * 2f - gapPx * (tabsCount - 1)) / tabsCount + } else { + 0f } - } - val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr - val animationScope = rememberCoroutineScope() - var currentIndex by remember(selectedTabIndex) { - mutableIntStateOf(selectedTabIndex()) - } - val dampedDragAnimation = remember(animationScope) { - DampedDragAnimation( - animationScope = animationScope, - initialValue = selectedTabIndex().toFloat(), - valueRange = 0f..(tabsCount - 1).toFloat(), - visibilityThreshold = 0.001f, - initialScale = 1f, - pressedScale = 78f / 56f, - onDragStarted = {}, - onDragStopped = { - val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1) - currentIndex = targetIndex - animateToValue(targetIndex.toFloat()) - animationScope.launch { - offsetAnimation.animateTo( - 0f, - spring(1f, 300f, 0.5f) - ) - } - }, - onDrag = { _, dragAmount -> - updateValue( - (targetValue + dragAmount.x / tabWidth * if (isLtr) 1f else -1f) - .fastCoerceIn(0f, (tabsCount - 1).toFloat()) - ) - animationScope.launch { - offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x) - } - } - ) - } - LaunchedEffect(selectedTabIndex) { - snapshotFlow { selectedTabIndex() } - .collectLatest { index -> - currentIndex = index - } - } - LaunchedEffect(dampedDragAnimation) { - snapshotFlow { currentIndex } - .drop(1) - .collectLatest { index -> - dampedDragAnimation.animateToValue(index.toFloat()) - onTabSelected(index) - } - } + // If evenly distributed width is greater than or equal to minimum width (90dp), use fixed width, otherwise use evenly distributed width + val itemWidthPx = if (calculatedItemWidthPx >= minItemWidthPx) { + minItemWidthPx + } else { + calculatedItemWidthPx + } + val itemWidthDp: Dp = with(density) { itemWidthPx.toDp() } - val interactiveHighlight = remember(animationScope) { - InteractiveHighlight( - animationScope = animationScope, - position = { size, offset -> - Offset( - if (isLtr) (dampedDragAnimation.value + 0.5f) * tabWidth + panelOffset - else size.width - (dampedDragAnimation.value + 0.5f) * tabWidth + panelOffset, - size.height / 2f - ) - } - ) - } + // NavBar total width = padding*2 + itemWidth*n + gap*(n-1) + val rowWidth = (paddingPx * 2f + itemWidthPx * tabsCount + gapPx * (tabsCount - 1)).coerceAtLeast(1f) - Row( - Modifier - .graphicsLayer { - translationX = panelOffset - } - .drawBackdrop( - backdrop = backdrop, - shape = { Capsule() }, - effects = { - vibrancy() - blur(2.dp.toPx()) - lens(24.dp.toPx(), 24.dp.toPx()) - }, - layerBlock = { - val progress = dampedDragAnimation.pressProgress - val scale = lerp(1f, 1f + 16.dp.toPx() / size.width, progress) - scaleX = scale - scaleY = scale - }, - onDrawSurface = { - drawRect(containerColor) + fun itemLeftX(index: Float): Float = paddingPx + (itemWidthPx + gapPx) * index + fun itemCenterX(index: Float): Float = itemLeftX(index) + itemWidthPx / 2f + + val tabStep = itemWidthPx + gapPx + + val offsetAnimation = remember { Animatable(0f) } + val panelOffset by remember(density) { + derivedStateOf { + val fraction = (offsetAnimation.value / rowWidth).fastCoerceIn(-1f, 1f) + with(density) { + 4.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction)) } - ) - .then(interactiveHighlight.modifier) - .height(64.dp) - .fillMaxWidth() - .padding(4.dp), - verticalAlignment = Alignment.CenterVertically, - content = content - ) + } + } - CompositionLocalProvider( - LocalLiquidBottomTabScale provides { - lerp(1f, 1.2f, dampedDragAnimation.pressProgress) + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val animationScope = rememberCoroutineScope() + var currentIndex by remember(selectedTabIndex) { + mutableIntStateOf(selectedTabIndex()) } - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - content = content, - modifier = Modifier - .clearAndSetSemantics {} - .alpha(0f) - .layerBackdrop(tabsBackdrop) - .graphicsLayer { - translationX = panelOffset - } - .drawBackdrop( - backdrop = backdrop, - shape = { Capsule() }, - effects = { - val progress = dampedDragAnimation.pressProgress - vibrancy() - blur(8.dp.toPx()) - lens( - 24.dp.toPx() * progress, - 24.dp.toPx() * progress + val dampedDragAnimation = remember(animationScope) { + DampedDragAnimation( + animationScope = animationScope, + initialValue = selectedTabIndex().toFloat(), + valueRange = 0f..(tabsCount - 1).toFloat(), + visibilityThreshold = 0.001f, + initialScale = 1f, + pressedScale = 78f / 56f, + onDragStarted = {}, + onDragStopped = { + val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1) + currentIndex = targetIndex + animateToValue(targetIndex.toFloat()) + animationScope.launch { + offsetAnimation.animateTo( + 0f, + spring(1f, 300f, 0.5f) ) - }, - highlight = { - val progress = dampedDragAnimation.pressProgress - Highlight.Default.copy(alpha = progress) - }, - onDrawSurface = { - drawRect(if (isLightTheme) Color.White else Color.Black) - drawRect(containerColor) } - ) - .then(interactiveHighlight.modifier) - .height(56.dp) - .fillMaxWidth() - .padding(horizontal = 4.dp) - .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)) - ) - } - - Box( - modifier = Modifier - .padding(horizontal = 4.dp) - .graphicsLayer { - translationX = - if (isLtr) dampedDragAnimation.value * tabWidth + panelOffset - else size.width - (dampedDragAnimation.value + 1f) * tabWidth + panelOffset - } - .then(interactiveHighlight.gestureModifier) - .then(dampedDragAnimation.modifier) - .drawBackdrop( - backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop), - shape = { Capsule() }, - effects = { - val progress = dampedDragAnimation.pressProgress - lens( - 10.dp.toPx() * progress, - 14.dp.toPx() * progress, - chromaticAberration = true - ) - }, - highlight = { - val progress = dampedDragAnimation.pressProgress - Highlight.Default.copy(alpha = progress) - }, - shadow = { - val progress = dampedDragAnimation.pressProgress - Shadow(alpha = progress) - }, - innerShadow = { - val progress = dampedDragAnimation.pressProgress - InnerShadow( - radius = 8.dp * progress, - alpha = progress - ) - }, - layerBlock = { - scaleX = dampedDragAnimation.scaleX - scaleY = dampedDragAnimation.scaleY - val velocity = dampedDragAnimation.velocity / 10f - scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f) - scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f) }, - onDrawSurface = { - val progress = dampedDragAnimation.pressProgress - drawRect( - if (isLightTheme) Color.Black.copy(0.1f) - else Color.White.copy(0.1f), - alpha = 1f - progress + onDrag = { _, dragAmount -> + updateValue( + (targetValue + dragAmount.x / tabStep * if (isLtr) 1f else -1f) + .fastCoerceIn(0f, (tabsCount - 1).toFloat()) ) - drawRect(Color.Black.copy(alpha = 0.03f * progress)) + animationScope.launch { + offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x) + } } ) - .height(56.dp) - .fillMaxWidth(1f / tabsCount) - ) - } -} - -/** - * Item of the [CupertinoNavigationBar] - * - * @param selected if tab with this item is selected - * @param onClick action performed on item click - * @param icon item icon - * @param modifier modifier applied to item container - * @param enabled if this item is clickable - * @param label item label located below the [icon] - * @param alwaysShowLabel if label should always be visible. If this flag is false then - * label will only be visible if this item is selected - * @param colors item colors. See [CupertinoNavigationBarDefaults.itemColors] - * @param interactionSource interaction source of the item click modifier - * */ -/* -@Composable -@ExperimentalCupertinoApi -fun RowScope.CupertinoNavigationBarItem2( - selected: Boolean, - onClick: () -> Unit, - icon: @Composable () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - label: @Composable (() -> Unit)? = null, - alwaysShowLabel: Boolean = true, - pressIndicationEnabled: Boolean = false, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, -) { - val pressed by interactionSource.collectIsPressedAsState() + } - Column( - modifier - .selectable( - selected = selected, - onClick = onClick, - enabled = enabled, - role = Role.Tab, - interactionSource = interactionSource, - indication = null, - ).weight(1f) - .padding(top = 6.dp) - .fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - val iconColor = colors.iconColor(selected, enabled) - val textColor = colors.textColor(selected, enabled) + LaunchedEffect(selectedTabIndex) { + snapshotFlow { selectedTabIndex() } + .collectLatest { index -> + currentIndex = index + } + } + LaunchedEffect(dampedDragAnimation) { + snapshotFlow { currentIndex } + .drop(1) + .collectLatest { index -> + dampedDragAnimation.animateToValue(index.toFloat()) + onTabSelected(index) + } + } - ProvideTextStyle( - value = CupertinoTheme.typography.caption2, - ) { - val alpha = - if (pressIndicationEnabled && pressed && !selected) { - textColor.alpha * CupertinoButtonTokens.PressedPlainButonAlpha - } else { - textColor.alpha - } + val interactiveHighlight = remember(animationScope) { + InteractiveHighlight( + animationScope = animationScope, + position = { size, _ -> + val cx = itemCenterX(dampedDragAnimation.value) + Offset( + if (isLtr) cx + panelOffset + else size.width - cx + panelOffset, + size.height / 2f + ) + } + ) + } + // Pass dynamic itemWidthDp via CompositionLocal CompositionLocalProvider( - LocalContentColor provides iconColor.copy(alpha = alpha), + LocalCupertinoNavItemWidth provides itemWidthDp, ) { - Box( - modifier = Modifier.size(CupertinoIconDefaults.MediumSize), - contentAlignment = Alignment.Center, + // ── Background Row ─────────────────────────────────────────────────────────────── + Row( + modifier = Modifier + .graphicsLayer { + translationX = panelOffset + } + .drawBackdrop( + backdrop = backdrop, + shape = { Capsule() }, + effects = { + vibrancy() + blur(2.dp.toPx()) + lens(24.dp.toPx(), 24.dp.toPx()) + }, + layerBlock = { + val progress = dampedDragAnimation.pressProgress + val scale = lerp(1f, 1f + 16.dp.toPx() / size.width, progress) + scaleX = scale + scaleY = scale + }, + onDrawSurface = { + drawRect(containerColor) + } + ) + .then(interactiveHighlight.modifier) + .wrapContentWidth() + .height(64.dp) + .padding(NavBarPadding), + horizontalArrangement = Arrangement.spacedBy(NavBarItemGap), + verticalAlignment = Alignment.CenterVertically, + content = content + ) + + // ── Accent color overlay Row ────────────────────────────────────────────────────── + CompositionLocalProvider( + LocalLiquidBottomTabScale provides { + lerp(1f, 1.2f, dampedDragAnimation.pressProgress) + } ) { - icon() + Row( + modifier = Modifier + .clearAndSetSemantics {} + .alpha(0f) + .layerBackdrop(tabsBackdrop) + .graphicsLayer { + translationX = panelOffset + } + .drawBackdrop( + backdrop = backdrop, + shape = { Capsule() }, + effects = { + val progress = dampedDragAnimation.pressProgress + vibrancy() + blur(8.dp.toPx()) + lens( + 24.dp.toPx() * progress, + 24.dp.toPx() * progress + ) + }, + highlight = { + val progress = dampedDragAnimation.pressProgress + Highlight.Default.copy(alpha = progress) + }, + onDrawSurface = {} + ) + .then(interactiveHighlight.modifier) + .wrapContentWidth() + .height(56.dp) + .padding(horizontal = NavBarPadding) + .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)), + horizontalArrangement = Arrangement.spacedBy(NavBarItemGap), + verticalAlignment = Alignment.CenterVertically, + content = content + ) } - if (label != null && (alwaysShowLabel || selected)) { - label() - } + // ── Sliding selection indicator Box ─────────────────────────────────────────── + Box( + modifier = Modifier + .graphicsLayer { + val leftX = itemLeftX(dampedDragAnimation.value) + translationX = if (isLtr) { + leftX + panelOffset + } else { + rowWidth - leftX - itemWidthPx + panelOffset + } + } + .then(interactiveHighlight.gestureModifier) + .then(dampedDragAnimation.modifier) + .drawBackdrop( + backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop), + shape = { Capsule() }, + effects = { + val progress = dampedDragAnimation.pressProgress + lens( + 10.dp.toPx() * progress, + 14.dp.toPx() * progress, + chromaticAberration = true + ) + }, + highlight = { + val progress = dampedDragAnimation.pressProgress + Highlight.Default.copy(alpha = progress) + }, + shadow = { + val progress = dampedDragAnimation.pressProgress + Shadow(alpha = progress) + }, + innerShadow = { + val progress = dampedDragAnimation.pressProgress + InnerShadow( + radius = 8.dp * progress, + alpha = progress + ) + }, + layerBlock = { + scaleX = dampedDragAnimation.scaleX + scaleY = dampedDragAnimation.scaleY + val velocity = dampedDragAnimation.velocity / 10f + scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f) + scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f) + }, + onDrawSurface = { + val progress = dampedDragAnimation.pressProgress + drawRect( + if (isLightTheme) Color.Black.copy(0.1f) + else Color.White.copy(0.1f), + alpha = 1f - progress + ) + drawRect(Color.Black.copy(alpha = 0.03f * progress)) + } + ) + .align(Alignment.CenterStart) + .height(56.dp) + .width(itemWidthDp) // ★ Apply dynamic width + ) } } } } - */ - @Composable fun RowScope.CupertinoNavigationBarItem( onClick: () -> Unit, @@ -444,9 +409,10 @@ fun RowScope.CupertinoNavigationBarItem( interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, ) { val scale = LocalLiquidBottomTabScale.current + val itemWidth = LocalCupertinoNavItemWidth.current // ★ Read dynamic width from CompositionLocal Column( - verticalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterVertically), + verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier .clip(Capsule()) @@ -458,21 +424,21 @@ fun RowScope.CupertinoNavigationBarItem( onClick = onClick ) .fillMaxHeight() - .weight(1f) + .width(itemWidth) // ★ Use dynamic width instead of fixed 90.dp .graphicsLayer { - val scale = scale() - scaleX = scale - scaleY = scale + val s = scale() + scaleX = s + scaleY = s } ) { Box( contentAlignment = Alignment.Center, - modifier = Modifier.size(28.dp) + modifier = Modifier.size(18.dp) ) { icon() } ProvideTextStyle( - value = TextStyle(fontSize = 12.sp) + value = TextStyle(fontSize = 10.sp, lineHeight = 10.sp, fontWeight = FontWeight.Bold) ) { label?.invoke() } @@ -491,34 +457,16 @@ class CupertinoNavigationBarColors internal constructor( private val disabledIconColor: Color, private val disabledTextColor: Color, ) { - /** - * Represents the icon color for this item, depending on whether it is [selected]. - * - * @param selected whether the item is selected - * @param enabled whether the item is enabled - */ @Composable - internal fun iconColor( - selected: Boolean, - enabled: Boolean, - ): Color = + internal fun iconColor(selected: Boolean, enabled: Boolean): Color = when { !enabled -> disabledIconColor selected -> selectedIconColor else -> unselectedIconColor } - /** - * Represents the text color for this item, depending on whether it is [selected]. - * - * @param selected whether the item is selected - * @param enabled whether the item is enabled - */ @Composable - internal fun textColor( - selected: Boolean, - enabled: Boolean, - ): Color = + internal fun textColor(selected: Boolean, enabled: Boolean): Color = when { !enabled -> disabledTextColor selected -> selectedTextColor @@ -528,7 +476,6 @@ class CupertinoNavigationBarColors internal constructor( override fun equals(other: Any?): Boolean { if (this === other) return true if (other == null || other !is CupertinoNavigationBarColors) return false - if (selectedIconColor != other.selectedIconColor) return false if (unselectedIconColor != other.unselectedIconColor) return false if (selectedTextColor != other.selectedTextColor) return false @@ -544,25 +491,13 @@ class CupertinoNavigationBarColors internal constructor( result = 31 * result + unselectedTextColor.hashCode() result = 31 * result + disabledIconColor.hashCode() result = 31 * result + disabledTextColor.hashCode() - return result } } - @ExperimentalCupertinoApi @Immutable object CupertinoNavigationBarDefaults { - /** - * Default container color of the [CupertinoNavigationBar] - * - * Note: navigation bar itself does not produce cupertino thin material glass effect. - * This effect works only inside [CupertinoScaffold], [CupertinoBottomSheetScaffold], [CupertinoBottomSheetContent]. - * To achieve this effect with custom top app bar use [cupertinoTranslucentTopBarColor] - * function that will communicate with scaffold and return either - * [Color.Transparent] if color was successfully applied to scaffold (and top bar itself - * should be transparent) or passed color if scaffold wasn't found. - * */ val containerColor: Color @Composable @ReadOnlyComposable @@ -591,6 +526,8 @@ object CupertinoNavigationBarDefaults { ) val windowInsets = WindowInsets(left = 36.dp, right = 36.dp) + val BottomPadding = 24.dp } internal val LocalLiquidBottomTabScale = staticCompositionLocalOf { { 1f } } +internal val LocalCupertinoNavItemWidth = staticCompositionLocalOf { 90.dp } // ★ Dynamic width transmission \ No newline at end of file diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTextField.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTextField.kt index b55c2db1..98d4ecd9 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTextField.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTextField.kt @@ -74,6 +74,34 @@ import zone.ien.hig.theme.CupertinoColors import zone.ien.hig.theme.CupertinoTheme import zone.ien.hig.theme.systemRed +internal expect fun KeyboardOptions.enableNativeInput(): KeyboardOptions + +/** + * A Cupertino-style text field that allows users to enter and edit text. + * + * @param value the text field's input value + * @param onValueChange called when the text field's input value changes + * @param modifier the [Modifier] to be applied to this text field + * @param enabled controls the enabled state of this text field. When `false`, this component will not + * respond to user input and will be displayed as disabled + * @param readOnly controls the read-only state of this text field. When `true`, this component will not + * respond to user input and will be displayed as read-only + * @param textStyle the [TextStyle] to be applied to the text content + * @param placeholder the placeholder content to be displayed when the text field is empty + * @param leadingIcon the icon displayed at the start of the text field + * @param trailingIcon the icon displayed at the end of the text field + * @param isError controls the error state of this text field. When `true`, this component will be displayed + * with error styling + * @param visualTransformation the [VisualTransformation] to be applied to the text content + * @param keyboardOptions the keyboard options configuration for this text field + * @param keyboardActions the keyboard actions configuration for this text field + * @param singleLine when `true`, this text field will be constrained to a single line + * @param maxLines the maximum number of lines this text field can grow to + * @param minLines the minimum number of lines this text field will show + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * @param contentAlignment the vertical alignment of the text field content + * @param colors the [CupertinoTextFieldColors] to be used to configure the text field's appearance + */ @Composable fun CupertinoTextField( value: String, @@ -121,7 +149,7 @@ fun CupertinoTextField( textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError).value), visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, @@ -196,7 +224,7 @@ fun CupertinoTextField( textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError).value), visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), keyboardActions = keyboardActions, interactionSource = interactionSource, singleLine = singleLine, @@ -267,7 +295,7 @@ fun CupertinoTextField( inputTransformation = inputTransformation, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError).value), - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), onKeyboardAction = onKeyboardAction, lineLimits = lineLimits, interactionSource = interactionSource, @@ -340,7 +368,7 @@ fun CupertinoSecureTextField( inputTransformation = inputTransformation, textStyle = mergedTextStyle, cursorBrush = SolidColor(colors.cursorColor(isError).value), - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), onKeyboardAction = onKeyboardAction, interactionSource = interactionSource, onTextLayout = { @@ -475,7 +503,7 @@ fun CupertinoBorderedTextField( trailingIcon = trailingIcon, isError = isError, visualTransformation = visualTransformation, - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), keyboardActions = keyboardActions, singleLine = singleLine, maxLines = maxLines, @@ -530,7 +558,7 @@ fun CupertinoBorderedTextField( leadingIcon = leadingIcon, trailingIcon = trailingIcon, isError = isError, - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), onKeyboardAction = onKeyboardAction, interactionSource = interactionSource, contentAlignment = contentAlignment, @@ -582,7 +610,7 @@ fun CupertinoBorderedSecureTextField( leadingIcon = leadingIcon, trailingIcon = trailingIcon, isError = isError, - keyboardOptions = keyboardOptions, + keyboardOptions = keyboardOptions.enableNativeInput(), onKeyboardAction = onKeyboardAction, interactionSource = interactionSource, contentAlignment = contentAlignment, diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTopBars.kt b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTopBars.kt index bc1c91a7..df56fe4a 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTopBars.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/CupertinoTopBars.kt @@ -302,20 +302,20 @@ fun CupertinoNavigationTitle( val titleAlpha by remember { derivedStateOf { - if (!topAppBarExists) { - 1f // TopBar 없으면 항상 보임 - } else { - val d = offsetDifference - actualTopBarHeight + 50 - // d가 음수면 Large Title 영역 → alpha 1 - // d가 양수면 TopBar로 들어감 → alpha 0 - if (d <= 0) { - 1f + if (!topAppBarExists) { + 1f // TopBar does not exist, always visible } else { - // fadeDistance만큼 부드럽게 0으로 - val fadeDistance = 100f // 조절 가능 (dp → px 변환 권장) - (1f - (d / fadeDistance)).coerceIn(0f, 1f) + val d = offsetDifference - actualTopBarHeight + 50 + // If d is negative, it's in Large Title area → alpha 1 + // If d is positive, it's entering TopBar → alpha 0 + if (d <= 0) { + 1f + } else { + // Fade smoothly to 0 over fadeDistance + val fadeDistance = 100f // Adjustable (recommended: dp → px conversion) + (1f - (d / fadeDistance)).coerceIn(0f, 1f) + } } - } } } @@ -471,7 +471,7 @@ private fun InlineTopAppBar( tween(300) ) } catch (e: RuntimeException) { - // layer가 dispose된 경우 → 루프 종료 + // When layer is disposed → exit loop break } } @@ -642,7 +642,7 @@ private fun TopAppBarLayout( if (constraints.maxWidth == Constraints.Infinity) { constraints.maxWidth } else { - val actionReservedWidth = actionIconsPlaceable.width + TopAppBarHorizontalPadding.roundToPx() * 2 // 또는 고정값 72.dp 등 + val actionReservedWidth = actionIconsPlaceable.width + TopAppBarHorizontalPadding.roundToPx() * 2 // or fixed value 72.dp, etc. (constraints.maxWidth - navigationIconPlaceable.width - actionReservedWidth) .coerceAtLeast(0) } diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/section/LazySectionScope.kt b/hig/src/commonMain/kotlin/zone/ien/hig/section/LazySectionScope.kt index 29822411..b84ebc80 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/section/LazySectionScope.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/section/LazySectionScope.kt @@ -60,7 +60,6 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.kyant.backdrop.backdrops.rememberLayerBackdrop -import zone.ien.hig.utils.rememberDefaultBackdrop import zone.ien.hig.LocalContentColor import zone.ien.hig.CupertinoButtonTokens import zone.ien.hig.CupertinoDatePicker @@ -91,6 +90,7 @@ import zone.ien.hig.icons.outlined.ChevronUp import zone.ien.hig.theme.CupertinoTheme import zone.ien.hig.toStringWithLeadingZero import zone.ien.hig.defaultLocale +import zone.ien.hig.enableNativeInput @Stable sealed interface LazySectionScope { diff --git a/hig/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt b/hig/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt index bf5d9a47..bc2321c8 100644 --- a/hig/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt +++ b/hig/src/commonMain/kotlin/zone/ien/hig/section/SectionScope.kt @@ -82,6 +82,7 @@ import zone.ien.hig.icons.outlined.ChevronUp import zone.ien.hig.theme.CupertinoTheme import zone.ien.hig.toStringWithLeadingZero import zone.ien.hig.defaultLocale +import zone.ien.hig.enableNativeInput @Stable internal object SectionScopeImpl: SectionScope diff --git a/hig/src/iosMain/kotlin/zone/ien/hig/CupertinoTextField.ios.kt b/hig/src/iosMain/kotlin/zone/ien/hig/CupertinoTextField.ios.kt new file mode 100644 index 00000000..dc8ccd89 --- /dev/null +++ b/hig/src/iosMain/kotlin/zone/ien/hig/CupertinoTextField.ios.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023-2024. Compose Cupertino project and open source contributors. + * Copyright (c) 2025. Scott Lanoue. + * Copyright (c) 2026. IENGROUND of IENLAB. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package zone.ien.hig + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.text.input.PlatformImeOptions + +/** + * Enables native input for keyboard options on iOS. + * + * This function enables native input handling for iOS platforms. + * + * @return The [KeyboardOptions] with native input enabled + */ +@OptIn(ExperimentalComposeUiApi::class) +internal actual fun KeyboardOptions.enableNativeInput() = + this +// copy(platformImeOptions = PlatformImeOptions { usingNativeTextInput(true) }) // TODO: Disabled due to keyboard layout breaking issues when using native input on iOS \ No newline at end of file diff --git a/hig/src/iosMain/kotlin/zone/ien/hig/PlatformDateFormat.kt b/hig/src/iosMain/kotlin/zone/ien/hig/PlatformDateFormat.kt index 7e08642f..7330b949 100644 --- a/hig/src/iosMain/kotlin/zone/ien/hig/PlatformDateFormat.kt +++ b/hig/src/iosMain/kotlin/zone/ien/hig/PlatformDateFormat.kt @@ -17,4 +17,3 @@ */ package zone.ien.hig - diff --git a/hig/src/nonIosMain/kotlin/zone/ien/hig/CupertinoTextField.nonIos.kt b/hig/src/nonIosMain/kotlin/zone/ien/hig/CupertinoTextField.nonIos.kt new file mode 100644 index 00000000..f93d9e26 --- /dev/null +++ b/hig/src/nonIosMain/kotlin/zone/ien/hig/CupertinoTextField.nonIos.kt @@ -0,0 +1,5 @@ +package zone.ien.hig + +import androidx.compose.foundation.text.KeyboardOptions + +internal actual fun KeyboardOptions.enableNativeInput(): KeyboardOptions = this \ No newline at end of file diff --git a/hig/src/skikoMain/kotlin/zone/ien/hig/HazePlatform.skiko.kt b/hig/src/skikoMain/kotlin/zone/ien/hig/HazePlatform.skiko.kt index c848411c..08eb1015 100644 --- a/hig/src/skikoMain/kotlin/zone/ien/hig/HazePlatform.skiko.kt +++ b/hig/src/skikoMain/kotlin/zone/ien/hig/HazePlatform.skiko.kt @@ -22,6 +22,7 @@ package zone.ien.hig // Copyright 2023, Christopher Banes and the Haze project contributors // SPDX-License-Identifier: Apache-2.0 +import androidx.compose.ui.InternalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.BlurEffect @@ -118,6 +119,7 @@ internal actual class HazeNode actual constructor( } } + @OptIn(InternalComposeUiApi::class) private fun createBlurImageFilter(blurRadius: Dp): ImageFilter { val blurRadiusPx = with(density) { diff --git a/hig/src/skikoMain/kotlin/zone/ien/hig/Luminance.skiko.kt b/hig/src/skikoMain/kotlin/zone/ien/hig/Luminance.skiko.kt index 1b1e8280..5b78f51a 100644 --- a/hig/src/skikoMain/kotlin/zone/ien/hig/Luminance.skiko.kt +++ b/hig/src/skikoMain/kotlin/zone/ien/hig/Luminance.skiko.kt @@ -37,8 +37,8 @@ actual fun ImageBitmap.crop(x: Int, y: Int, width: Int, height: Int): ImageBitma Canvas(dstBitmap).use { canvas -> val srcRect = Rect.makeXYWH(x.toFloat(), y.toFloat(), cropWidth.toFloat(), cropHeight.toFloat()) - val dstRect = Rect.makeXYWH(0f, 0f, cropWidth.toFloat(), cropHeight.toFloat()) // ← 추가 - canvas.drawImageRect(Image.makeFromBitmap(srcBitmap), srcRect, dstRect) // ← dst 명시 + val dstRect = Rect.makeXYWH(0f, 0f, cropWidth.toFloat(), cropHeight.toFloat()) // ← added + canvas.drawImageRect(Image.makeFromBitmap(srcBitmap), srcRect, dstRect) // ← dst specified } return dstBitmap.asComposeImageBitmap()