├── sample
├── android-app
│ ├── src
│ │ └── main
│ │ │ ├── res
│ │ │ ├── values
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ │ ├── kotlin
│ │ │ └── dev
│ │ │ │ └── chrisbanes
│ │ │ │ └── material3
│ │ │ │ └── windowsizeclass
│ │ │ │ └── sample
│ │ │ │ └── MainActivity.kt
│ │ │ └── AndroidManifest.xml
│ └── build.gradle.kts
├── ios-app
│ ├── ios-app
│ │ ├── Assets.xcassets
│ │ │ ├── Contents.json
│ │ │ ├── AccentColor.colorset
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ └── Contents.json
│ │ ├── Preview Content
│ │ │ └── Preview Assets.xcassets
│ │ │ │ └── Contents.json
│ │ ├── ios_appApp.swift
│ │ └── ContentView.swift
│ └── ios-app.xcodeproj
│ │ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ │ └── project.pbxproj
├── web-js-app
│ ├── src
│ │ └── jsMain
│ │ │ ├── kotlin
│ │ │ └── dev
│ │ │ │ └── chrisbanes
│ │ │ │ └── material3
│ │ │ │ └── windowsizeclass
│ │ │ │ └── sample
│ │ │ │ ├── Main.kt
│ │ │ │ └── BrowserViewportWindow.kt
│ │ │ └── resources
│ │ │ └── index.html
│ └── build.gradle.kts
├── shared
│ ├── src
│ │ ├── androidMain
│ │ │ └── kotlin
│ │ │ │ └── dev
│ │ │ │ └── chrisbanes
│ │ │ │ └── material3
│ │ │ │ └── windowsizeclass
│ │ │ │ └── sample
│ │ │ │ └── Sample.android.kt
│ │ ├── jvmMain
│ │ │ └── kotlin
│ │ │ │ └── dev
│ │ │ │ └── chrisbanes
│ │ │ │ └── material3
│ │ │ │ └── windowsizeclass
│ │ │ │ └── sample
│ │ │ │ └── Sample.desktop.kt
│ │ ├── iosMain
│ │ │ └── kotlin
│ │ │ │ └── dev
│ │ │ │ └── chrisbanes
│ │ │ │ └── material3
│ │ │ │ └── windowsizeclass
│ │ │ │ └── sample
│ │ │ │ └── Sample.ios.kt
│ │ └── commonMain
│ │ │ └── kotlin
│ │ │ └── dev
│ │ │ └── chrisbanes
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ └── sample
│ │ │ └── Sample.kt
│ └── build.gradle.kts
├── web-wasm-app
│ ├── src
│ │ └── wasmJsMain
│ │ │ ├── kotlin
│ │ │ └── dev
│ │ │ │ └── chrisbanes
│ │ │ │ └── material3
│ │ │ │ └── windowsizeclass
│ │ │ │ └── sample
│ │ │ │ └── Main.kt
│ │ │ └── resources
│ │ │ └── index.html
│ └── build.gradle.kts
└── desktop-app
│ ├── src
│ └── main
│ │ └── kotlin
│ │ └── dev
│ │ └── chrisbanes
│ │ └── material3
│ │ └── windowsizeclass
│ │ └── sample
│ │ └── Main.kt
│ └── build.gradle.kts
├── renovate.json
├── spotless
└── cb-copyright.txt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── lib
├── gradle.properties
├── src
│ ├── iosMain
│ │ ├── def
│ │ │ └── objc.def
│ │ └── kotlin
│ │ │ └── androidx
│ │ │ └── compose
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ └── WindowSizeClass.ios.kt
│ ├── commonMain
│ │ └── kotlin
│ │ │ └── androidx
│ │ │ └── compose
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ ├── ExperimentalMaterial3WindowSizeClassApi.kt
│ │ │ └── WindowSizeClass.kt
│ ├── jsMain
│ │ └── kotlin
│ │ │ └── androidx
│ │ │ └── compose
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ └── WindowSizeClass.js.kt
│ ├── wasmJsMain
│ │ └── kotlin
│ │ │ └── androidx
│ │ │ └── compose
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ └── WindowSizeClass.js.kt
│ ├── jvmMain
│ │ └── kotlin
│ │ │ └── androidx
│ │ │ └── compose
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ └── WindowSizeClass.desktop.kt
│ ├── androidMain
│ │ └── kotlin
│ │ │ └── androidx
│ │ │ └── compose
│ │ │ └── material3
│ │ │ └── windowsizeclass
│ │ │ └── WindowSizeClass.android.kt
│ └── commonTest
│ │ └── kotlin
│ │ └── androidx
│ │ └── compose
│ │ └── material3
│ │ └── windowsizeclass
│ │ └── WindowSizeClassTest.kt
└── build.gradle.kts
├── .editorconfig
├── settings.gradle.kts
├── gradle.properties
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── gradlew.bat
├── README.md
├── gradlew
└── LICENSE
/sample/android-app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Android Sample
3 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/spotless/cb-copyright.txt:
--------------------------------------------------------------------------------
1 | // Copyright $YEAR, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisbanes/material3-windowsizeclass-multiplatform/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/sample/ios-app/ios-app/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/lib/gradle.properties:
--------------------------------------------------------------------------------
1 | POM_ARTIFACT_ID=material3-window-size-class-multiplatform
2 | POM_NAME=Compose Material 3 Window Size Class
3 | POM_DESCRIPTION=Provides window size classes for building responsive UIs
4 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisbanes/material3-windowsizeclass-multiplatform/HEAD/sample/android-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chrisbanes/material3-windowsizeclass-multiplatform/HEAD/sample/android-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/sample/ios-app/ios-app.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app/ios_appApp.swift:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | import SwiftUI
5 |
6 | @main
7 | struct ios_appApp: App {
8 | var body: some Scene {
9 | WindowGroup {
10 | ContentView()
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample/web-js-app/src/jsMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Main.kt:
--------------------------------------------------------------------------------
1 | package dev.chrisbanes.material3.windowsizeclass.sample
2 |
3 | import org.jetbrains.skiko.wasm.onWasmReady
4 |
5 | fun main() {
6 | onWasmReady {
7 | BrowserViewportWindow("Sample") {
8 | Sample()
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/shared/src/androidMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Sample.android.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dev.chrisbanes.material3.windowsizeclass.sample
5 |
6 | import androidx.compose.runtime.Composable
7 |
8 | @Composable
9 | fun MainView() = Sample()
10 |
--------------------------------------------------------------------------------
/sample/shared/src/jvmMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Sample.desktop.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dev.chrisbanes.material3.windowsizeclass.sample
5 |
6 | import androidx.compose.runtime.Composable
7 |
8 | @Composable
9 | fun MainView() = Sample()
10 |
--------------------------------------------------------------------------------
/lib/src/iosMain/def/objc.def:
--------------------------------------------------------------------------------
1 | language = Objective-C
2 | package = androidx.compose.material3.windowsizeclass.objc
3 | ---
4 |
5 | #import
6 |
7 | @protocol KeyValueObserver
8 | - (void)observeValueForKeyPath:(NSString *)keyPath
9 | ofObject:(id)object
10 | change:(NSDictionary *)change
11 | context:(void *)context;
12 | @end
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_size = 4
6 | indent_style = space
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 |
10 | [*.{kt,kts}]
11 | ij_kotlin_imports_layout = *
12 | ktlint_code_style = intellij_idea
13 | ktlint_standard_discouraged-comment-location = disabled
14 | ktlint_function_naming_ignore_when_annotated_with = Composable
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/sample/web-wasm-app/src/wasmJsMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Main.kt:
--------------------------------------------------------------------------------
1 | package dev.chrisbanes.material3.windowsizeclass.sample
2 |
3 | import androidx.compose.ui.ExperimentalComposeUiApi
4 | import androidx.compose.ui.window.CanvasBasedWindow
5 |
6 | @OptIn(ExperimentalComposeUiApi::class)
7 | fun main() {
8 | CanvasBasedWindow(canvasElementId = "Sample") {
9 | Sample()
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample/desktop-app/src/main/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Main.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dev.chrisbanes.material3.windowsizeclass.sample
5 |
6 | import androidx.compose.ui.window.Window
7 | import androidx.compose.ui.window.application
8 |
9 | fun main() = application {
10 | Window(onCloseRequest = ::exitApplication) {
11 | MainView()
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample/shared/src/iosMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Sample.ios.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dev.chrisbanes.material3.windowsizeclass.sample
5 |
6 | import androidx.compose.ui.window.ComposeUIViewController
7 | import platform.UIKit.UIViewController
8 |
9 | @Suppress("FunctionName")
10 | fun MainViewController(): UIViewController = ComposeUIViewController { Sample() }
11 |
--------------------------------------------------------------------------------
/sample/web-js-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | plugins {
6 | kotlin("multiplatform")
7 | id("org.jetbrains.compose")
8 | }
9 |
10 | kotlin {
11 | js(IR) {
12 | browser()
13 | binaries.executable()
14 | }
15 |
16 | sourceSets {
17 | val commonMain by getting {
18 | dependencies {
19 | implementation(project(":sample:shared"))
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | gradlePluginPortal()
5 | mavenCentral()
6 | }
7 | }
8 |
9 | dependencyResolutionManagement {
10 | repositories {
11 | google()
12 | mavenCentral()
13 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
14 | }
15 | }
16 |
17 | rootProject.name = "material3-windowsizeclass-multiplatform"
18 |
19 | include(":lib")
20 | include(":sample:shared")
21 | include(":sample:android-app")
22 | include(":sample:desktop-app")
23 | include(":sample:web-js-app")
24 | include(":sample:web-wasm-app")
25 |
--------------------------------------------------------------------------------
/sample/web-js-app/src/jsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Sample
7 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // ios-app
4 | //
5 | // Created by Chris Banes on 20/06/2023.
6 | //
7 |
8 | import UIKit
9 | import SwiftUI
10 | import shared
11 |
12 | struct ComposeView: UIViewControllerRepresentable {
13 | func makeUIViewController(context: Context) -> UIViewController {
14 | Sample_iosKt.MainViewController()
15 | }
16 |
17 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
18 | }
19 |
20 | struct ContentView: View {
21 | var body: some View {
22 | ComposeView()
23 | .ignoresSafeArea(.all, edges: .bottom)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/sample/web-wasm-app/src/wasmJsMain/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | material3-windowsizeclass-multiplatform sample
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/MainActivity.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dev.chrisbanes.material3.windowsizeclass.sample
5 |
6 | import android.os.Bundle
7 | import androidx.activity.ComponentActivity
8 | import androidx.activity.compose.setContent
9 | import androidx.compose.material3.MaterialTheme
10 |
11 | class MainActivity : ComponentActivity() {
12 | override fun onCreate(savedInstanceState: Bundle?) {
13 | super.onCreate(savedInstanceState)
14 | setContent {
15 | MaterialTheme {
16 | MainView()
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/sample/desktop-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat
6 |
7 | plugins {
8 | kotlin("jvm")
9 | id("org.jetbrains.compose")
10 | }
11 |
12 | dependencies {
13 | implementation(compose.desktop.currentOs)
14 | implementation(project(":sample:shared"))
15 | }
16 |
17 | compose.desktop {
18 | application {
19 | mainClass = "MainKt"
20 |
21 | nativeDistributions {
22 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
23 | packageName = "dev.chrisbanes.material3.windowsizeclass.sample"
24 | packageVersion = "1.0.0"
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/sample/web-wasm-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
6 |
7 | plugins {
8 | kotlin("multiplatform")
9 | id("org.jetbrains.compose")
10 | }
11 |
12 | kotlin {
13 | @OptIn(ExperimentalWasmDsl::class)
14 | wasmJs {
15 | browser {
16 | commonWebpackConfig {
17 | outputFileName = "sample.js"
18 | }
19 | }
20 |
21 | binaries.executable()
22 | }
23 |
24 | sourceSets {
25 | val commonMain by getting {
26 | dependencies {
27 | implementation(project(":sample:shared"))
28 | }
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/sample/android-app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | plugins {
6 | kotlin("android")
7 | id("com.android.application")
8 | id("org.jetbrains.compose")
9 | }
10 |
11 | android {
12 | namespace = "dev.chrisbanes.material3.windowsizeclass.sample"
13 | compileSdk = 34
14 |
15 | defaultConfig {
16 | applicationId = "dev.chrisbanes.material3.windowsizeclass.sample"
17 | minSdk = 21
18 | targetSdk = 34
19 |
20 | versionCode = 1
21 | versionName = "1.0"
22 | }
23 |
24 | compileOptions {
25 | sourceCompatibility = JavaVersion.VERSION_1_8
26 | targetCompatibility = JavaVersion.VERSION_1_8
27 | }
28 |
29 | kotlinOptions {
30 | jvmTarget = "1.8"
31 | }
32 | }
33 |
34 | dependencies {
35 | implementation(project(":sample:shared"))
36 | implementation("androidx.activity:activity-compose:1.9.0")
37 | }
38 |
--------------------------------------------------------------------------------
/lib/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/ExperimentalMaterial3WindowSizeClassApi.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.compose.material3.windowsizeclass
18 |
19 | @RequiresOptIn(
20 | "This material3-window-size-class API is experimental and is likely to change or to " +
21 | "be removed in the future.",
22 | )
23 | @Retention(AnnotationRetention.BINARY)
24 | annotation class ExperimentalMaterial3WindowSizeClassApi
25 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | },
8 | {
9 | "idiom" : "mac",
10 | "scale" : "1x",
11 | "size" : "16x16"
12 | },
13 | {
14 | "idiom" : "mac",
15 | "scale" : "2x",
16 | "size" : "16x16"
17 | },
18 | {
19 | "idiom" : "mac",
20 | "scale" : "1x",
21 | "size" : "32x32"
22 | },
23 | {
24 | "idiom" : "mac",
25 | "scale" : "2x",
26 | "size" : "32x32"
27 | },
28 | {
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "idiom" : "mac",
35 | "scale" : "2x",
36 | "size" : "128x128"
37 | },
38 | {
39 | "idiom" : "mac",
40 | "scale" : "1x",
41 | "size" : "256x256"
42 | },
43 | {
44 | "idiom" : "mac",
45 | "scale" : "2x",
46 | "size" : "256x256"
47 | },
48 | {
49 | "idiom" : "mac",
50 | "scale" : "1x",
51 | "size" : "512x512"
52 | },
53 | {
54 | "idiom" : "mac",
55 | "scale" : "2x",
56 | "size" : "512x512"
57 | }
58 | ],
59 | "info" : {
60 | "author" : "xcode",
61 | "version" : 1
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/sample/shared/src/commonMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/Sample.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package dev.chrisbanes.material3.windowsizeclass.sample
5 |
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.material3.Surface
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
12 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 |
17 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
18 | @Composable
19 | fun Sample() {
20 | Surface(Modifier.fillMaxSize()) {
21 | val windowSizeClass = calculateWindowSizeClass()
22 |
23 | Box(modifier = Modifier.fillMaxSize()) {
24 | Column(modifier = Modifier.align(Alignment.Center)) {
25 | Text(text = "width class")
26 | Text(text = windowSizeClass.widthSizeClass.toString())
27 | Text(text = "height class")
28 | Text(text = windowSizeClass.heightSizeClass.toString())
29 | }
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/sample/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
6 |
7 | // Copyright 2023, Christopher Banes and the project contributors
8 | // SPDX-License-Identifier: Apache-2.0
9 |
10 | plugins {
11 | kotlin("multiplatform")
12 | id("com.android.library")
13 | id("org.jetbrains.compose")
14 | }
15 |
16 | kotlin {
17 | applyDefaultHierarchyTemplate()
18 |
19 | jvm()
20 |
21 | androidTarget {
22 | compilations.all {
23 | kotlinOptions {
24 | jvmTarget = "1.8"
25 | }
26 | }
27 | }
28 |
29 | js(IR) {
30 | browser()
31 | }
32 |
33 | @OptIn(ExperimentalWasmDsl::class)
34 | wasmJs {
35 | browser()
36 | }
37 |
38 | listOf(
39 | iosX64(),
40 | iosArm64(),
41 | iosSimulatorArm64(),
42 | ).forEach {
43 | it.binaries.framework {
44 | baseName = "shared"
45 | isStatic = true
46 | }
47 | }
48 |
49 | sourceSets {
50 | val commonMain by getting {
51 | dependencies {
52 | api(project(":lib"))
53 | api(compose.material3)
54 | }
55 | }
56 | }
57 | }
58 |
59 | android {
60 | namespace = "dev.chrisbanes.material3.windowsizeclass.sample.shared"
61 | compileSdk = 34
62 | defaultConfig {
63 | minSdk = 21
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/lib/src/jsMain/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass.js.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package androidx.compose.material3.windowsizeclass
5 |
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.DisposableEffect
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.unit.DpSize
13 | import androidx.compose.ui.unit.dp
14 | import kotlinx.browser.window
15 | import org.w3c.dom.Window
16 | import org.w3c.dom.events.Event
17 |
18 | @ExperimentalMaterial3WindowSizeClassApi
19 | @Composable
20 | actual fun calculateWindowSizeClass(): WindowSizeClass {
21 | var windowSizeClass by remember {
22 | mutableStateOf(
23 | WindowSizeClass.calculateFromSize(window.getDpSize()),
24 | )
25 | }
26 |
27 | // Add a listener and listen for resize events
28 | DisposableEffect(Unit) {
29 | val callback: (Event) -> Unit = {
30 | windowSizeClass = WindowSizeClass.calculateFromSize(window.getDpSize())
31 | }
32 |
33 | window.addEventListener("resize", callback)
34 |
35 | onDispose {
36 | window.removeEventListener("resize", callback)
37 | }
38 | }
39 |
40 | return windowSizeClass
41 | }
42 |
43 | private fun Window.getDpSize(): DpSize = DpSize(innerWidth.dp, innerHeight.dp)
44 |
--------------------------------------------------------------------------------
/lib/src/wasmJsMain/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass.js.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package androidx.compose.material3.windowsizeclass
5 |
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.DisposableEffect
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.unit.DpSize
13 | import androidx.compose.ui.unit.dp
14 | import kotlinx.browser.window
15 | import org.w3c.dom.Window
16 | import org.w3c.dom.events.Event
17 |
18 | @ExperimentalMaterial3WindowSizeClassApi
19 | @Composable
20 | actual fun calculateWindowSizeClass(): WindowSizeClass {
21 | var windowSizeClass by remember {
22 | mutableStateOf(
23 | WindowSizeClass.calculateFromSize(window.getDpSize()),
24 | )
25 | }
26 |
27 | // Add a listener and listen for resize events
28 | DisposableEffect(Unit) {
29 | val callback: (Event) -> Unit = {
30 | windowSizeClass = WindowSizeClass.calculateFromSize(window.getDpSize())
31 | }
32 |
33 | window.addEventListener("resize", callback)
34 |
35 | onDispose {
36 | window.removeEventListener("resize", callback)
37 | }
38 | }
39 |
40 | return windowSizeClass
41 | }
42 |
43 | private fun Window.getDpSize(): DpSize = DpSize(innerWidth.dp, innerHeight.dp)
44 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Copyright 2023, Christopher Banes and the Tivi project contributors
2 | # SPDX-License-Identifier: Apache-2.0
3 |
4 | # Gradle
5 | org.gradle.caching=true
6 | org.gradle.parallel=true
7 | org.gradle.jvmargs=-Xmx4096m -Dfile.encoding=UTF-8
8 | # https://docs.gradle.org/7.6/userguide/configuration_cache.html
9 | org.gradle.configuration-cache=true
10 |
11 | # Kotlin
12 | kotlin.code.style=official
13 |
14 | # Android
15 | android.useAndroidX=true
16 | android.defaults.buildfeatures.resvalues=false
17 | android.defaults.buildfeatures.shaders=false
18 |
19 | # MPP
20 | kotlin.mpp.enableCInteropCommonization=true
21 | kotlin.mpp.androidSourceSetLayoutVersion=2
22 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
23 |
24 | # Compose MP
25 | org.jetbrains.compose.experimental.uikit.enabled=true
26 | org.jetbrains.compose.experimental.jscanvas.enabled=true
27 | org.jetbrains.compose.experimental.wasm.enabled=true
28 |
29 | # Maven Central
30 | SONATYPE_HOST=DEFAULT
31 | SONATYPE_AUTOMATIC_RELEASE=true
32 | RELEASE_SIGNING_ENABLED=true
33 |
34 | GROUP=dev.chrisbanes.material3
35 | VERSION_NAME=0.5.0-SNAPSHOT
36 |
37 | POM_URL=https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/
38 | POM_SCM_URL=https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/
39 | POM_SCM_CONNECTION=scm:git:git://github.com/chrisbanes/material3-windowsizeclass-multiplatform.git
40 | POM_SCM_DEV_CONNECTION=scm:git:git://github.com/chrisbanes/material3-windowsizeclass-multiplatform.git
41 |
42 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
43 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
44 | POM_LICENCE_DIST=repo
45 |
46 | POM_DEVELOPER_ID=chrisbanes
47 | POM_DEVELOPER_NAME=Chris Banes
48 | POM_INCEPTION_YEAR=2023
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build & test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | build:
11 | runs-on: macos-14
12 | timeout-minutes: 45
13 |
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Validate Gradle Wrapper
18 | uses: gradle/wrapper-validation-action@v3
19 |
20 | - name: Setup JDK
21 | uses: actions/setup-java@v4
22 | with:
23 | distribution: 'zulu'
24 | java-version: 21
25 |
26 | - uses: gradle/gradle-build-action@v3
27 | with:
28 | gradle-home-cache-cleanup: true
29 |
30 | - name: Build
31 | run: ./gradlew build --no-configuration-cache
32 |
33 | - name: Deploy to Sonatype
34 | if: github.event_name == 'push' # only deploy for pushed commits (not PRs)
35 | run: ./gradlew publish --no-configuration-cache
36 | env:
37 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
38 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
39 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.GPG_KEY }}
40 | ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.GPG_KEY_ID }}
41 | ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.GPG_KEY_PASSWORD }}
42 |
43 | - name: Upload reports
44 | if: always()
45 | uses: actions/upload-artifact@v4
46 | with:
47 | name: android-reports
48 | path: |
49 | **/build/reports/*
50 |
51 | - name: Upload test results
52 | if: always()
53 | uses: actions/upload-artifact@v4
54 | with:
55 | name: android-test-results
56 | path: |
57 | **/build/test-results/*
58 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/lib/src/jvmMain/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass.desktop.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | @file:Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
5 |
6 | package androidx.compose.material3.windowsizeclass
7 |
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.DisposableEffect
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.unit.DpSize
15 | import androidx.compose.ui.unit.dp
16 | import androidx.compose.ui.window.LocalWindow
17 | import java.awt.Component
18 | import java.awt.Window
19 | import java.awt.event.ComponentAdapter
20 | import java.awt.event.ComponentEvent
21 |
22 | @ExperimentalMaterial3WindowSizeClassApi
23 | @Composable
24 | actual fun calculateWindowSizeClass(): WindowSizeClass {
25 | val window: Window? = LocalWindow.current
26 |
27 | var windowSizeClass by remember(window) {
28 | mutableStateOf(WindowSizeClass.calculateFromSize(window?.getDpSize() ?: DpSize.Zero))
29 | }
30 |
31 | // Add a listener and listen for componentResized events
32 | DisposableEffect(window) {
33 | val listener = object : ComponentAdapter() {
34 | override fun componentResized(event: ComponentEvent) {
35 | windowSizeClass = WindowSizeClass.calculateFromSize(window!!.getDpSize())
36 | }
37 | }
38 |
39 | window?.addComponentListener(listener)
40 |
41 | onDispose {
42 | window?.removeComponentListener(listener)
43 | }
44 | }
45 |
46 | return windowSizeClass
47 | }
48 |
49 | private fun Component.getDpSize(): DpSize = DpSize(width.dp, height.dp)
50 |
--------------------------------------------------------------------------------
/lib/src/androidMain/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass.android.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.compose.material3.windowsizeclass
18 |
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.graphics.toComposeRect
21 | import androidx.compose.ui.platform.LocalConfiguration
22 | import androidx.compose.ui.platform.LocalContext
23 | import androidx.compose.ui.platform.LocalDensity
24 | import androidx.window.layout.WindowMetricsCalculator
25 |
26 | @ExperimentalMaterial3WindowSizeClassApi
27 | @Composable
28 | actual fun calculateWindowSizeClass(): WindowSizeClass {
29 | // Observe view configuration changes and recalculate the size class on each change. We can't
30 | // use Activity#onConfigurationChanged as this will sometimes fail to be called on different
31 | // API levels, hence why this function needs to be @Composable so we can observe the
32 | // ComposeView's configuration changes.
33 | LocalConfiguration.current
34 | val density = LocalDensity.current
35 | val metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(LocalContext.current)
36 | val size = with(density) { metrics.bounds.toComposeRect().size.toDpSize() }
37 | return WindowSizeClass.calculateFromSize(size)
38 | }
39 |
--------------------------------------------------------------------------------
/sample/web-js-app/src/jsMain/kotlin/dev/chrisbanes/material3/windowsizeclass/sample/BrowserViewportWindow.kt:
--------------------------------------------------------------------------------
1 | package dev.chrisbanes.material3.windowsizeclass.sample
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.ExperimentalComposeUiApi
5 | import androidx.compose.ui.window.CanvasBasedWindow
6 | import kotlinx.browser.document
7 | import kotlinx.browser.window
8 | import org.w3c.dom.HTMLCanvasElement
9 | import org.w3c.dom.HTMLStyleElement
10 |
11 | private const val CANVAS_ELEMENT_ID = "ComposeTarget" // Hardwired into ComposeWindow
12 |
13 | /**
14 | * A Skiko/Canvas-based top-level window using the browser's entire viewport. Supports resizing.
15 | */
16 | @OptIn(ExperimentalComposeUiApi::class)
17 | @Suppress("FunctionName")
18 | fun BrowserViewportWindow(
19 | title: String,
20 | content: @Composable () -> Unit,
21 | ) {
22 | val htmlHeadElement = document.head!!
23 | htmlHeadElement.appendChild(
24 | (document.createElement("style") as HTMLStyleElement).apply {
25 | type = "text/css"
26 | appendChild(
27 | document.createTextNode(
28 | """
29 | html, body {
30 | overflow: hidden;
31 | margin: 0 !important;
32 | padding: 0 !important;
33 | }
34 |
35 | #$CANVAS_ELEMENT_ID {
36 | outline: none;
37 | }
38 | """.trimIndent(),
39 | ),
40 | )
41 | },
42 | )
43 |
44 | fun HTMLCanvasElement.fillViewportSize() {
45 | setAttribute("width", "${window.innerWidth}")
46 | setAttribute("height", "${window.innerHeight}")
47 | }
48 |
49 | (document.getElementById(CANVAS_ELEMENT_ID) as HTMLCanvasElement).apply {
50 | fillViewportSize()
51 | }
52 |
53 | // WORKAROUND: ComposeWindow does not implement `setTitle(title)`
54 | val titleElement = htmlHeadElement.getElementsByTagName("title").item(0)
55 | ?: document.createElement("title").also { htmlHeadElement.appendChild(it) }
56 | titleElement.textContent = title
57 |
58 | CanvasBasedWindow(title = title) {
59 | content()
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/lib/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 |
5 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
6 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
7 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl
8 |
9 | plugins {
10 | kotlin("multiplatform")
11 | id("com.android.library")
12 | id("org.jetbrains.compose")
13 | id("com.vanniktech.maven.publish")
14 | }
15 |
16 | kotlin {
17 | applyDefaultHierarchyTemplate()
18 |
19 | jvm {
20 | compilations.all {
21 | compilerOptions.configure {
22 | jvmTarget.set(JvmTarget.JVM_1_8)
23 | }
24 | }
25 | }
26 | androidTarget {
27 | publishLibraryVariants("release")
28 |
29 | compilations.all {
30 | kotlinOptions {
31 | jvmTarget = "1.8"
32 | }
33 | }
34 | }
35 |
36 | iosX64()
37 | iosArm64()
38 | iosSimulatorArm64()
39 |
40 | js(IR) {
41 | browser()
42 | }
43 |
44 | @OptIn(ExperimentalWasmDsl::class)
45 | wasmJs {
46 | browser()
47 | }
48 |
49 | configure(targets) {
50 | if (this is KotlinNativeTarget && konanTarget.family.isAppleFamily) {
51 | compilations.getByName("main") {
52 | val objc by cinterops.creating {
53 | defFile(project.file("src/iosMain/def/objc.def"))
54 | }
55 | }
56 | }
57 | }
58 |
59 | sourceSets {
60 | val commonMain by getting {
61 | dependencies {
62 | api(compose.ui)
63 | }
64 | }
65 | val androidMain by getting {
66 | dependencies {
67 | implementation("androidx.window:window:1.3.0")
68 | }
69 | }
70 |
71 | val commonTest by getting {
72 | dependencies {
73 | implementation(kotlin("test"))
74 | implementation("com.willowtreeapps.assertk:assertk:0.28.1")
75 | }
76 | }
77 | }
78 | }
79 |
80 | android {
81 | namespace = "androidx.compose.material3.windowsizeclass"
82 | compileSdk = 34
83 | defaultConfig {
84 | minSdk = 21
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lib/src/iosMain/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass.ios.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2023, Christopher Banes and the project contributors
2 | // SPDX-License-Identifier: Apache-2.0
3 |
4 | package androidx.compose.material3.windowsizeclass
5 |
6 | import androidx.compose.material3.windowsizeclass.objc.KeyValueObserverProtocol
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.DisposableEffect
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.interop.LocalUIViewController
14 | import androidx.compose.ui.unit.DpSize
15 | import androidx.compose.ui.unit.dp
16 | import kotlinx.cinterop.BetaInteropApi
17 | import kotlinx.cinterop.COpaquePointer
18 | import kotlinx.cinterop.ExperimentalForeignApi
19 | import kotlinx.cinterop.ExportObjCClass
20 | import kotlinx.cinterop.useContents
21 | import platform.Foundation.NSKeyValueObservingOptionNew
22 | import platform.Foundation.addObserver
23 | import platform.Foundation.removeObserver
24 | import platform.UIKit.UIViewController
25 | import platform.darwin.NSObject
26 |
27 | @OptIn(ExperimentalForeignApi::class)
28 | @ExperimentalMaterial3WindowSizeClassApi
29 | @Composable
30 | actual fun calculateWindowSizeClass(): WindowSizeClass {
31 | val uiViewController = LocalUIViewController.current
32 |
33 | var windowSizeClass by remember(uiViewController) {
34 | mutableStateOf(WindowSizeClass.calculateFromSize(uiViewController.getViewFrameSize()))
35 | }
36 |
37 | DisposableEffect(uiViewController) {
38 | val observer = ObserverObject {
39 | windowSizeClass = WindowSizeClass.calculateFromSize(uiViewController.getViewFrameSize())
40 | }
41 |
42 | uiViewController.view.layer.addObserver(
43 | observer = observer,
44 | forKeyPath = "bounds",
45 | options = NSKeyValueObservingOptionNew,
46 | context = null,
47 | )
48 |
49 | onDispose {
50 | uiViewController.view.layer.removeObserver(
51 | observer = observer,
52 | forKeyPath = "bounds",
53 | )
54 | }
55 | }
56 |
57 | return windowSizeClass
58 | }
59 |
60 | @OptIn(ExperimentalForeignApi::class)
61 | private fun UIViewController.getViewFrameSize(): DpSize = view.frame().useContents {
62 | // iOS returns density independent pixels, rather than raw pixels
63 | DpSize(size.width.dp, size.height.dp)
64 | }
65 |
66 | @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
67 | @ExportObjCClass
68 | private class ObserverObject(
69 | private val onChange: () -> Unit,
70 | ) : NSObject(), KeyValueObserverProtocol {
71 | override fun observeValueForKeyPath(
72 | keyPath: String?,
73 | ofObject: Any?,
74 | change: Map?,
75 | context: COpaquePointer?,
76 | ) {
77 | onChange()
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | .idea
4 | .DS_Store
5 | build
6 | bin
7 | captures
8 | .externalNativeBuild
9 | .cxx
10 | local.properties
11 |
12 | kotlin-js-store/
13 |
14 | #######################################################################################
15 | # From https://github.com/github/gitignore/blob/main/Swift.gitignore
16 | #######################################################################################
17 |
18 | # Xcode
19 | #
20 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
21 |
22 | ## User settings
23 | xcuserdata/
24 |
25 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
26 | *.xcscmblueprint
27 | *.xccheckout
28 |
29 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
30 | build/
31 | DerivedData/
32 | *.moved-aside
33 | *.pbxuser
34 | !default.pbxuser
35 | *.mode1v3
36 | !default.mode1v3
37 | *.mode2v3
38 | !default.mode2v3
39 | *.perspectivev3
40 | !default.perspectivev3
41 |
42 | ## Obj-C/Swift specific
43 | *.hmap
44 |
45 | ## App packaging
46 | *.ipa
47 | *.dSYM.zip
48 | *.dSYM
49 |
50 | ## Playgrounds
51 | timeline.xctimeline
52 | playground.xcworkspace
53 |
54 | # Swift Package Manager
55 | #
56 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
57 | # Packages/
58 | # Package.pins
59 | # Package.resolved
60 | # *.xcodeproj
61 | #
62 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
63 | # hence it is not needed unless you have added a package configuration file to your project
64 | # .swiftpm
65 |
66 | .build/
67 |
68 | # CocoaPods
69 | #
70 | # We recommend against adding the Pods directory to your .gitignore. However
71 | # you should judge for yourself, the pros and cons are mentioned at:
72 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
73 | #
74 | # Pods/
75 | #
76 | # Add this line if you want to avoid checking in source code from the Xcode workspace
77 | # *.xcworkspace
78 |
79 | # Carthage
80 | #
81 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
82 | # Carthage/Checkouts
83 |
84 | Carthage/Build/
85 |
86 | # Accio dependency management
87 | Dependencies/
88 | .accio/
89 |
90 | # fastlane
91 | #
92 | # It is recommended to not store the screenshots in the git repo.
93 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
94 | # For more information about the recommended setup visit:
95 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
96 |
97 | fastlane/report.xml
98 | fastlane/Preview.html
99 | fastlane/screenshots/**/*.png
100 | fastlane/test_output
101 |
102 | # Code Injection
103 | #
104 | # After new code Injection tools there's a generated folder /iOSInjectionProject
105 | # https://github.com/johnno1962/injectionforxcode
106 |
107 | iOSInjectionProject/
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | @rem This is normally unused
30 | set APP_BASE_NAME=%~n0
31 | set APP_HOME=%DIRNAME%
32 |
33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
35 |
36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
38 |
39 | @rem Find java.exe
40 | if defined JAVA_HOME goto findJavaFromJavaHome
41 |
42 | set JAVA_EXE=java.exe
43 | %JAVA_EXE% -version >NUL 2>&1
44 | if %ERRORLEVEL% equ 0 goto execute
45 |
46 | echo. 1>&2
47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
48 | echo. 1>&2
49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
50 | echo location of your Java installation. 1>&2
51 |
52 | goto fail
53 |
54 | :findJavaFromJavaHome
55 | set JAVA_HOME=%JAVA_HOME:"=%
56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
57 |
58 | if exist "%JAVA_EXE%" goto execute
59 |
60 | echo. 1>&2
61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
62 | echo. 1>&2
63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
64 | echo location of your Java installation. 1>&2
65 |
66 | goto fail
67 |
68 | :execute
69 | @rem Setup the command line
70 |
71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
72 |
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if %ERRORLEVEL% equ 0 goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | set EXIT_CODE=%ERRORLEVEL%
85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
87 | exit /b %EXIT_CODE%
88 |
89 | :mainEnd
90 | if "%OS%"=="Windows_NT" endlocal
91 |
92 | :omega
93 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Deprecated
2 |
3 | This library is now deprecated, as JetBrains have released their own version as part of [Compose Multiplatform 1.7.0](https://www.jetbrains.com/help/kotlin-multiplatform-dev/whats-new-compose-170.html#material3-material3-window-size-class). See the [tracking issue](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/issues/100) for more info.
4 |
5 | ---
6 |
7 |
8 | [](https://search.maven.org/search?q=g:dev.chrisbanes.material3) 
9 |
10 | ## Compose Multiplatform: Material 3 Window Size Class
11 |
12 | The [Material 3 Window size classes](https://m3.material.io/foundations/layout/applying-layout/window-size-classes) are a set of opinionated breakpoints, the window size at which a layout needs to change to match available space, device conventions, and ergonomics. All devices fall into one of three Material Design window size classes: compact, medium, or expanded. Rather than designing for an ever increasing number of display states, focusing on window class sizes ensures layouts work across a range of devices.
13 |
14 | The `androidx.compose.material3:material3-window-size-class` library is available for Jetpack Compose, but it is not currently available for Compose Multiplatform. This library changes that, by providing the [WindowSizeClass](https://developer.android.com/reference/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass) for many of the platforms supported by Compose Multiplatform.
15 |
16 | | Platform | Supported | Sample |
17 | |---------------|------------------|-------------------------------------------------------------------------------------------------------------------|
18 | | Android | ✅ | [android-app](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/tree/main/sample/android-app) |
19 | | iOS | ✅ | [ios-app](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/tree/main/sample/ios-app) |
20 | | Desktop (JVM) | ✅ | [desktop-app](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/tree/main/sample/desktop-app) |
21 | | Web | ✅ (experimental) | [web-js-app](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/tree/main/sample/web-js-app) [web-wasm-app](https://github.com/chrisbanes/material3-windowsizeclass-multiplatform/tree/main/sample/web-wasm-app) |
22 |
23 | ## Usage
24 |
25 | Usage is very simple:
26 |
27 | ```kotlin
28 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
29 |
30 | @Composable
31 | fun MyApplication() {
32 | val windowSizeClass = calculateWindowSizeClass()
33 |
34 | // Example of how to change the font size based on the screen width
35 | val fontSize = when (windowSizeClass.widthSizeClass) {
36 | WindowWidthSizeClass.Compact -> 16.sp
37 | WindowWidthSizeClass.Medium -> 24.sp
38 | else -> 30.sp
39 | }
40 |
41 | Column( /* ... */) {
42 | Box (/* ... */){
43 | Image (/* ... */)
44 | Text (fontSize = fontSize)
45 | }
46 | }
47 | }
48 | ```
49 |
50 | You'll note that I have kept the package name the same as that in AndroidX. This is to enable easy migration for when support is added to Compose Multiplatform ([tracking issue](https://github.com/JetBrains/compose-multiplatform/issues/2404)).
51 |
52 | ## Download
53 |
54 | [](https://central.sonatype.com/namespace/dev.chrisbanes.material3)
55 |
56 | ```kotlin
57 | val commonMain by getting {
58 | dependencies {
59 | implementation("dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0")
60 | }
61 | }
62 | ```
63 |
64 | ## License
65 |
66 | ```
67 | Copyright 2022 The Android Open Source Project
68 | Portions 2023 Christopher Banes
69 |
70 | Licensed under the Apache License, Version 2.0 (the "License");
71 | you may not use this file except in compliance with the License.
72 | You may obtain a copy of the License at
73 |
74 | https://www.apache.org/licenses/LICENSE-2.0
75 |
76 | Unless required by applicable law or agreed to in writing, software
77 | distributed under the License is distributed on an "AS IS" BASIS,
78 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
79 | See the License for the specific language governing permissions and
80 | limitations under the License.
81 | ```
82 |
--------------------------------------------------------------------------------
/sample/android-app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | # This is normally unused
84 | # shellcheck disable=SC2034
85 | APP_BASE_NAME=${0##*/}
86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
88 |
89 | # Use the maximum available, or set MAX_FD != -1 to use that value.
90 | MAX_FD=maximum
91 |
92 | warn () {
93 | echo "$*"
94 | } >&2
95 |
96 | die () {
97 | echo
98 | echo "$*"
99 | echo
100 | exit 1
101 | } >&2
102 |
103 | # OS specific support (must be 'true' or 'false').
104 | cygwin=false
105 | msys=false
106 | darwin=false
107 | nonstop=false
108 | case "$( uname )" in #(
109 | CYGWIN* ) cygwin=true ;; #(
110 | Darwin* ) darwin=true ;; #(
111 | MSYS* | MINGW* ) msys=true ;; #(
112 | NONSTOP* ) nonstop=true ;;
113 | esac
114 |
115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
116 |
117 |
118 | # Determine the Java command to use to start the JVM.
119 | if [ -n "$JAVA_HOME" ] ; then
120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
121 | # IBM's JDK on AIX uses strange locations for the executables
122 | JAVACMD=$JAVA_HOME/jre/sh/java
123 | else
124 | JAVACMD=$JAVA_HOME/bin/java
125 | fi
126 | if [ ! -x "$JAVACMD" ] ; then
127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
128 |
129 | Please set the JAVA_HOME variable in your environment to match the
130 | location of your Java installation."
131 | fi
132 | else
133 | JAVACMD=java
134 | if ! command -v java >/dev/null 2>&1
135 | then
136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 | fi
142 |
143 | # Increase the maximum file descriptors if we can.
144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
145 | case $MAX_FD in #(
146 | max*)
147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
148 | # shellcheck disable=SC2039,SC3045
149 | MAX_FD=$( ulimit -H -n ) ||
150 | warn "Could not query maximum file descriptor limit"
151 | esac
152 | case $MAX_FD in #(
153 | '' | soft) :;; #(
154 | *)
155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
156 | # shellcheck disable=SC2039,SC3045
157 | ulimit -n "$MAX_FD" ||
158 | warn "Could not set maximum file descriptor limit to $MAX_FD"
159 | esac
160 | fi
161 |
162 | # Collect all arguments for the java command, stacking in reverse order:
163 | # * args from the command line
164 | # * the main class name
165 | # * -classpath
166 | # * -D...appname settings
167 | # * --module-path (only if needed)
168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
169 |
170 | # For Cygwin or MSYS, switch paths to Windows format before running java
171 | if "$cygwin" || "$msys" ; then
172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
174 |
175 | JAVACMD=$( cygpath --unix "$JAVACMD" )
176 |
177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
178 | for arg do
179 | if
180 | case $arg in #(
181 | -*) false ;; # don't mess with options #(
182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
183 | [ -e "$t" ] ;; #(
184 | *) false ;;
185 | esac
186 | then
187 | arg=$( cygpath --path --ignore --mixed "$arg" )
188 | fi
189 | # Roll the args list around exactly as many times as the number of
190 | # args, so each arg winds up back in the position where it started, but
191 | # possibly modified.
192 | #
193 | # NB: a `for` loop captures its iteration list before it begins, so
194 | # changing the positional parameters here affects neither the number of
195 | # iterations, nor the values presented in `arg`.
196 | shift # remove old arg
197 | set -- "$@" "$arg" # push replacement arg
198 | done
199 | fi
200 |
201 |
202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
204 |
205 | # Collect all arguments for the java command:
206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
207 | # and any embedded shellness will be escaped.
208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
209 | # treated as '${Hostname}' itself on the command line.
210 |
211 | set -- \
212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
213 | -classpath "$CLASSPATH" \
214 | org.gradle.wrapper.GradleWrapperMain \
215 | "$@"
216 |
217 | # Stop when "xargs" is not available.
218 | if ! command -v xargs >/dev/null 2>&1
219 | then
220 | die "xargs is not available"
221 | fi
222 |
223 | # Use "xargs" to parse quoted args.
224 | #
225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
226 | #
227 | # In Bash we could simply go:
228 | #
229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
230 | # set -- "${ARGS[@]}" "$@"
231 | #
232 | # but POSIX shell has neither arrays nor command substitution, so instead we
233 | # post-process each arg (as a line of input to sed) to backslash-escape any
234 | # character that might be a shell metacharacter, then use eval to reverse
235 | # that process (while maintaining the separation between arguments), and wrap
236 | # the whole thing up as a single "set" statement.
237 | #
238 | # This will of course break if any of these variables contains a newline or
239 | # an unmatched quote.
240 | #
241 |
242 | eval "set -- $(
243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
244 | xargs -n1 |
245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
246 | tr '\n' ' '
247 | )" '"$@"'
248 |
249 | exec "$JAVACMD" "$@"
250 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/lib/src/commonMain/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClass.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.compose.material3.windowsizeclass
18 |
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.Immutable
21 | import androidx.compose.ui.geometry.Size
22 | import androidx.compose.ui.unit.Density
23 | import androidx.compose.ui.unit.Dp
24 | import androidx.compose.ui.unit.DpSize
25 | import androidx.compose.ui.unit.dp
26 |
27 | /**
28 | * Calculates the window's [WindowSizeClass].
29 | *
30 | * A new [WindowSizeClass] will be returned whenever a change causes the width or
31 | * height of the window to cross a breakpoint, such as when the device is rotated or the window
32 | * is resized.
33 | */
34 | @ExperimentalMaterial3WindowSizeClassApi
35 | @Composable
36 | expect fun calculateWindowSizeClass(): WindowSizeClass
37 |
38 | /**
39 | * Window size classes are a set of opinionated viewport breakpoints to design, develop, and test
40 | * responsive application layouts against.
41 | * For more details check Support different screen sizes documentation.
42 | *
43 | * WindowSizeClass contains a [WindowWidthSizeClass] and [WindowHeightSizeClass], representing the
44 | * window size classes for this window's width and height respectively.
45 | *
46 | * See [calculateWindowSizeClass] to calculate the WindowSizeClass.
47 | *
48 | * @property widthSizeClass width-based window size class ([WindowWidthSizeClass])
49 | * @property heightSizeClass height-based window size class ([WindowHeightSizeClass])
50 | */
51 | @Immutable
52 | class WindowSizeClass private constructor(
53 | val widthSizeClass: WindowWidthSizeClass,
54 | val heightSizeClass: WindowHeightSizeClass,
55 | ) {
56 | companion object {
57 | @ExperimentalMaterial3WindowSizeClassApi
58 | internal fun calculateFromSize(size: DpSize): WindowSizeClass {
59 | val windowWidthSizeClass = WindowWidthSizeClass.fromWidth(size.width)
60 | val windowHeightSizeClass = WindowHeightSizeClass.fromHeight(size.height)
61 | return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass)
62 | }
63 |
64 | /**
65 | * Calculates the best matched [WindowSizeClass] for a given [size] and [Density] according
66 | * to the provided [supportedWidthSizeClasses] and [supportedHeightSizeClasses].
67 | *
68 | * @param size of the window
69 | * @param density of the window
70 | * @param supportedWidthSizeClasses the set of width size classes that are supported
71 | * @param supportedHeightSizeClasses the set of height size classes that are supported
72 | * @return [WindowSizeClass] corresponding to the given width and height
73 | */
74 | @ExperimentalMaterial3WindowSizeClassApi
75 | fun calculateFromSize(
76 | size: Size,
77 | density: Density,
78 | supportedWidthSizeClasses: Set =
79 | WindowWidthSizeClass.DefaultSizeClasses,
80 | supportedHeightSizeClasses: Set =
81 | WindowHeightSizeClass.DefaultSizeClasses,
82 | ): WindowSizeClass {
83 | val windowWidthSizeClass =
84 | WindowWidthSizeClass.fromWidth(size.width, density, supportedWidthSizeClasses)
85 | val windowHeightSizeClass =
86 | WindowHeightSizeClass.fromHeight(size.height, density, supportedHeightSizeClasses)
87 | return WindowSizeClass(windowWidthSizeClass, windowHeightSizeClass)
88 | }
89 | }
90 |
91 | override fun equals(other: Any?): Boolean {
92 | if (this === other) return true
93 | if (other == null || this::class != other::class) return false
94 |
95 | other as WindowSizeClass
96 |
97 | if (widthSizeClass != other.widthSizeClass) return false
98 | if (heightSizeClass != other.heightSizeClass) return false
99 |
100 | return true
101 | }
102 |
103 | override fun hashCode(): Int {
104 | var result = widthSizeClass.hashCode()
105 | result = 31 * result + heightSizeClass.hashCode()
106 | return result
107 | }
108 |
109 | override fun toString() = "WindowSizeClass($widthSizeClass, $heightSizeClass)"
110 | }
111 |
112 | /**
113 | * Width-based window size class.
114 | *
115 | * A window size class represents a breakpoint that can be used to build responsive layouts. Each
116 | * window size class breakpoint represents a majority case for typical device scenarios so your
117 | * layouts will work well on most devices and configurations.
118 | *
119 | * For more details see Window size classes documentation.
120 | */
121 | @Immutable
122 | @kotlin.jvm.JvmInline
123 | value class WindowWidthSizeClass private constructor(private val value: Int) :
124 | Comparable {
125 |
126 | override operator fun compareTo(other: WindowWidthSizeClass) =
127 | breakpoint().compareTo(other.breakpoint())
128 |
129 | override fun toString(): String {
130 | return "WindowWidthSizeClass." + when (this) {
131 | Compact -> "Compact"
132 | Medium -> "Medium"
133 | Expanded -> "Expanded"
134 | else -> ""
135 | }
136 | }
137 |
138 | companion object {
139 | /** Represents the majority of phones in portrait. */
140 | val Compact = WindowWidthSizeClass(0)
141 |
142 | /**
143 | * Represents the majority of tablets in portrait and large unfolded inner displays in
144 | * portrait.
145 | */
146 | val Medium = WindowWidthSizeClass(1)
147 |
148 | /**
149 | * Represents the majority of tablets in landscape and large unfolded inner displays in
150 | * landscape.
151 | */
152 | val Expanded = WindowWidthSizeClass(2)
153 |
154 | /**
155 | * The default set of size classes that includes [Compact], [Medium], and [Expanded] size
156 | * classes. Should never expand to ensure behavioral consistency.
157 | */
158 | val DefaultSizeClasses = setOf(Compact, Medium, Expanded)
159 |
160 | /**
161 | * The standard set of size classes. It's supposed to include all size classes and will be
162 | * expanded whenever a new size class is defined. By default
163 | * [WindowSizeClass.calculateFromSize] will only return size classes in [DefaultSizeClasses]
164 | * in order to avoid behavioral changes when new size classes are added. You can opt in to
165 | * support all available size classes by doing:
166 | * ```
167 | * WindowSizeClass.calculateFromSize(
168 | * size = size,
169 | * density = density,
170 | * supportedWidthSizeClasses = WindowWidthSizeClass.StandardSizeClasses,
171 | * supportedHeightSizeClasses = WindowHeightSizeClass.StandardSizeClasses
172 | * )
173 | * ```
174 | */
175 | val StandardSizeClasses get() = DefaultSizeClasses
176 |
177 | private fun WindowWidthSizeClass.breakpoint(): Dp {
178 | return when {
179 | this == Expanded -> 840.dp
180 | this == Medium -> 600.dp
181 | else -> 0.dp
182 | }
183 | }
184 |
185 | /** Calculates the [WindowWidthSizeClass] for a given [width] */
186 | internal fun fromWidth(width: Dp): WindowWidthSizeClass {
187 | return fromWidth(
188 | with(defaultDensity) { width.toPx() },
189 | defaultDensity,
190 | DefaultSizeClasses,
191 | )
192 | }
193 |
194 | /**
195 | * Calculates the best matched [WindowWidthSizeClass] for a given [width] in Pixels and
196 | * a given [Density] from [supportedSizeClasses].
197 | */
198 | internal fun fromWidth(
199 | width: Float,
200 | density: Density,
201 | supportedSizeClasses: Set,
202 | ): WindowWidthSizeClass {
203 | require(width >= 0) { "Width must not be negative" }
204 | require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" }
205 | val sortedSizeClasses = supportedSizeClasses.sortedDescending()
206 | // Find the largest supported size class that matches the width
207 | sortedSizeClasses.forEach {
208 | if (width >= with(density) { it.breakpoint().toPx() }) {
209 | return it
210 | }
211 | }
212 | // If none of the size classes matches, return the smallest one.
213 | return sortedSizeClasses.last()
214 | }
215 | }
216 | }
217 |
218 | /**
219 | * Height-based window size class.
220 | *
221 | * A window size class represents a breakpoint that can be used to build responsive layouts. Each
222 | * window size class breakpoint represents a majority case for typical device scenarios so your
223 | * layouts will work well on most devices and configurations.
224 | *
225 | * For more details see Window size classes documentation.
226 | */
227 | @Immutable
228 | @kotlin.jvm.JvmInline
229 | value class WindowHeightSizeClass private constructor(private val value: Int) :
230 | Comparable {
231 |
232 | override operator fun compareTo(other: WindowHeightSizeClass) =
233 | breakpoint().compareTo(other.breakpoint())
234 |
235 | override fun toString(): String {
236 | return "WindowHeightSizeClass." + when (this) {
237 | Compact -> "Compact"
238 | Medium -> "Medium"
239 | Expanded -> "Expanded"
240 | else -> ""
241 | }
242 | }
243 |
244 | companion object {
245 | /** Represents the majority of phones in landscape */
246 | val Compact = WindowHeightSizeClass(0)
247 |
248 | /** Represents the majority of tablets in landscape and majority of phones in portrait */
249 | val Medium = WindowHeightSizeClass(1)
250 |
251 | /** Represents the majority of tablets in portrait */
252 | val Expanded = WindowHeightSizeClass(2)
253 |
254 | /**
255 | * The default set of size classes that includes [Compact], [Medium], and [Expanded] size
256 | * classes. Should never expand to ensure behavioral consistency.
257 | */
258 | val DefaultSizeClasses = setOf(Compact, Medium, Expanded)
259 |
260 | /**
261 | * The standard set of size classes. It's supposed to include all size classes and will be
262 | * expanded whenever a new size class is defined. By default
263 | * [WindowSizeClass.calculateFromSize] will only return size classes in [DefaultSizeClasses]
264 | * in order to avoid behavioral changes when new size classes are added. You can opt in to
265 | * support all available size classes by doing:
266 | * ```
267 | * WindowSizeClass.calculateFromSize(
268 | * size = size,
269 | * density = density,
270 | * supportedWidthSizeClasses = WindowWidthSizeClass.StandardSizeClasses,
271 | * supportedHeightSizeClasses = WindowHeightSizeClass.StandardSizeClasses
272 | * )
273 | * ```
274 | */
275 | val StandardSizeClasses get() = DefaultSizeClasses
276 |
277 | private fun WindowHeightSizeClass.breakpoint(): Dp {
278 | return when {
279 | this == Expanded -> 900.dp
280 | this == Medium -> 480.dp
281 | else -> 0.dp
282 | }
283 | }
284 |
285 | /** Calculates the [WindowHeightSizeClass] for a given [height] */
286 | internal fun fromHeight(height: Dp): WindowHeightSizeClass {
287 | return fromHeight(
288 | with(defaultDensity) { height.toPx() },
289 | defaultDensity,
290 | DefaultSizeClasses,
291 | )
292 | }
293 |
294 | /**
295 | * Calculates the best matched [WindowHeightSizeClass] for a given [height] in Pixels and
296 | * a given [Density] from [supportedSizeClasses].
297 | */
298 | internal fun fromHeight(
299 | height: Float,
300 | density: Density,
301 | supportedSizeClasses: Set,
302 | ): WindowHeightSizeClass {
303 | require(height >= 0) { "Width must not be negative" }
304 | require(supportedSizeClasses.isNotEmpty()) { "Must support at least one size class" }
305 | val sortedSizeClasses = supportedSizeClasses.sortedDescending()
306 | // Find the largest supported size class that matches the width
307 | sortedSizeClasses.forEach {
308 | if (height >= with(density) { it.breakpoint().toPx() }) {
309 | return it
310 | }
311 | }
312 | // If none of the size classes matches, return the smallest one.
313 | return sortedSizeClasses.last()
314 | }
315 | }
316 | }
317 |
318 | private val defaultDensity = Density(1F, 1F)
319 |
--------------------------------------------------------------------------------
/sample/ios-app/ios-app.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 56;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | 381953142A41D3B10048CE04 /* ios_appApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381953132A41D3B10048CE04 /* ios_appApp.swift */; };
11 | 381953162A41D3B10048CE04 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 381953152A41D3B10048CE04 /* ContentView.swift */; };
12 | 381953182A41D3B10048CE04 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 381953172A41D3B10048CE04 /* Assets.xcassets */; };
13 | 3819531C2A41D3B10048CE04 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3819531B2A41D3B10048CE04 /* Preview Assets.xcassets */; };
14 | /* End PBXBuildFile section */
15 |
16 | /* Begin PBXFileReference section */
17 | 381953102A41D3B10048CE04 /* ios-app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ios-app.app"; sourceTree = BUILT_PRODUCTS_DIR; };
18 | 381953132A41D3B10048CE04 /* ios_appApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ios_appApp.swift; sourceTree = ""; };
19 | 381953152A41D3B10048CE04 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
20 | 381953172A41D3B10048CE04 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
21 | 381953192A41D3B10048CE04 /* ios_app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ios_app.entitlements; sourceTree = ""; };
22 | 3819531B2A41D3B10048CE04 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
23 | /* End PBXFileReference section */
24 |
25 | /* Begin PBXFrameworksBuildPhase section */
26 | 3819530D2A41D3B10048CE04 /* Frameworks */ = {
27 | isa = PBXFrameworksBuildPhase;
28 | buildActionMask = 2147483647;
29 | files = (
30 | );
31 | runOnlyForDeploymentPostprocessing = 0;
32 | };
33 | /* End PBXFrameworksBuildPhase section */
34 |
35 | /* Begin PBXGroup section */
36 | 381953072A41D3B10048CE04 = {
37 | isa = PBXGroup;
38 | children = (
39 | 381953122A41D3B10048CE04 /* ios-app */,
40 | 381953112A41D3B10048CE04 /* Products */,
41 | );
42 | sourceTree = "";
43 | };
44 | 381953112A41D3B10048CE04 /* Products */ = {
45 | isa = PBXGroup;
46 | children = (
47 | 381953102A41D3B10048CE04 /* ios-app.app */,
48 | );
49 | name = Products;
50 | sourceTree = "";
51 | };
52 | 381953122A41D3B10048CE04 /* ios-app */ = {
53 | isa = PBXGroup;
54 | children = (
55 | 381953132A41D3B10048CE04 /* ios_appApp.swift */,
56 | 381953152A41D3B10048CE04 /* ContentView.swift */,
57 | 381953172A41D3B10048CE04 /* Assets.xcassets */,
58 | 381953192A41D3B10048CE04 /* ios_app.entitlements */,
59 | 3819531A2A41D3B10048CE04 /* Preview Content */,
60 | );
61 | path = "ios-app";
62 | sourceTree = "";
63 | };
64 | 3819531A2A41D3B10048CE04 /* Preview Content */ = {
65 | isa = PBXGroup;
66 | children = (
67 | 3819531B2A41D3B10048CE04 /* Preview Assets.xcassets */,
68 | );
69 | path = "Preview Content";
70 | sourceTree = "";
71 | };
72 | /* End PBXGroup section */
73 |
74 | /* Begin PBXNativeTarget section */
75 | 3819530F2A41D3B10048CE04 /* ios-app */ = {
76 | isa = PBXNativeTarget;
77 | buildConfigurationList = 3819531F2A41D3B10048CE04 /* Build configuration list for PBXNativeTarget "ios-app" */;
78 | buildPhases = (
79 | 381953222A41D3E10048CE04 /* ShellScript */,
80 | 3819530C2A41D3B10048CE04 /* Sources */,
81 | 3819530D2A41D3B10048CE04 /* Frameworks */,
82 | 3819530E2A41D3B10048CE04 /* Resources */,
83 | );
84 | buildRules = (
85 | );
86 | dependencies = (
87 | );
88 | name = "ios-app";
89 | productName = "ios-app";
90 | productReference = 381953102A41D3B10048CE04 /* ios-app.app */;
91 | productType = "com.apple.product-type.application";
92 | };
93 | /* End PBXNativeTarget section */
94 |
95 | /* Begin PBXProject section */
96 | 381953082A41D3B10048CE04 /* Project object */ = {
97 | isa = PBXProject;
98 | attributes = {
99 | BuildIndependentTargetsInParallel = 1;
100 | LastSwiftUpdateCheck = 1430;
101 | LastUpgradeCheck = 1430;
102 | TargetAttributes = {
103 | 3819530F2A41D3B10048CE04 = {
104 | CreatedOnToolsVersion = 14.3.1;
105 | };
106 | };
107 | };
108 | buildConfigurationList = 3819530B2A41D3B10048CE04 /* Build configuration list for PBXProject "ios-app" */;
109 | compatibilityVersion = "Xcode 14.0";
110 | developmentRegion = en;
111 | hasScannedForEncodings = 0;
112 | knownRegions = (
113 | en,
114 | Base,
115 | );
116 | mainGroup = 381953072A41D3B10048CE04;
117 | productRefGroup = 381953112A41D3B10048CE04 /* Products */;
118 | projectDirPath = "";
119 | projectRoot = "";
120 | targets = (
121 | 3819530F2A41D3B10048CE04 /* ios-app */,
122 | );
123 | };
124 | /* End PBXProject section */
125 |
126 | /* Begin PBXResourcesBuildPhase section */
127 | 3819530E2A41D3B10048CE04 /* Resources */ = {
128 | isa = PBXResourcesBuildPhase;
129 | buildActionMask = 2147483647;
130 | files = (
131 | 3819531C2A41D3B10048CE04 /* Preview Assets.xcassets in Resources */,
132 | 381953182A41D3B10048CE04 /* Assets.xcassets in Resources */,
133 | );
134 | runOnlyForDeploymentPostprocessing = 0;
135 | };
136 | /* End PBXResourcesBuildPhase section */
137 |
138 | /* Begin PBXShellScriptBuildPhase section */
139 | 381953222A41D3E10048CE04 /* ShellScript */ = {
140 | isa = PBXShellScriptBuildPhase;
141 | buildActionMask = 2147483647;
142 | files = (
143 | );
144 | inputFileListPaths = (
145 | );
146 | inputPaths = (
147 | );
148 | outputFileListPaths = (
149 | );
150 | outputPaths = (
151 | );
152 | runOnlyForDeploymentPostprocessing = 0;
153 | shellPath = /bin/sh;
154 | shellScript = "cd \"$SRCROOT/../..\"\n./gradlew :sample:shared:embedAndSignAppleFrameworkForXcode\n";
155 | };
156 | /* End PBXShellScriptBuildPhase section */
157 |
158 | /* Begin PBXSourcesBuildPhase section */
159 | 3819530C2A41D3B10048CE04 /* Sources */ = {
160 | isa = PBXSourcesBuildPhase;
161 | buildActionMask = 2147483647;
162 | files = (
163 | 381953162A41D3B10048CE04 /* ContentView.swift in Sources */,
164 | 381953142A41D3B10048CE04 /* ios_appApp.swift in Sources */,
165 | );
166 | runOnlyForDeploymentPostprocessing = 0;
167 | };
168 | /* End PBXSourcesBuildPhase section */
169 |
170 | /* Begin XCBuildConfiguration section */
171 | 3819531D2A41D3B10048CE04 /* Debug */ = {
172 | isa = XCBuildConfiguration;
173 | buildSettings = {
174 | ALWAYS_SEARCH_USER_PATHS = NO;
175 | CLANG_ANALYZER_NONNULL = YES;
176 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
177 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
178 | CLANG_ENABLE_MODULES = YES;
179 | CLANG_ENABLE_OBJC_ARC = YES;
180 | CLANG_ENABLE_OBJC_WEAK = YES;
181 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
182 | CLANG_WARN_BOOL_CONVERSION = YES;
183 | CLANG_WARN_COMMA = YES;
184 | CLANG_WARN_CONSTANT_CONVERSION = YES;
185 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
186 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
187 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
188 | CLANG_WARN_EMPTY_BODY = YES;
189 | CLANG_WARN_ENUM_CONVERSION = YES;
190 | CLANG_WARN_INFINITE_RECURSION = YES;
191 | CLANG_WARN_INT_CONVERSION = YES;
192 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
193 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
194 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
195 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
196 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
197 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
198 | CLANG_WARN_STRICT_PROTOTYPES = YES;
199 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
200 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
201 | CLANG_WARN_UNREACHABLE_CODE = YES;
202 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
203 | COPY_PHASE_STRIP = NO;
204 | DEBUG_INFORMATION_FORMAT = dwarf;
205 | ENABLE_STRICT_OBJC_MSGSEND = YES;
206 | ENABLE_TESTABILITY = YES;
207 | GCC_C_LANGUAGE_STANDARD = gnu11;
208 | GCC_DYNAMIC_NO_PIC = NO;
209 | GCC_NO_COMMON_BLOCKS = YES;
210 | GCC_OPTIMIZATION_LEVEL = 0;
211 | GCC_PREPROCESSOR_DEFINITIONS = (
212 | "DEBUG=1",
213 | "$(inherited)",
214 | );
215 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
216 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
217 | GCC_WARN_UNDECLARED_SELECTOR = YES;
218 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
219 | GCC_WARN_UNUSED_FUNCTION = YES;
220 | GCC_WARN_UNUSED_VARIABLE = YES;
221 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
222 | MTL_FAST_MATH = YES;
223 | ONLY_ACTIVE_ARCH = YES;
224 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
225 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
226 | };
227 | name = Debug;
228 | };
229 | 3819531E2A41D3B10048CE04 /* Release */ = {
230 | isa = XCBuildConfiguration;
231 | buildSettings = {
232 | ALWAYS_SEARCH_USER_PATHS = NO;
233 | CLANG_ANALYZER_NONNULL = YES;
234 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
235 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
236 | CLANG_ENABLE_MODULES = YES;
237 | CLANG_ENABLE_OBJC_ARC = YES;
238 | CLANG_ENABLE_OBJC_WEAK = YES;
239 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
240 | CLANG_WARN_BOOL_CONVERSION = YES;
241 | CLANG_WARN_COMMA = YES;
242 | CLANG_WARN_CONSTANT_CONVERSION = YES;
243 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
244 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
245 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
246 | CLANG_WARN_EMPTY_BODY = YES;
247 | CLANG_WARN_ENUM_CONVERSION = YES;
248 | CLANG_WARN_INFINITE_RECURSION = YES;
249 | CLANG_WARN_INT_CONVERSION = YES;
250 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
251 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
252 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
253 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
254 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
255 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
256 | CLANG_WARN_STRICT_PROTOTYPES = YES;
257 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
258 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
259 | CLANG_WARN_UNREACHABLE_CODE = YES;
260 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
261 | COPY_PHASE_STRIP = NO;
262 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
263 | ENABLE_NS_ASSERTIONS = NO;
264 | ENABLE_STRICT_OBJC_MSGSEND = YES;
265 | GCC_C_LANGUAGE_STANDARD = gnu11;
266 | GCC_NO_COMMON_BLOCKS = YES;
267 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
268 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
269 | GCC_WARN_UNDECLARED_SELECTOR = YES;
270 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
271 | GCC_WARN_UNUSED_FUNCTION = YES;
272 | GCC_WARN_UNUSED_VARIABLE = YES;
273 | MTL_ENABLE_DEBUG_INFO = NO;
274 | MTL_FAST_MATH = YES;
275 | SWIFT_COMPILATION_MODE = wholemodule;
276 | SWIFT_OPTIMIZATION_LEVEL = "-O";
277 | };
278 | name = Release;
279 | };
280 | 381953202A41D3B10048CE04 /* Debug */ = {
281 | isa = XCBuildConfiguration;
282 | buildSettings = {
283 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
284 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
285 | CODE_SIGN_STYLE = Automatic;
286 | CURRENT_PROJECT_VERSION = 1;
287 | DEVELOPMENT_ASSET_PATHS = "\"ios-app/Preview Content\"";
288 | ENABLE_PREVIEWS = YES;
289 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
290 | GENERATE_INFOPLIST_FILE = YES;
291 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
292 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
293 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
294 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
295 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
296 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
297 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
298 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
299 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
300 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
301 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
302 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
303 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
304 | MACOSX_DEPLOYMENT_TARGET = 13.3;
305 | MARKETING_VERSION = 1.0;
306 | OTHER_LDFLAGS = (
307 | "$(inherited)",
308 | "-framework",
309 | shared,
310 | );
311 | PRODUCT_BUNDLE_IDENTIFIER = dev.chrisbanes.material3.windowsizeclass.sample;
312 | PRODUCT_NAME = "$(TARGET_NAME)";
313 | SDKROOT = auto;
314 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
315 | SUPPORTS_MACCATALYST = NO;
316 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
317 | SWIFT_EMIT_LOC_STRINGS = YES;
318 | SWIFT_VERSION = 5.0;
319 | TARGETED_DEVICE_FAMILY = "1,2";
320 | };
321 | name = Debug;
322 | };
323 | 381953212A41D3B10048CE04 /* Release */ = {
324 | isa = XCBuildConfiguration;
325 | buildSettings = {
326 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
327 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
328 | CODE_SIGN_STYLE = Automatic;
329 | CURRENT_PROJECT_VERSION = 1;
330 | DEVELOPMENT_ASSET_PATHS = "\"ios-app/Preview Content\"";
331 | ENABLE_PREVIEWS = YES;
332 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)";
333 | GENERATE_INFOPLIST_FILE = YES;
334 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
335 | "INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
336 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
337 | "INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
338 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
339 | "INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
340 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
341 | "INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
342 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
343 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
344 | IPHONEOS_DEPLOYMENT_TARGET = 16.4;
345 | LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
346 | "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
347 | MACOSX_DEPLOYMENT_TARGET = 13.3;
348 | MARKETING_VERSION = 1.0;
349 | OTHER_LDFLAGS = (
350 | "$(inherited)",
351 | "-framework",
352 | shared,
353 | );
354 | PRODUCT_BUNDLE_IDENTIFIER = dev.chrisbanes.material3.windowsizeclass.sample;
355 | PRODUCT_NAME = "$(TARGET_NAME)";
356 | SDKROOT = auto;
357 | SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
358 | SUPPORTS_MACCATALYST = NO;
359 | SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
360 | SWIFT_EMIT_LOC_STRINGS = YES;
361 | SWIFT_VERSION = 5.0;
362 | TARGETED_DEVICE_FAMILY = "1,2";
363 | };
364 | name = Release;
365 | };
366 | /* End XCBuildConfiguration section */
367 |
368 | /* Begin XCConfigurationList section */
369 | 3819530B2A41D3B10048CE04 /* Build configuration list for PBXProject "ios-app" */ = {
370 | isa = XCConfigurationList;
371 | buildConfigurations = (
372 | 3819531D2A41D3B10048CE04 /* Debug */,
373 | 3819531E2A41D3B10048CE04 /* Release */,
374 | );
375 | defaultConfigurationIsVisible = 0;
376 | defaultConfigurationName = Release;
377 | };
378 | 3819531F2A41D3B10048CE04 /* Build configuration list for PBXNativeTarget "ios-app" */ = {
379 | isa = XCConfigurationList;
380 | buildConfigurations = (
381 | 381953202A41D3B10048CE04 /* Debug */,
382 | 381953212A41D3B10048CE04 /* Release */,
383 | );
384 | defaultConfigurationIsVisible = 0;
385 | defaultConfigurationName = Release;
386 | };
387 | /* End XCConfigurationList section */
388 | };
389 | rootObject = 381953082A41D3B10048CE04 /* Project object */;
390 | }
391 |
--------------------------------------------------------------------------------
/lib/src/commonTest/kotlin/androidx/compose/material3/windowsizeclass/WindowSizeClassTest.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2022 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package androidx.compose.material3.windowsizeclass
18 |
19 | import androidx.compose.ui.unit.Density
20 | import androidx.compose.ui.unit.dp
21 | import assertk.assertThat
22 | import assertk.assertions.isEqualTo
23 | import assertk.assertions.isFalse
24 | import assertk.assertions.isTrue
25 | import kotlin.test.Test
26 | import kotlin.test.assertFailsWith
27 |
28 | class WindowSizeClassTest {
29 | @Test
30 | fun calculateWidthSizeClass_forNegativeWidth_throws() {
31 | assertFailsWith(IllegalArgumentException::class) {
32 | WindowWidthSizeClass.fromWidth((-10).dp)
33 | }
34 | }
35 |
36 | @Test
37 | fun calculateHeightSizeClass_forNegativeHeight_throws() {
38 | assertFailsWith(IllegalArgumentException::class) {
39 | WindowHeightSizeClass.fromHeight((-10).dp)
40 | }
41 | }
42 |
43 | @Test
44 | fun calculateWidthSizeClass_forNegativeWidthInPx_throws() {
45 | assertFailsWith(IllegalArgumentException::class) {
46 | WindowWidthSizeClass.fromWidth(
47 | -10F,
48 | DefaultDensity,
49 | WindowWidthSizeClass.DefaultSizeClasses,
50 | )
51 | }
52 | }
53 |
54 | @Test
55 | fun calculateHeightSizeClass_forNegativeHeightInPx_throws() {
56 | assertFailsWith(IllegalArgumentException::class) {
57 | WindowHeightSizeClass.fromHeight(
58 | -10F,
59 | DefaultDensity,
60 | WindowHeightSizeClass.DefaultSizeClasses,
61 | )
62 | }
63 | }
64 |
65 | @Test
66 | fun calculateWidthSizeClass_noSupportedSizeClass_throws() {
67 | assertFailsWith(IllegalArgumentException::class) {
68 | WindowWidthSizeClass.fromWidth(10F, DefaultDensity, emptySet())
69 | }
70 | }
71 |
72 | @Test
73 | fun calculateHeightSizeClass_noSupportedSizeClass_throws() {
74 | assertFailsWith(IllegalArgumentException::class) {
75 | WindowHeightSizeClass.fromHeight(10F, DefaultDensity, emptySet())
76 | }
77 | }
78 |
79 | @Test
80 | fun calculateWidthSizeClass() {
81 | assertThat(WindowWidthSizeClass.fromWidth(0.dp)).isEqualTo(WindowWidthSizeClass.Compact)
82 | assertThat(WindowWidthSizeClass.fromWidth(200.dp)).isEqualTo(WindowWidthSizeClass.Compact)
83 |
84 | assertThat(WindowWidthSizeClass.fromWidth(600.dp)).isEqualTo(WindowWidthSizeClass.Medium)
85 | assertThat(WindowWidthSizeClass.fromWidth(700.dp)).isEqualTo(WindowWidthSizeClass.Medium)
86 |
87 | assertThat(WindowWidthSizeClass.fromWidth(840.dp)).isEqualTo(WindowWidthSizeClass.Expanded)
88 | assertThat(WindowWidthSizeClass.fromWidth(1000.dp)).isEqualTo(WindowWidthSizeClass.Expanded)
89 | }
90 |
91 | @Test
92 | fun calculateHeightSizeClass() {
93 | assertThat(WindowHeightSizeClass.fromHeight(0.dp)).isEqualTo(WindowHeightSizeClass.Compact)
94 | assertThat(WindowHeightSizeClass.fromHeight(200.dp))
95 | .isEqualTo(WindowHeightSizeClass.Compact)
96 |
97 | assertThat(WindowHeightSizeClass.fromHeight(480.dp)).isEqualTo(WindowHeightSizeClass.Medium)
98 | assertThat(WindowHeightSizeClass.fromHeight(700.dp))
99 | .isEqualTo(WindowHeightSizeClass.Medium)
100 |
101 | assertThat(WindowHeightSizeClass.fromHeight(900.dp))
102 | .isEqualTo(WindowHeightSizeClass.Expanded)
103 | assertThat(WindowHeightSizeClass.fromHeight(1000.dp))
104 | .isEqualTo(WindowHeightSizeClass.Expanded)
105 | }
106 |
107 | @Test
108 | fun calculateWidthSizeClass_withDefaultDensity() {
109 | assertWidthClass(WindowWidthSizeClass.Compact, 0F)
110 | assertWidthClass(WindowWidthSizeClass.Compact, 200F)
111 |
112 | assertWidthClass(WindowWidthSizeClass.Medium, 600F)
113 | assertWidthClass(WindowWidthSizeClass.Medium, 700F)
114 |
115 | assertWidthClass(WindowWidthSizeClass.Expanded, 840F)
116 | assertWidthClass(WindowWidthSizeClass.Expanded, 1000F)
117 | }
118 |
119 | @Test
120 | fun calculateHeightSizeClass_withDefaultDensity() {
121 | assertHeightClass(WindowHeightSizeClass.Compact, 0F)
122 | assertHeightClass(WindowHeightSizeClass.Compact, 200F)
123 |
124 | assertHeightClass(WindowHeightSizeClass.Medium, 480F)
125 | assertHeightClass(WindowHeightSizeClass.Medium, 700F)
126 |
127 | assertHeightClass(WindowHeightSizeClass.Expanded, 900F)
128 | assertHeightClass(WindowHeightSizeClass.Expanded, 1000F)
129 | }
130 |
131 | @Test
132 | fun calculateWidthSizeClass_withMockDensity() {
133 | val mockDensity = Density(2F, 2F)
134 |
135 | assertWidthClass(WindowWidthSizeClass.Compact, 0F, mockDensity)
136 | assertWidthClass(WindowWidthSizeClass.Compact, 400F, mockDensity)
137 |
138 | assertWidthClass(WindowWidthSizeClass.Medium, 1200F, mockDensity)
139 | assertWidthClass(WindowWidthSizeClass.Medium, 1400F, mockDensity)
140 |
141 | assertWidthClass(WindowWidthSizeClass.Expanded, 1680F, mockDensity)
142 | assertWidthClass(WindowWidthSizeClass.Expanded, 2000F, mockDensity)
143 | }
144 |
145 | @Test
146 | fun calculateHeightSizeClass_withMockDensity() {
147 | val mockDensity = Density(2F, 2F)
148 |
149 | assertHeightClass(WindowHeightSizeClass.Compact, 0F, mockDensity)
150 | assertHeightClass(WindowHeightSizeClass.Compact, 400F, mockDensity)
151 |
152 | assertHeightClass(WindowHeightSizeClass.Medium, 960F, mockDensity)
153 | assertHeightClass(WindowHeightSizeClass.Medium, 1400F, mockDensity)
154 |
155 | assertHeightClass(WindowHeightSizeClass.Expanded, 1800F, mockDensity)
156 | assertHeightClass(WindowHeightSizeClass.Expanded, 2000F, mockDensity)
157 | }
158 |
159 | @Test
160 | fun calculateWidthSizeClass_useBestMatchedSupportedSizeClasses() {
161 | assertWidthClass(
162 | WindowWidthSizeClass.Compact,
163 | 700F,
164 | supportedSizeClasses = setOf(
165 | WindowWidthSizeClass.Compact,
166 | WindowWidthSizeClass.Expanded,
167 | ),
168 | )
169 |
170 | assertWidthClass(
171 | WindowWidthSizeClass.Medium,
172 | 1000F,
173 | supportedSizeClasses = setOf(
174 | WindowWidthSizeClass.Compact,
175 | WindowWidthSizeClass.Medium,
176 | ),
177 | )
178 | }
179 |
180 | @Test
181 | fun calculateHeightSizeClass_useBestMatchedSupportedSizeClasses() {
182 | assertHeightClass(
183 | WindowHeightSizeClass.Compact,
184 | 700F,
185 | supportedSizeClasses = setOf(
186 | WindowHeightSizeClass.Compact,
187 | WindowHeightSizeClass.Expanded,
188 | ),
189 | )
190 |
191 | assertHeightClass(
192 | WindowHeightSizeClass.Medium,
193 | 1000F,
194 | supportedSizeClasses = setOf(
195 | WindowHeightSizeClass.Compact,
196 | WindowHeightSizeClass.Medium,
197 | ),
198 | )
199 | }
200 |
201 | @Test
202 | fun calculateWidthSizeClass_fallbackToTheSmallestSizeClasses() {
203 | assertWidthClass(
204 | WindowWidthSizeClass.Medium,
205 | 200F,
206 | supportedSizeClasses = setOf(
207 | WindowWidthSizeClass.Medium,
208 | WindowWidthSizeClass.Expanded,
209 | ),
210 | )
211 | }
212 |
213 | @Test
214 | fun calculateHeightSizeClass_fallbackToTheSmallestSizeClasses() {
215 | assertHeightClass(
216 | WindowHeightSizeClass.Medium,
217 | 200F,
218 | supportedSizeClasses = setOf(
219 | WindowHeightSizeClass.Medium,
220 | WindowHeightSizeClass.Expanded,
221 | ),
222 | )
223 | }
224 |
225 | @Test
226 | fun widthSizeClassToString() {
227 | assertThat(WindowWidthSizeClass.Compact.toString())
228 | .isEqualTo("WindowWidthSizeClass.Compact")
229 | assertThat(WindowWidthSizeClass.Medium.toString())
230 | .isEqualTo("WindowWidthSizeClass.Medium")
231 | assertThat(WindowWidthSizeClass.Expanded.toString())
232 | .isEqualTo("WindowWidthSizeClass.Expanded")
233 | }
234 |
235 | @Test
236 | fun heightSizeClassToString() {
237 | assertThat(WindowHeightSizeClass.Compact.toString())
238 | .isEqualTo("WindowHeightSizeClass.Compact")
239 | assertThat(WindowHeightSizeClass.Medium.toString())
240 | .isEqualTo("WindowHeightSizeClass.Medium")
241 | assertThat(WindowHeightSizeClass.Expanded.toString())
242 | .isEqualTo("WindowHeightSizeClass.Expanded")
243 | }
244 |
245 | @Test
246 | fun widthSizeClassCompareTo() {
247 | // Less than
248 | assertThat(WindowWidthSizeClass.Compact < WindowWidthSizeClass.Medium).isTrue()
249 | assertThat(WindowWidthSizeClass.Compact < WindowWidthSizeClass.Expanded).isTrue()
250 | assertThat(WindowWidthSizeClass.Medium < WindowWidthSizeClass.Expanded).isTrue()
251 |
252 | assertThat(WindowWidthSizeClass.Compact < WindowWidthSizeClass.Compact).isFalse()
253 | assertThat(WindowWidthSizeClass.Medium < WindowWidthSizeClass.Medium).isFalse()
254 | assertThat(WindowWidthSizeClass.Expanded < WindowWidthSizeClass.Expanded).isFalse()
255 |
256 | assertThat(WindowWidthSizeClass.Expanded < WindowWidthSizeClass.Medium).isFalse()
257 | assertThat(WindowWidthSizeClass.Expanded < WindowWidthSizeClass.Compact).isFalse()
258 | assertThat(WindowWidthSizeClass.Medium < WindowWidthSizeClass.Compact).isFalse()
259 |
260 | // Less than or equal to
261 | assertThat(WindowWidthSizeClass.Compact <= WindowWidthSizeClass.Compact).isTrue()
262 | assertThat(WindowWidthSizeClass.Compact <= WindowWidthSizeClass.Medium).isTrue()
263 | assertThat(WindowWidthSizeClass.Compact <= WindowWidthSizeClass.Expanded).isTrue()
264 | assertThat(WindowWidthSizeClass.Medium <= WindowWidthSizeClass.Medium).isTrue()
265 | assertThat(WindowWidthSizeClass.Medium <= WindowWidthSizeClass.Expanded).isTrue()
266 | assertThat(WindowWidthSizeClass.Expanded <= WindowWidthSizeClass.Expanded).isTrue()
267 |
268 | assertThat(WindowWidthSizeClass.Expanded <= WindowWidthSizeClass.Medium).isFalse()
269 | assertThat(WindowWidthSizeClass.Expanded <= WindowWidthSizeClass.Compact).isFalse()
270 | assertThat(WindowWidthSizeClass.Medium <= WindowWidthSizeClass.Compact).isFalse()
271 |
272 | // Greater than
273 | assertThat(WindowWidthSizeClass.Expanded > WindowWidthSizeClass.Medium).isTrue()
274 | assertThat(WindowWidthSizeClass.Expanded > WindowWidthSizeClass.Compact).isTrue()
275 | assertThat(WindowWidthSizeClass.Medium > WindowWidthSizeClass.Compact).isTrue()
276 |
277 | assertThat(WindowWidthSizeClass.Expanded > WindowWidthSizeClass.Expanded).isFalse()
278 | assertThat(WindowWidthSizeClass.Medium > WindowWidthSizeClass.Medium).isFalse()
279 | assertThat(WindowWidthSizeClass.Compact > WindowWidthSizeClass.Compact).isFalse()
280 |
281 | assertThat(WindowWidthSizeClass.Compact > WindowWidthSizeClass.Medium).isFalse()
282 | assertThat(WindowWidthSizeClass.Compact > WindowWidthSizeClass.Expanded).isFalse()
283 | assertThat(WindowWidthSizeClass.Medium > WindowWidthSizeClass.Expanded).isFalse()
284 |
285 | // Greater than or equal to
286 | assertThat(WindowWidthSizeClass.Expanded >= WindowWidthSizeClass.Expanded).isTrue()
287 | assertThat(WindowWidthSizeClass.Expanded >= WindowWidthSizeClass.Medium).isTrue()
288 | assertThat(WindowWidthSizeClass.Expanded >= WindowWidthSizeClass.Compact).isTrue()
289 | assertThat(WindowWidthSizeClass.Medium >= WindowWidthSizeClass.Medium).isTrue()
290 | assertThat(WindowWidthSizeClass.Medium >= WindowWidthSizeClass.Compact).isTrue()
291 | assertThat(WindowWidthSizeClass.Compact >= WindowWidthSizeClass.Compact).isTrue()
292 |
293 | assertThat(WindowWidthSizeClass.Compact >= WindowWidthSizeClass.Medium).isFalse()
294 | assertThat(WindowWidthSizeClass.Compact >= WindowWidthSizeClass.Expanded).isFalse()
295 | assertThat(WindowWidthSizeClass.Medium >= WindowWidthSizeClass.Expanded).isFalse()
296 | }
297 |
298 | @Test
299 | fun heightSizeClassCompareTo() {
300 | // Less than
301 | assertThat(WindowHeightSizeClass.Compact < WindowHeightSizeClass.Medium).isTrue()
302 | assertThat(WindowHeightSizeClass.Compact < WindowHeightSizeClass.Expanded).isTrue()
303 | assertThat(WindowHeightSizeClass.Medium < WindowHeightSizeClass.Expanded).isTrue()
304 |
305 | assertThat(WindowHeightSizeClass.Compact < WindowHeightSizeClass.Compact).isFalse()
306 | assertThat(WindowHeightSizeClass.Medium < WindowHeightSizeClass.Medium).isFalse()
307 | assertThat(WindowHeightSizeClass.Expanded < WindowHeightSizeClass.Expanded).isFalse()
308 |
309 | assertThat(WindowHeightSizeClass.Expanded < WindowHeightSizeClass.Medium).isFalse()
310 | assertThat(WindowHeightSizeClass.Expanded < WindowHeightSizeClass.Compact).isFalse()
311 | assertThat(WindowHeightSizeClass.Medium < WindowHeightSizeClass.Compact).isFalse()
312 |
313 | // Less than or equal to
314 | assertThat(WindowHeightSizeClass.Compact <= WindowHeightSizeClass.Compact).isTrue()
315 | assertThat(WindowHeightSizeClass.Compact <= WindowHeightSizeClass.Medium).isTrue()
316 | assertThat(WindowHeightSizeClass.Compact <= WindowHeightSizeClass.Expanded).isTrue()
317 | assertThat(WindowHeightSizeClass.Medium <= WindowHeightSizeClass.Medium).isTrue()
318 | assertThat(WindowHeightSizeClass.Medium <= WindowHeightSizeClass.Expanded).isTrue()
319 | assertThat(WindowHeightSizeClass.Expanded <= WindowHeightSizeClass.Expanded).isTrue()
320 |
321 | assertThat(WindowHeightSizeClass.Expanded <= WindowHeightSizeClass.Medium).isFalse()
322 | assertThat(WindowHeightSizeClass.Expanded <= WindowHeightSizeClass.Compact).isFalse()
323 | assertThat(WindowHeightSizeClass.Medium <= WindowHeightSizeClass.Compact).isFalse()
324 |
325 | // Greater than
326 | assertThat(WindowHeightSizeClass.Expanded > WindowHeightSizeClass.Medium).isTrue()
327 | assertThat(WindowHeightSizeClass.Expanded > WindowHeightSizeClass.Compact).isTrue()
328 | assertThat(WindowHeightSizeClass.Medium > WindowHeightSizeClass.Compact).isTrue()
329 |
330 | assertThat(WindowHeightSizeClass.Expanded > WindowHeightSizeClass.Expanded).isFalse()
331 | assertThat(WindowHeightSizeClass.Medium > WindowHeightSizeClass.Medium).isFalse()
332 | assertThat(WindowHeightSizeClass.Compact > WindowHeightSizeClass.Compact).isFalse()
333 |
334 | assertThat(WindowHeightSizeClass.Compact > WindowHeightSizeClass.Medium).isFalse()
335 | assertThat(WindowHeightSizeClass.Compact > WindowHeightSizeClass.Expanded).isFalse()
336 | assertThat(WindowHeightSizeClass.Medium > WindowHeightSizeClass.Expanded).isFalse()
337 |
338 | // Greater than or equal to
339 | assertThat(WindowHeightSizeClass.Expanded >= WindowHeightSizeClass.Expanded).isTrue()
340 | assertThat(WindowHeightSizeClass.Expanded >= WindowHeightSizeClass.Medium).isTrue()
341 | assertThat(WindowHeightSizeClass.Expanded >= WindowHeightSizeClass.Compact).isTrue()
342 | assertThat(WindowHeightSizeClass.Medium >= WindowHeightSizeClass.Medium).isTrue()
343 | assertThat(WindowHeightSizeClass.Medium >= WindowHeightSizeClass.Compact).isTrue()
344 | assertThat(WindowHeightSizeClass.Compact >= WindowHeightSizeClass.Compact).isTrue()
345 |
346 | assertThat(WindowHeightSizeClass.Compact >= WindowHeightSizeClass.Medium).isFalse()
347 | assertThat(WindowHeightSizeClass.Compact >= WindowHeightSizeClass.Expanded).isFalse()
348 | assertThat(WindowHeightSizeClass.Medium >= WindowHeightSizeClass.Expanded).isFalse()
349 | }
350 |
351 | private fun assertWidthClass(
352 | expectedSizeClass: WindowWidthSizeClass,
353 | width: Float,
354 | density: Density = DefaultDensity,
355 | supportedSizeClasses: Set = WindowWidthSizeClass.DefaultSizeClasses,
356 | ) {
357 | assertThat(
358 | WindowWidthSizeClass.fromWidth(width, density, supportedSizeClasses),
359 | ).isEqualTo(expectedSizeClass)
360 | }
361 |
362 | private fun assertHeightClass(
363 | expectedSizeClass: WindowHeightSizeClass,
364 | height: Float,
365 | density: Density = DefaultDensity,
366 | supportedSizeClasses: Set = WindowHeightSizeClass.DefaultSizeClasses,
367 | ) {
368 | assertThat(
369 | WindowHeightSizeClass.fromHeight(height, density, supportedSizeClasses),
370 | ).isEqualTo(expectedSizeClass)
371 | }
372 |
373 | companion object {
374 | private val DefaultDensity = Density(1F, 1F)
375 | }
376 | }
377 |
--------------------------------------------------------------------------------