├── .github
├── CODEOWNERS
├── screenshots
│ ├── main-dark.png
│ ├── main-light.png
│ ├── change-color.png
│ ├── change-color-pink.png
│ └── change-color-updated-main-page.png
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── verification.yml
├── gradle.properties
├── src
├── main
│ ├── resources
│ │ ├── icon.icns
│ │ ├── icon.png
│ │ ├── ic_arrow_back.xml
│ │ └── ic_android.xml
│ └── kotlin
│ │ ├── m3components
│ │ └── components
│ │ │ ├── Text.kt
│ │ │ ├── M3Divider.kt
│ │ │ ├── ProgressBar.kt
│ │ │ ├── Slider.kt
│ │ │ ├── Tab.kt
│ │ │ ├── Switch.kt
│ │ │ ├── Checkbox.kt
│ │ │ ├── Chips.kt
│ │ │ ├── Cards.kt
│ │ │ ├── NavigationBar.kt
│ │ │ ├── TextFields.kt
│ │ │ ├── TopAppBar.kt
│ │ │ └── Buttons.kt
│ │ ├── TestTags.kt
│ │ ├── FileUtils.kt
│ │ ├── ThemeColorPicker.kt
│ │ ├── Main.kt
│ │ └── ComponentScope.kt
└── test
│ └── kotlin
│ ├── FileUtilsKtTest.kt
│ └── m3components
│ └── components
│ ├── SaveFileDialogTest.kt
│ ├── SliderVisibilityTest.kt
│ ├── DividerVisibilityTest.kt
│ ├── ProgressBarVisibilityTest.kt
│ ├── ChipsVisibilityTest.kt
│ ├── SwitchVisibilityTest.kt
│ ├── CheckboxVisibilityTest.kt
│ ├── TextFieldVisibilityTest.kt
│ ├── CardVisibilityTest.kt
│ ├── TopAppBarVisibilityTest.kt
│ ├── ButtonVisibilityTest.kt
│ └── NavRailButtonTest.kt
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── renovate.json
├── settings.gradle.kts
├── .gitignore
├── CONTRIBUTING.md
├── LICENSE
├── gradlew.bat
├── README.md
└── gradlew
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @oas004 @fgiris
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | kotlin.code.style=official
2 |
--------------------------------------------------------------------------------
/src/main/resources/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/src/main/resources/icon.icns
--------------------------------------------------------------------------------
/src/main/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/src/main/resources/icon.png
--------------------------------------------------------------------------------
/.github/screenshots/main-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/main-dark.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/.github/screenshots/main-light.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/main-light.png
--------------------------------------------------------------------------------
/.github/screenshots/change-color.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/change-color.png
--------------------------------------------------------------------------------
/.github/screenshots/change-color-pink.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/change-color-pink.png
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ]
6 | }
7 |
--------------------------------------------------------------------------------
/.github/screenshots/change-color-updated-main-page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/oas004/Material3Playground/HEAD/.github/screenshots/change-color-updated-main-page.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | gradlePluginPortal()
4 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
5 | }
6 | }
7 | rootProject.name = "Material3Playground"
8 |
9 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/src/main/resources/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Text.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.material3.MaterialTheme
4 | import androidx.compose.material3.Text
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.text.TextStyle
7 |
8 | @Composable
9 | fun M3OnSurfaceText(
10 | text: String,
11 | style: TextStyle,
12 | ) {
13 | Text(
14 | color = MaterialTheme.colorScheme.onSurface,
15 | text = text,
16 | style = style,
17 | )
18 | }
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/M3Divider.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.lazy.grid.LazyGridScope
4 | import androidx.compose.material3.Divider
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.platform.testTag
7 | import androidx.compose.ui.unit.dp
8 |
9 | fun LazyGridScope.m3Divider() {
10 | item {
11 | Divider(
12 | modifier = Modifier.testTag(TestTags.Divider.regular),
13 | thickness = 0.5.dp
14 | )
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/test/kotlin/FileUtilsKtTest.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.graphics.Color
2 | import org.junit.Assert.*
3 | import org.junit.Test
4 |
5 | class FileUtilsKtTest {
6 | @Test
7 | fun testGetColorNameFunction() {
8 | val colorNameLowerCase = "red"
9 | val color = Color.Red
10 |
11 | val result = getColorKotlinProperty(colorNameLowerCase, color)
12 |
13 | // Assert
14 | val expected = "val Red: Color = Color(0xFFFF0000) \n"
15 | assertEquals(expected, result)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /idea
4 | /local.properties
5 | /.idea/caches
6 | /.idea/libraries
7 | /.idea/modules.xml
8 | /.idea/workspace.xml
9 | /.idea/navEditor.xml
10 | /.idea/assetWizardSettings.xml
11 | /.idea/markdown-navigator-enh.xml
12 | /.idea/markdown-navigator.xml
13 | /.idea/shelf/
14 | .DS_Store
15 | */build
16 | /captures
17 | .externalNativeBuild
18 | .cxx
19 |
20 | /src/build
21 |
22 | **/.idea/**
23 |
24 | # Local configuration file (sdk path, etc)
25 | local.properties
26 |
27 | # Build folder
28 | **/build/**
29 |
30 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it.
2 |
3 | Steps to start contributing are
4 |
5 | 1. Fork project.
6 | 2. Clone your fork.
7 | 3. Find issues that needs to be solved/make an issue and discuss solutions.
8 | 4. Make a pull request to your forked project.
9 | 5. Sync PR to this project.
10 |
11 | If you have any questions about this process. Please do reach out! :) Have fun!
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/ProgressBar.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.lazy.grid.LazyGridScope
5 | import androidx.compose.material3.CircularProgressIndicator
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.platform.testTag
8 |
9 | fun LazyGridScope.m3ProgressBar() {
10 | item {
11 | Box {
12 | CircularProgressIndicator(
13 | modifier = Modifier.testTag(TestTags.ProgressBar.circular)
14 | )
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/SaveFileDialogTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import TestTags
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import org.junit.Rule
8 | import org.junit.Test
9 |
10 | class SaveFileDialogTest {
11 |
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testAlertDialogIsNotOpenAtDefault() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.FileSaverDialog.dialog).assertDoesNotExist()
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - OS: [e.g. macOS]
28 | - Version [e.g. 22]
29 |
30 | **Additional context**
31 | Add any other context about the problem here.
32 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Slider.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.lazy.grid.LazyGridScope
4 | import androidx.compose.material3.Slider
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import androidx.compose.ui.Modifier
10 | import androidx.compose.ui.platform.testTag
11 |
12 | fun LazyGridScope.m3Slider() {
13 | item {
14 | var sliderPosition by remember { mutableStateOf(0f) }
15 |
16 | Slider(
17 | modifier = Modifier.testTag(TestTags.Slider.regular),
18 | value = sliderPosition,
19 | onValueChange = { sliderPosition = it }
20 | )
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/SliderVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.performClick
8 | import org.junit.Rule
9 | import org.junit.Test
10 |
11 | class SliderVisibilityTest {
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testSliderIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.NavRailButtons.sliders).performClick()
23 | waitForIdle()
24 | onNodeWithTag(TestTags.Slider.regular, false).assertIsDisplayed()
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/DividerVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.performClick
8 | import org.junit.Rule
9 | import org.junit.Test
10 |
11 | class DividerVisibilityTest {
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testDividerIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.NavRailButtons.dividers).performClick()
23 | waitForIdle()
24 | onNodeWithTag(TestTags.Divider.regular, false).assertIsDisplayed()
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/ProgressBarVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.performClick
8 | import org.junit.Rule
9 | import org.junit.Test
10 |
11 | class ProgressBarVisibilityTest {
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testProgressBarIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.NavRailButtons.progressBar).performClick()
23 | waitForIdle()
24 | onNodeWithTag(TestTags.ProgressBar.circular, false).assertIsDisplayed()
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/main/resources/ic_android.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/ChipsVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import androidx.compose.ui.test.assertCountEquals
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onAllNodesWithTag
7 | import androidx.compose.ui.test.onNodeWithTag
8 | import androidx.compose.ui.test.performClick
9 | import org.junit.Rule
10 | import org.junit.Test
11 |
12 | class ChipsVisibilityTest {
13 | @get:Rule
14 | val rule = createComposeRule()
15 |
16 | @Test
17 | fun testChipIsPresent() {
18 | rule.apply {
19 | setContent {
20 | Material3Playground(false)
21 | }
22 | waitForIdle()
23 | onNodeWithTag(TestTags.NavRailButtons.chips).performClick()
24 | waitForIdle()
25 | onAllNodesWithTag(TestTags.CheckBox.regular, false).assertCountEquals(3)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/SwitchVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import androidx.compose.ui.test.assertCountEquals
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onAllNodesWithTag
7 | import androidx.compose.ui.test.onNodeWithTag
8 | import androidx.compose.ui.test.performClick
9 | import org.junit.Rule
10 | import org.junit.Test
11 |
12 | class SwitchVisibilityTest {
13 | @get:Rule
14 | val rule = createComposeRule()
15 |
16 | @Test
17 | fun testSwitchIsPresent() {
18 | rule.apply {
19 | setContent {
20 | Material3Playground(false)
21 | }
22 | waitForIdle()
23 | onNodeWithTag(TestTags.NavRailButtons.switch).performClick()
24 | waitForIdle()
25 | onAllNodesWithTag(TestTags.Switch.regular, false).assertCountEquals(2)
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Tab.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.lazy.grid.LazyGridScope
4 | import androidx.compose.material3.Tab
5 | import androidx.compose.material3.TabRow
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.mutableStateOf
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.runtime.setValue
11 |
12 | fun LazyGridScope.m3Tab() {
13 | item {
14 | var selectedIndex by remember { mutableStateOf(0) }
15 | val tabs = listOf("Accounts", "Cards", "Funds")
16 |
17 | TabRow(selectedTabIndex = selectedIndex) {
18 | tabs.forEachIndexed { index, title ->
19 | Tab(
20 | text = { Text(title) },
21 | selected = index == selectedIndex,
22 | onClick = { selectedIndex = index }
23 | )
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/CheckboxVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import TestTags
5 | import androidx.compose.ui.test.assertCountEquals
6 | import androidx.compose.ui.test.junit4.createComposeRule
7 | import androidx.compose.ui.test.onAllNodesWithTag
8 | import androidx.compose.ui.test.onNodeWithTag
9 | import androidx.compose.ui.test.performClick
10 | import org.junit.Rule
11 | import org.junit.Test
12 |
13 | class CheckboxVisibilityTest {
14 | @get:Rule
15 | val rule = createComposeRule()
16 |
17 | @Test
18 | fun testCheckboxIsPresent() {
19 | rule.apply {
20 | setContent {
21 | Material3Playground(false)
22 | }
23 | waitForIdle()
24 | onNodeWithTag(TestTags.NavRailButtons.checkBox).performClick()
25 | waitForIdle()
26 | onAllNodesWithTag(TestTags.CheckBox.regular, false).assertCountEquals(2)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Odin Asbjørnsen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Switch.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.size
6 | import androidx.compose.foundation.lazy.grid.LazyGridScope
7 | import androidx.compose.material3.Switch
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.Modifier
13 | import androidx.compose.ui.platform.testTag
14 | import androidx.compose.ui.unit.dp
15 |
16 | fun LazyGridScope.m3Switch() {
17 | item {
18 | Row {
19 | var checked by remember { mutableStateOf(false) }
20 |
21 | Switch(
22 | modifier = Modifier.testTag(TestTags.Switch.regular),
23 | checked = checked,
24 | onCheckedChange = { checked = !checked }
25 | )
26 | Spacer(modifier = Modifier.size(16.dp))
27 | Switch(
28 | modifier = Modifier.testTag(TestTags.Switch.regular),
29 | checked = !checked,
30 | onCheckedChange = { checked = !checked }
31 | )
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/.github/workflows/verification.yml:
--------------------------------------------------------------------------------
1 | name: Build & test
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**.md'
9 | pull_request:
10 | workflow_dispatch:
11 |
12 | jobs:
13 | unitTest:
14 | name: Run Unit Tests
15 | runs-on: macos-latest
16 |
17 | steps:
18 | - uses: actions/checkout@v5
19 | - name: Set up JDK 17
20 | uses: actions/setup-java@v5
21 | with:
22 | distribution: 'zulu'
23 | java-version: 17
24 | - name: Make gradlew executable
25 | run: chmod +x ./gradlew
26 | - name: Run unit tests
27 | run: ./gradlew test
28 | - name: Upload test report
29 | uses: actions/upload-artifact@v5
30 | with:
31 | name: test report
32 | path: build/reports/tests/test/index.html
33 | retention-days: 1
34 | assemble:
35 | name: Assemble Project
36 | runs-on: ubuntu-latest
37 |
38 | steps:
39 | - uses: actions/checkout@v5
40 | - name: Set up JDK 17
41 | uses: actions/setup-java@v5
42 | with:
43 | distribution: 'zulu'
44 | java-version: 17
45 | - name: Make gradlew executable
46 | run: chmod +x ./gradlew
47 | - name: Run unit tests
48 | run: ./gradlew assemble
49 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Checkbox.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import TestTags
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.lazy.grid.LazyGridScope
8 | import androidx.compose.material3.Checkbox
9 | import androidx.compose.material3.ExperimentalMaterial3Api
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.Modifier
15 | import androidx.compose.ui.platform.testTag
16 | import androidx.compose.ui.unit.dp
17 |
18 | @OptIn(ExperimentalMaterial3Api::class)
19 | fun LazyGridScope.m3Checkbox() {
20 | item {
21 | var checked by remember { mutableStateOf(true) }
22 | Row {
23 | Checkbox(
24 | modifier = Modifier.testTag(TestTags.CheckBox.regular),
25 | checked = checked,
26 | onCheckedChange = { checked = it }
27 | )
28 |
29 | Spacer(Modifier.size(8.dp))
30 |
31 | Checkbox(
32 | modifier = Modifier.testTag(TestTags.CheckBox.regular),
33 | checked = !checked,
34 | onCheckedChange = { checked = !it }
35 | )
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/TextFieldVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import androidx.compose.ui.test.assertIsDisplayed
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.performClick
8 | import org.junit.Rule
9 | import org.junit.Test
10 |
11 | class TextFieldVisibilityTest {
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testRegularTextFieldIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.NavRailButtons.textFields).performClick()
23 | waitForIdle()
24 | onNodeWithTag(TestTags.TextField.regular, false).assertIsDisplayed()
25 | }
26 | }
27 |
28 | @Test
29 | fun testErrorTextFieldIsPresent() {
30 | rule.apply {
31 | setContent {
32 | Material3Playground(false)
33 | }
34 | waitForIdle()
35 | onNodeWithTag(TestTags.NavRailButtons.textFields).performClick()
36 | waitForIdle()
37 | onNodeWithTag(TestTags.TextField.error, false).assertIsDisplayed()
38 | onNodeWithTag(TestTags.TextField.errorLabel, false).assertIsDisplayed()
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/CardVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import TestTags
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.performClick
8 | import org.junit.Rule
9 | import org.junit.Test
10 |
11 | class CardVisibilityTest {
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testOutlinedCardIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.NavRailButtons.cards).performClick()
23 | waitForIdle()
24 | onNodeWithTag(TestTags.Cards.outlined, false).assertExists()
25 | }
26 | }
27 |
28 |
29 | @Test
30 | fun testElevatedCardIsPresent() {
31 | rule.apply {
32 | setContent {
33 | Material3Playground(false)
34 | }
35 | waitForIdle()
36 | onNodeWithTag(TestTags.NavRailButtons.cards).performClick()
37 | waitForIdle()
38 | onNodeWithTag(TestTags.Cards.elevated, false).assertExists()
39 | }
40 | }
41 |
42 | @Test
43 | fun testFilledCardIsPresent() {
44 | rule.apply {
45 | setContent {
46 | Material3Playground(false)
47 | }
48 | waitForIdle()
49 | onNodeWithTag(TestTags.NavRailButtons.cards).performClick()
50 | waitForIdle()
51 | onNodeWithTag(TestTags.Cards.filled, false).assertExists()
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Chips.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.Spacer
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.layout.size
7 | import androidx.compose.foundation.lazy.grid.LazyGridScope
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.FilterChip
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableStateOf
13 | import androidx.compose.runtime.remember
14 | import androidx.compose.runtime.setValue
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.testTag
17 | import androidx.compose.ui.unit.dp
18 |
19 | @OptIn(ExperimentalMaterial3Api::class)
20 | fun LazyGridScope.m3Chips() {
21 | item {
22 | Row(Modifier.padding(8.dp)) {
23 | var selectedId by remember { mutableStateOf(1) }
24 |
25 | FilterChip(
26 | modifier = Modifier.testTag(TestTags.Chips.regular),
27 | label = { Text("Android") },
28 | onClick = { selectedId = 1 },
29 | selected = selectedId == 1
30 | )
31 |
32 | Spacer(modifier = Modifier.size(4.dp))
33 |
34 | FilterChip(
35 | modifier = Modifier.testTag(TestTags.Chips.regular),
36 | label = { Text("Material") },
37 | onClick = { selectedId = 2 },
38 | selected = selectedId == 2
39 | )
40 |
41 | Spacer(modifier = Modifier.size(4.dp))
42 |
43 | FilterChip(
44 | modifier = Modifier.testTag(TestTags.Chips.regular),
45 | label = { Text("Compose") },
46 | onClick = { selectedId = 3 },
47 | selected = selectedId == 3
48 | )
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Cards.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import TestTags
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.fillMaxSize
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.lazy.grid.LazyGridScope
9 | import androidx.compose.material3.Card
10 | import androidx.compose.material3.ElevatedCard
11 | import androidx.compose.material3.ExperimentalMaterial3Api
12 | import androidx.compose.material3.OutlinedCard
13 | import androidx.compose.material3.Text
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.platform.testTag
17 | import androidx.compose.ui.unit.dp
18 |
19 | @OptIn(ExperimentalMaterial3Api::class)
20 | fun LazyGridScope.m3Cards() {
21 | item {
22 | ElevatedCard(
23 | onClick = {},
24 | modifier = Modifier.size(width = 180.dp, height = 100.dp).padding(8.dp).testTag(TestTags.Cards.elevated)
25 | ) {
26 | Box(Modifier.fillMaxSize()) {
27 | Text("Elevated card", Modifier.align(Alignment.Center))
28 | }
29 | }
30 | }
31 |
32 | item {
33 | OutlinedCard(
34 | onClick = {},
35 | modifier = Modifier.size(width = 180.dp, height = 100.dp).padding(8.dp).testTag(TestTags.Cards.outlined)
36 | ) {
37 | Box(Modifier.fillMaxSize()) {
38 | Text("Outlined card", Modifier.align(Alignment.Center))
39 | }
40 | }
41 | }
42 |
43 |
44 | item {
45 | Card(
46 | onClick = {},
47 | modifier = Modifier.size(width = 180.dp, height = 100.dp).padding(8.dp).testTag(TestTags.Cards.filled)
48 | ) {
49 | Box(Modifier.fillMaxSize()) {
50 | Text("Filled card", Modifier.align(Alignment.Center))
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/NavigationBar.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.foundation.layout.width
5 | import androidx.compose.foundation.lazy.grid.LazyGridScope
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Favorite
8 | import androidx.compose.material3.Icon
9 | import androidx.compose.material3.NavigationBar
10 | import androidx.compose.material3.NavigationBarItem
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.mutableStateOf
14 | import androidx.compose.runtime.remember
15 | import androidx.compose.runtime.setValue
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.unit.dp
18 |
19 | fun LazyGridScope.m3NavigationBars() {
20 |
21 | item {
22 | val navbarItems = listOf("Home", "Payments", "Spending")
23 | var selectedItem by remember { mutableStateOf(0) }
24 |
25 | NavigationBar(Modifier.width(600.dp).padding(8.dp)) {
26 | navbarItems.forEachIndexed { index, title ->
27 | NavigationBarItem(
28 | icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
29 | label = { Text(title) },
30 | selected = selectedItem == index,
31 | onClick = { selectedItem = index }
32 | )
33 | }
34 | }
35 | }
36 |
37 | item {
38 | val navbarItems = listOf("Home", "Payments", "Spending")
39 | var selectedItem by remember { mutableStateOf(0) }
40 |
41 | NavigationBar(Modifier.width(600.dp).padding(8.dp)) {
42 | navbarItems.forEachIndexed { index, title ->
43 | NavigationBarItem(
44 | icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
45 | label = { Text(title) },
46 | selected = selectedItem == index,
47 | onClick = { selectedItem = index },
48 | alwaysShowLabel = false,
49 | )
50 | }
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/kotlin/TestTags.kt:
--------------------------------------------------------------------------------
1 | object TestTags {
2 | object FileSaverDialog {
3 | const val dialog = "FileSaverDialog"
4 | }
5 |
6 | object Components {
7 | const val navRail = "NavRail"
8 | }
9 |
10 | object Buttons {
11 | const val regular = "RegularButton"
12 | const val outlined = "OutlinedButton"
13 | const val text = "TextButton"
14 | const val floating = "FloatingActionButton"
15 | const val extendedFloating = "ExtendedFloatingActionButton"
16 | const val icon = "IconButton"
17 | const val filledIcon = "FilledIconButton"
18 | const val iconToggle = "IconToggleButton"
19 | }
20 |
21 | object Cards {
22 | const val elevated = "ElevatedCard"
23 | const val outlined = "OutlinedCard"
24 | const val filled = "FilledCard"
25 | }
26 |
27 | object CheckBox {
28 | const val regular = "Regular"
29 | }
30 |
31 | object Chips {
32 | const val regular = "Regular"
33 | }
34 |
35 | object Divider {
36 | const val regular = "Regular"
37 | }
38 |
39 | object ProgressBar {
40 | const val circular = "CircularProgressBar"
41 | }
42 |
43 | object Slider {
44 | const val regular = "Regular"
45 | }
46 |
47 | object Switch {
48 | const val regular = "Regular"
49 | }
50 |
51 | object TextField {
52 | const val regular = "RegularTextField"
53 | const val error = "ErrorTextField"
54 | const val errorLabel = "ErrorLabel"
55 | }
56 |
57 | object TopAppBar {
58 | const val small = "SmallTopAppBar"
59 | const val navigationIconSmall = "NavigationIconSmallTopAppBar"
60 | const val centerAligned = "CenterAlignedTopAppBar"
61 | const val navigationIconCenterAligned = "NavigationIconCenterAlignedTopAppBar"
62 | const val smallScroll = "SmallScrollTopAppBar"
63 | const val navigationIconsmallScroll = "NavigationIconSmallScrollTopAppBar"
64 | const val centerAlignedScroll = "CenterAlignedScrollTopAppBar"
65 | const val navigationIconCenterAlignedScroll = "NavigationIconCenterAlignedScrollTopAppBar"
66 | }
67 |
68 | object NavRailButtons {
69 | const val buttons = "Buttons"
70 | const val appBars = "AppBars"
71 | const val cards = "Cards"
72 | const val textFields = "TextFields"
73 | const val chips = "Chips"
74 | const val switch = "Switch"
75 | const val checkBox = "CheckBox"
76 | const val sliders = "Sliders"
77 | const val progressBar = "ProgressBar"
78 | const val dividers = "Dividers"
79 | }
80 | }
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/TextFields.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import TestTags
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.calculateStartPadding
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.lazy.grid.LazyGridScope
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.MaterialTheme
10 | import androidx.compose.material3.OutlinedTextField
11 | import androidx.compose.material3.Text
12 | import androidx.compose.material3.TextFieldDefaults
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.LocalLayoutDirection
18 | import androidx.compose.ui.platform.testTag
19 | import androidx.compose.ui.unit.dp
20 |
21 | fun LazyGridScope.m3TextFields() {
22 |
23 | item {
24 |
25 | val text = remember {
26 | mutableStateOf("")
27 | }
28 |
29 | OutlinedTextField(
30 | modifier = Modifier.padding(8.dp).testTag(TestTags.TextField.regular),
31 | value = text.value,
32 | onValueChange = {
33 | text.value = it
34 | },
35 | placeholder = {
36 | Text("Please type a text")
37 | }
38 | )
39 | }
40 |
41 | item {
42 |
43 | Column(
44 | modifier = Modifier.padding(8.dp),
45 | ) {
46 | val text = remember {
47 | mutableStateOf("")
48 | }
49 |
50 | OutlinedTextField(
51 | modifier = Modifier.testTag(TestTags.TextField.error),
52 | value = text.value,
53 | onValueChange = {
54 | text.value = it
55 | },
56 | placeholder = {
57 | Text("Please type a text")
58 | },
59 | isError = true,
60 | )
61 | TextFieldErrorMessage(
62 | modifier = Modifier.testTag(TestTags.TextField.errorLabel),
63 | message = "Something went wrong"
64 | )
65 | }
66 | }
67 | }
68 |
69 | @OptIn(ExperimentalMaterial3Api::class)
70 | @Composable
71 | private fun TextFieldErrorMessage(modifier: Modifier = Modifier, message: String) {
72 | // This will ensure that the error message will be aligned with the input text. Since we do
73 | // not have a built-in label supported for our text fields, using textFieldWithoutLabelPadding
74 | // is just fine here.
75 | val startPadding = TextFieldDefaults.textFieldWithoutLabelPadding()
76 | .calculateStartPadding(layoutDirection = LocalLayoutDirection.current)
77 |
78 | Text(
79 | modifier = modifier
80 | .padding(
81 | top = 4.dp,
82 | start = startPadding
83 | ),
84 | text = message,
85 | style = MaterialTheme.typography.labelSmall,
86 | color = MaterialTheme.colorScheme.error,
87 | )
88 | }
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/TopAppBarVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import TestTags
5 | import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
6 | import androidx.compose.ui.test.assertIsDisplayed
7 | import androidx.compose.ui.test.junit4.createComposeRule
8 | import androidx.compose.ui.test.onNodeWithTag
9 | import androidx.compose.ui.test.performClick
10 | import org.junit.Rule
11 | import org.junit.Test
12 |
13 | class TopAppBarVisibilityTest {
14 | @get:Rule
15 | val rule = createComposeRule()
16 |
17 | @Test
18 | fun testSmallAppBarIsPresent() {
19 | rule.apply {
20 | setContent {
21 | Material3Playground(false)
22 | }
23 | waitForIdle()
24 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick()
25 | waitForIdle()
26 | assertTopAppBarDisplayed(
27 | navigationIconTestTag = TestTags.TopAppBar.navigationIconSmall,
28 | appBarTestTag = TestTags.TopAppBar.small
29 | )
30 | }
31 | }
32 |
33 | @Test
34 | fun testCenterAlignedAppBarIsPresent() {
35 | rule.apply {
36 | setContent {
37 | Material3Playground(false)
38 | }
39 | waitForIdle()
40 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick()
41 | waitForIdle()
42 | assertTopAppBarDisplayed(
43 | navigationIconTestTag = TestTags.TopAppBar.navigationIconCenterAligned,
44 | appBarTestTag = TestTags.TopAppBar.centerAligned
45 | )
46 | }
47 | }
48 |
49 | @Test
50 | fun testSmallAppBarWithScrollStateIsPresent() {
51 | rule.apply {
52 | setContent {
53 | Material3Playground(false)
54 | }
55 | waitForIdle()
56 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick()
57 | waitForIdle()
58 | assertTopAppBarDisplayed(
59 | navigationIconTestTag = TestTags.TopAppBar.navigationIconsmallScroll,
60 | appBarTestTag = TestTags.TopAppBar.smallScroll
61 | )
62 | }
63 | }
64 |
65 | @Test
66 | fun testCenterAlignedAppBarWithScrollStateIsPresent() {
67 | rule.apply {
68 | setContent {
69 | Material3Playground(false)
70 | }
71 | waitForIdle()
72 | onNodeWithTag(TestTags.NavRailButtons.appBars).performClick()
73 | waitForIdle()
74 | assertTopAppBarDisplayed(
75 | navigationIconTestTag = TestTags.TopAppBar.navigationIconCenterAlignedScroll,
76 | appBarTestTag = TestTags.TopAppBar.centerAlignedScroll
77 | )
78 | }
79 | }
80 | }
81 |
82 | private fun SemanticsNodeInteractionsProvider.assertTopAppBarDisplayed(
83 | navigationIconTestTag: String,
84 | appBarTestTag: String
85 | ) {
86 | onNodeWithTag(navigationIconTestTag, false).assertIsDisplayed()
87 | onNodeWithTag(appBarTestTag, false).assertIsDisplayed()
88 | }
--------------------------------------------------------------------------------
/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 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 |
74 |
75 | @rem Execute Gradle
76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
77 |
78 | :end
79 | @rem End local scope for the variables with windows NT shell
80 | if %ERRORLEVEL% equ 0 goto mainEnd
81 |
82 | :fail
83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
84 | rem the _cmd.exe /c_ return code!
85 | set EXIT_CODE=%ERRORLEVEL%
86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
88 | exit /b %EXIT_CODE%
89 |
90 | :mainEnd
91 | if "%OS%"=="Windows_NT" endlocal
92 |
93 | :omega
94 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/TopAppBar.kt:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import TestTags
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.layout.width
6 | import androidx.compose.foundation.lazy.grid.LazyGridScope
7 | import androidx.compose.material3.CenterAlignedTopAppBar
8 | import androidx.compose.material3.ExperimentalMaterial3Api
9 | import androidx.compose.material3.Icon
10 | import androidx.compose.material3.IconButton
11 | import androidx.compose.material3.SmallTopAppBar
12 | import androidx.compose.material3.Text
13 | import androidx.compose.material3.TopAppBarDefaults
14 | import androidx.compose.material3.TopAppBarScrollState
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.platform.testTag
18 | import androidx.compose.ui.res.painterResource
19 | import androidx.compose.ui.unit.dp
20 |
21 | @OptIn(ExperimentalMaterial3Api::class)
22 | fun LazyGridScope.m3TopAppBars() {
23 | item {
24 | SmallTopAppBar(
25 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.small),
26 | navigationIcon = {
27 | NavigationIcon(
28 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconSmall)
29 | )
30 | },
31 | title = { Text("Small top app bar") },
32 | )
33 | }
34 | item {
35 | CenterAlignedTopAppBar(
36 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.centerAligned),
37 | navigationIcon = {
38 | NavigationIcon(
39 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconCenterAligned)
40 | )
41 | },
42 | title = { Text("Center aligned top app bar") },
43 | )
44 | }
45 | item {
46 |
47 | // Hacky scroll state to show how the toolbar will look if its scrolled state
48 | val scrollState = TopAppBarScrollState(-1f, 0f, -2f)
49 |
50 | SmallTopAppBar(
51 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.smallScroll),
52 | navigationIcon = {
53 | NavigationIcon(
54 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconsmallScroll)
55 | )
56 | },
57 | title = { Text("Small top app bar") },
58 | scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(scrollState)
59 | )
60 | }
61 |
62 | item {
63 |
64 | // Hacky scroll state to show how the toolbar will look if its scrolled state
65 | val scrollState = TopAppBarScrollState(-1f, 0f, -2f)
66 |
67 | CenterAlignedTopAppBar(
68 | modifier = Modifier.width(600.dp).padding(8.dp).testTag(TestTags.TopAppBar.centerAlignedScroll),
69 | navigationIcon = {
70 | NavigationIcon(
71 | modifier = Modifier.testTag(TestTags.TopAppBar.navigationIconCenterAlignedScroll)
72 | )
73 | },
74 | title = { Text("Center aligned top app bar") },
75 | scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(scrollState)
76 | )
77 | }
78 | }
79 |
80 | @Composable
81 | private fun NavigationIcon(modifier: Modifier = Modifier) {
82 | IconButton(
83 | modifier = modifier,
84 | onClick = {}
85 | ) {
86 | Icon(
87 | painter = painterResource("ic_arrow_back.xml"),
88 | contentDescription = null
89 | )
90 | }
91 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/oas004/Material3Playground/actions/workflows/verification.yml)
2 |
3 | # Material3Playground
4 | Desktop Playground to check how changing the color system can affect the different components in Material 3. Mainly used to learn and to get an overview of the Material 3 design components.
5 |
6 | ## Setup 🔧
7 |
8 | You want to check out the code, you can clone the project with
9 | ```
10 | git clone git@github.com:oas004/Material3Playground.git
11 | ```
12 | Make sure that you are running a JDK version higher than 15. You can run the application either by running
13 | ```
14 | ./gradlew run
15 | ```
16 | or pressing the play gutter icon button in the `Main.kt` file.
17 |
18 | ## Gameplay 🔥
19 |
20 | ### UI Mode
21 |
22 | You can toggle the UI Mode in the window menu bar at the top left. This will update all the colors to the dark/light theme.
23 |
24 | | Light Mode | Dark Mode |
25 | |-----------------------------------------------------------|:-------------------------------------------------------:|
26 | |  |  |
27 |
28 | You can interact with the components as you would on a mobile application. However, note that this is build with compose for desktop and there will be some differences on mobile.
29 |
30 | ### Interaction 🌻
31 | You can interact with the colors on the right side. If you press a color, you can change the hexcode for this color. Pressing the "OK" button will update the components with the new color.
32 |
33 | 
34 |
35 | For instance changing the primary color to pink will update the main display with pink as primary color like this:
36 |
37 |
38 | 
39 |
40 | The second time you change this color, the pink color will be remembered as a recently used color.
41 |
42 |
43 | 
44 |
45 | ## Upcoming features 👷
46 | - We are still missing some key components from Material 3. We are planing on adding them. If there are someone that you miss, please file an issue or even better; make a PR on it.
47 |
48 | ## Contribution ❄️ ⚡
49 | Pull requests are welcome! We'd love help improving this library. Feel free to browse through open issues to look for things that need work. If you have a feature request or bug, please open a new issue so we can track it.
50 |
51 | ## License
52 | MIT License
53 |
54 | Copyright (c) 2025 Odin Asbjørnsen
55 |
56 | Permission is hereby granted, free of charge, to any person obtaining a copy
57 | of this software and associated documentation files (the "Software"), to deal
58 | in the Software without restriction, including without limitation the rights
59 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
60 | copies of the Software, and to permit persons to whom the Software is
61 | furnished to do so, subject to the following conditions:
62 |
63 | The above copyright notice and this permission notice shall be included in all
64 | copies or substantial portions of the Software.
65 |
66 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
67 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
68 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
69 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
70 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
71 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
72 | SOFTWARE.
73 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/ButtonVisibilityTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import TestTags
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import androidx.compose.ui.test.performClick
8 | import org.junit.Rule
9 | import org.junit.Test
10 |
11 | class ButtonVisibilityTest {
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testIconButtonIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
23 | waitForIdle()
24 | onNodeWithTag(TestTags.Buttons.icon, false).assertExists()
25 | }
26 | }
27 |
28 |
29 | @Test
30 | fun testRegularButtonIsPresent() {
31 | rule.apply {
32 | setContent {
33 | Material3Playground(false)
34 | }
35 | waitForIdle()
36 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
37 | waitForIdle()
38 | onNodeWithTag(TestTags.Buttons.regular, false).assertExists()
39 | }
40 | }
41 |
42 |
43 | @Test
44 | fun testOutlinedButtonIsPresent() {
45 | rule.apply {
46 | setContent {
47 | Material3Playground(false)
48 | }
49 | waitForIdle()
50 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
51 | waitForIdle()
52 | onNodeWithTag(TestTags.Buttons.outlined, false).assertExists()
53 | }
54 | }
55 |
56 |
57 | @Test
58 | fun testTextButtonIsPresent() {
59 | rule.apply {
60 | setContent {
61 | Material3Playground(false)
62 | }
63 | waitForIdle()
64 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
65 | waitForIdle()
66 | onNodeWithTag(TestTags.Buttons.text, false).assertExists()
67 | }
68 | }
69 |
70 | @Test
71 | fun testFloatingButtonIsPresent() {
72 | rule.apply {
73 | setContent {
74 | Material3Playground(false)
75 | }
76 | waitForIdle()
77 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
78 | waitForIdle()
79 | onNodeWithTag(TestTags.Buttons.floating, false).assertExists()
80 | }
81 | }
82 |
83 |
84 | @Test
85 | fun testExtendedFloatingButtonIsPresent() {
86 | rule.apply {
87 | setContent {
88 | Material3Playground(false)
89 | }
90 | waitForIdle()
91 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
92 | waitForIdle()
93 | onNodeWithTag(TestTags.Buttons.extendedFloating, false).assertExists()
94 | }
95 | }
96 |
97 |
98 | @Test
99 | fun testFilledIconButtonIsPresent() {
100 | rule.apply {
101 | setContent {
102 | Material3Playground(false)
103 | }
104 | waitForIdle()
105 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
106 | waitForIdle()
107 | onNodeWithTag(TestTags.Buttons.filledIcon, false).assertExists()
108 | }
109 | }
110 |
111 |
112 | @Test
113 | fun testIconToggleButtonIsPresent() {
114 | rule.apply {
115 | setContent {
116 | Material3Playground(false)
117 | }
118 | waitForIdle()
119 | onNodeWithTag(TestTags.NavRailButtons.buttons).performClick()
120 | waitForIdle()
121 | onNodeWithTag(TestTags.Buttons.iconToggle, false).assertExists()
122 | }
123 | }
124 | }
125 |
--------------------------------------------------------------------------------
/src/test/kotlin/m3components/components/NavRailButtonTest.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import Material3Playground
4 | import TestTags
5 | import androidx.compose.ui.test.junit4.createComposeRule
6 | import androidx.compose.ui.test.onNodeWithTag
7 | import org.junit.Rule
8 | import org.junit.Test
9 |
10 | class NavRailButtonTest {
11 |
12 | @get:Rule
13 | val rule = createComposeRule()
14 |
15 | @Test
16 | fun testNavRailIsPresent() {
17 | rule.apply {
18 | setContent {
19 | Material3Playground(false)
20 | }
21 | waitForIdle()
22 | onNodeWithTag(TestTags.Components.navRail, false).assertExists()
23 | }
24 | }
25 |
26 |
27 | @Test
28 | fun testNavRailButtonsIsPresent() {
29 | rule.apply {
30 | setContent {
31 | Material3Playground(false)
32 | }
33 | waitForIdle()
34 | onNodeWithTag(TestTags.NavRailButtons.buttons, false).assertExists()
35 | }
36 | }
37 |
38 | @Test
39 | fun testNavRailAppBarIsPresent() {
40 | rule.apply {
41 | setContent {
42 | Material3Playground(false)
43 | }
44 | waitForIdle()
45 | onNodeWithTag(TestTags.NavRailButtons.appBars, false).assertExists()
46 | }
47 | }
48 |
49 | @Test
50 | fun testNavRailCardsIsPresent() {
51 | rule.apply {
52 | setContent {
53 | Material3Playground(false)
54 | }
55 | waitForIdle()
56 | onNodeWithTag(TestTags.NavRailButtons.cards, false).assertExists()
57 | }
58 | }
59 |
60 | @Test
61 | fun testNavRailTextFieldIsPresent() {
62 | rule.apply {
63 | setContent {
64 | Material3Playground(false)
65 | }
66 | waitForIdle()
67 | onNodeWithTag(TestTags.NavRailButtons.textFields, false).assertExists()
68 | }
69 | }
70 |
71 | @Test
72 | fun testNavRailChipsIsPresent() {
73 | rule.apply {
74 | setContent {
75 | Material3Playground(false)
76 | }
77 | waitForIdle()
78 | onNodeWithTag(TestTags.NavRailButtons.chips, false).assertExists()
79 | }
80 | }
81 |
82 | @Test
83 | fun testNavRailSwitchIsPresent() {
84 | rule.apply {
85 | setContent {
86 | Material3Playground(false)
87 | }
88 | waitForIdle()
89 | onNodeWithTag(TestTags.NavRailButtons.switch, false).assertExists()
90 | }
91 | }
92 |
93 | @Test
94 | fun testNavRailCheckBoxIsPresent() {
95 | rule.apply {
96 | setContent {
97 | Material3Playground(false)
98 | }
99 | waitForIdle()
100 | onNodeWithTag(TestTags.NavRailButtons.checkBox, false).assertExists()
101 | }
102 | }
103 |
104 | @Test
105 | fun testNavRailSlidersIsPresent() {
106 | rule.apply {
107 | setContent {
108 | Material3Playground(false)
109 | }
110 | waitForIdle()
111 | onNodeWithTag(TestTags.NavRailButtons.sliders, false).assertExists()
112 | }
113 | }
114 |
115 | @Test
116 | fun testNavRailProgressBarIsPresent() {
117 | rule.apply {
118 | setContent {
119 | Material3Playground(false)
120 | }
121 | waitForIdle()
122 | onNodeWithTag(TestTags.NavRailButtons.progressBar, false).assertExists()
123 | }
124 | }
125 | @Test
126 | fun testNavRailDividerIsPresent() {
127 | rule.apply {
128 | setContent {
129 | Material3Playground(false)
130 | }
131 | waitForIdle()
132 | onNodeWithTag(TestTags.NavRailButtons.dividers, false).assertExists()
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/main/kotlin/m3components/components/Buttons.kt:
--------------------------------------------------------------------------------
1 | package m3components.components
2 |
3 | import TestTags
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.lazy.grid.LazyGridScope
7 | import androidx.compose.material.icons.Icons
8 | import androidx.compose.material.icons.filled.CardTravel
9 | import androidx.compose.material.icons.filled.Lock
10 | import androidx.compose.material.icons.outlined.Lock
11 | import androidx.compose.material3.Button
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.ExtendedFloatingActionButton
14 | import androidx.compose.material3.FilledIconButton
15 | import androidx.compose.material3.FloatingActionButton
16 | import androidx.compose.material3.Icon
17 | import androidx.compose.material3.IconButton
18 | import androidx.compose.material3.IconToggleButton
19 | import androidx.compose.material3.OutlinedButton
20 | import androidx.compose.material3.Text
21 | import androidx.compose.material3.TextButton
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.platform.testTag
29 | import androidx.compose.ui.unit.dp
30 |
31 |
32 | @OptIn(ExperimentalMaterial3Api::class)
33 | fun LazyGridScope.m3Buttons() {
34 |
35 | item {
36 | Button(
37 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.regular),
38 | onClick = {},
39 | enabled = true
40 | ) {
41 | Text("Primary button")
42 | }
43 | }
44 |
45 | item {
46 | OutlinedButton(
47 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.outlined),
48 | onClick = {},
49 | enabled = true
50 | ) {
51 | Text("Secondary button")
52 | }
53 | }
54 |
55 | item {
56 | TextButton(
57 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.text),
58 | onClick = {},
59 | enabled = true
60 | ) {
61 | Text("Text button")
62 | }
63 | }
64 |
65 | item {
66 | Box {
67 | FloatingActionButton(
68 | modifier = Modifier.align(Alignment.Center).padding(8.dp).testTag(TestTags.Buttons.floating),
69 | onClick = {}) {
70 | Text("FAB")
71 | }
72 | }
73 | }
74 |
75 | item {
76 | Box {
77 | ExtendedFloatingActionButton(
78 | modifier = Modifier.testTag(TestTags.Buttons.extendedFloating)
79 | .align(Alignment.Center)
80 | .padding(8.dp),
81 | onClick = {}
82 | ) {
83 | Text("Extended FAB")
84 | }
85 | }
86 | }
87 |
88 | item {
89 | IconButton(
90 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.icon),
91 | onClick = {}, enabled = true
92 | ) {
93 | Icon(imageVector = Icons.Default.CardTravel, contentDescription = null)
94 | }
95 | }
96 |
97 | item {
98 | var checked by remember { mutableStateOf(false) }
99 | IconToggleButton(
100 | modifier = Modifier.padding(8.dp).testTag(TestTags.Buttons.iconToggle),
101 | checked = checked, onCheckedChange = { checked = it }
102 | ) {
103 | if (checked) {
104 | Icon(Icons.Filled.Lock, contentDescription = "Localized description")
105 | } else {
106 | Icon(Icons.Outlined.Lock, contentDescription = "Localized description")
107 | }
108 | }
109 | }
110 |
111 | item {
112 | Box {
113 | FilledIconButton(
114 | modifier = Modifier.align(Alignment.Center).padding(8.dp).testTag(TestTags.Buttons.filledIcon),
115 | onClick = {}, enabled = true
116 | ) {
117 | Icon(imageVector = Icons.Default.CardTravel, contentDescription = null)
118 | }
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/main/kotlin/FileUtils.kt:
--------------------------------------------------------------------------------
1 | import ColorFileType.Kotlin
2 | import ColorFileType.XML
3 | import androidx.compose.material3.ColorScheme
4 | import androidx.compose.runtime.Composable
5 | import androidx.compose.ui.graphics.Color
6 | import java.io.File
7 | import java.io.FileNotFoundException
8 | import java.nio.file.FileAlreadyExistsException
9 | import java.util.Locale
10 | import javax.swing.JFileChooser
11 | import javax.swing.filechooser.FileSystemView
12 | import kotlin.io.path.Path
13 | import kotlin.io.path.createFile
14 |
15 | @Composable
16 | internal fun FileChooserDialog(
17 | title: String,
18 | onResult: (result: File) -> Unit,
19 | onCancel: () -> Unit
20 | ) {
21 | val fileChooser = JFileChooser(FileSystemView.getFileSystemView()).apply {
22 | currentDirectory = File(System.getProperty("user.dir"))
23 | dialogTitle = title
24 | fileSelectionMode = JFileChooser.DIRECTORIES_ONLY
25 | isAcceptAllFileFilterUsed = true
26 | selectedFile = null
27 | currentDirectory = null
28 | }
29 | if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
30 | val file = fileChooser.selectedFile
31 | onResult(file)
32 | } else {
33 | onCancel.invoke()
34 | }
35 | }
36 |
37 | internal fun saveColorsToFile(
38 | currentColorPalette: ColorScheme,
39 | file: File,
40 | fileType: ColorFileType,
41 | onResult: (success: Boolean, message: String) -> Unit
42 | ) {
43 | when (fileType) {
44 | Kotlin -> saveColorsToKotlinFile(file = file, currentColorPalette = currentColorPalette, onResult = onResult)
45 | XML -> saveColorsToXMLFile(file = file, currentColorPalette = currentColorPalette, onResult = onResult)
46 | }
47 | }
48 |
49 | private fun saveColorsToXMLFile(
50 | file: File,
51 | currentColorPalette: ColorScheme,
52 | onResult: (success: Boolean, message: String) -> Unit
53 | ) {
54 | tryWriteToFile(
55 | onResult = onResult
56 | ) {
57 | val path = file.path
58 | val outputFile = Path(path, "colors.xml").createFile().toFile()
59 | outputFile.apply {
60 |
61 | getColorList(colorPalette = currentColorPalette).forEach { color ->
62 | appendText(getColorsXMLProperty(colorNameLowerCase = color.key, color = color.value))
63 | }
64 |
65 | }
66 | onResult(true, "Saving colors worked")
67 | }
68 | }
69 |
70 |
71 | private fun saveColorsToKotlinFile(
72 | file: File,
73 | currentColorPalette: ColorScheme,
74 | onResult: (success: Boolean, message: String) -> Unit
75 | ) {
76 | tryWriteToFile(
77 | onResult = onResult
78 | ) {
79 | val path = file.path
80 | val outputFile = Path(path, "colors.kt").createFile().toFile()
81 | outputFile.apply {
82 |
83 | getColorList(colorPalette = currentColorPalette).forEach { color ->
84 | appendText(getColorKotlinProperty(colorNameLowerCase = color.key, color = color.value))
85 | }
86 |
87 | }
88 | onResult(true, "Saving colors worked")
89 | }
90 | }
91 | private fun tryWriteToFile(
92 | onResult: (success: Boolean, message: String) -> Unit,
93 | block: () -> Unit,
94 | ) {
95 | try {
96 | block.invoke()
97 | } catch (fileNotFound: FileNotFoundException) {
98 | onResult(false, "Could not save to this folder as it does not exist.")
99 | } catch (securityException: SecurityException) {
100 | onResult(false, "Can't save this file due to security reasons.")
101 | } catch (fileAlreadyExists: FileAlreadyExistsException) {
102 | onResult(false, "A file with this name already exists here.")
103 | }
104 | }
105 |
106 | internal fun getColorsXMLProperty(colorNameLowerCase: String, color: Color): String {
107 | return "${color.hexCode} \n"
108 | }
109 |
110 |
111 | internal fun getColorKotlinProperty(colorNameLowerCase: String, color: Color): String {
112 | val capitalizedColorName = colorNameLowerCase.replaceFirstChar {
113 | if (it.isLowerCase()) {
114 | it.titlecase(Locale.getDefault())
115 | } else {
116 | it.toString()
117 | }
118 | }
119 | return "val $capitalizedColorName: Color = Color(0x${color.hexCode.takeLast(8)}) \n"
120 | }
121 |
122 | private fun getColorList(colorPalette: ColorScheme): Map {
123 | return mapOf(
124 | "primary" to colorPalette.primary,
125 | "onPrimary" to colorPalette.onPrimary,
126 | "primaryContainer" to colorPalette.primaryContainer,
127 | "onPrimaryContainer" to colorPalette.onPrimaryContainer,
128 | "inversePrimary" to colorPalette.inversePrimary,
129 | "secondary" to colorPalette.secondary,
130 | "onSecondary" to colorPalette.onSecondary,
131 | "secondaryContainer" to colorPalette.secondaryContainer,
132 | "onSecondaryContainer" to colorPalette.onSecondaryContainer,
133 | "tertiary" to colorPalette.tertiary,
134 | "onTertiary" to colorPalette.onTertiary,
135 | "tertiaryContainer" to colorPalette.tertiaryContainer,
136 | "onTertiaryContainer" to colorPalette.onTertiaryContainer,
137 | "background" to colorPalette.background,
138 | "onBackground" to colorPalette.onBackground,
139 | "surface" to colorPalette.surface,
140 | "onSurface" to colorPalette.onSurface,
141 | "surfaceVariant" to colorPalette.surfaceVariant,
142 | "onSurfaceVariant" to colorPalette.onSurfaceVariant,
143 | "surfaceTint" to colorPalette.surfaceTint,
144 | "inverseSurface" to colorPalette.inverseSurface,
145 | "inverseOnSurface" to colorPalette.inverseOnSurface,
146 | "error" to colorPalette.error,
147 | "onError" to colorPalette.onError,
148 | "errorContainer" to colorPalette.errorContainer,
149 | "onErrorContainer" to colorPalette.onErrorContainer,
150 | "outline" to colorPalette.outline,
151 | )
152 | }
153 |
154 | internal enum class ColorFileType {
155 | Kotlin, XML
156 | }
157 |
--------------------------------------------------------------------------------
/src/main/kotlin/ThemeColorPicker.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.foundation.background
2 | import androidx.compose.foundation.clickable
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.foundation.shape.CircleShape
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.foundation.verticalScroll
17 | import androidx.compose.material3.Button
18 | import androidx.compose.material3.MaterialTheme
19 | import androidx.compose.material3.Text
20 | import androidx.compose.material3.TextField
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.draw.clip
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.graphics.toArgb
31 | import androidx.compose.ui.text.TextStyle
32 | import androidx.compose.ui.unit.dp
33 | import androidx.compose.ui.window.Dialog
34 | import java.awt.Dimension
35 |
36 | @Composable
37 | internal fun ThemeColorPicker(
38 | modifier: Modifier = Modifier,
39 | colorName: String,
40 | currentColor: Color,
41 | onColorPicked: (colorName: String, pickedColor: Color) -> Unit,
42 | recentlyUsedColors: List = listOf(),
43 | ) {
44 | var isDialogVisible by remember { mutableStateOf(false) }
45 |
46 | Column(
47 | modifier = modifier then Modifier.clickable { isDialogVisible = true }
48 | .padding(8.dp),
49 | horizontalAlignment = Alignment.CenterHorizontally
50 | ) {
51 | Box(
52 | modifier = Modifier.size(40.dp)
53 | .background(color = currentColor)
54 | .clip(shape = RoundedCornerShape(8.dp))
55 | )
56 | M3OnSurfaceText(
57 | text = colorName,
58 | style = MaterialTheme.typography.titleMedium
59 | )
60 | }
61 |
62 | Dialog(
63 | visible = isDialogVisible,
64 | onCloseRequest = { isDialogVisible = false },
65 | title = colorName,
66 | ) {
67 | window.size = Dimension(400, 400)
68 |
69 | ColorPickerDialogContent(
70 | currentColor,
71 | onOkClicked = { pickedColor ->
72 | // Update the color palette and hide the dialog
73 | onColorPicked(colorName, pickedColor)
74 | isDialogVisible = false
75 | },
76 | recentlyUsedColors = recentlyUsedColors
77 | )
78 | }
79 | }
80 |
81 | @Composable
82 | private fun ColorPickerDialogContent(
83 | currentColor: Color,
84 | onOkClicked: (pickedColor: Color) -> Unit,
85 | modifier: Modifier = Modifier,
86 | recentlyUsedColors: List = listOf(),
87 | ) {
88 | var pickedColor by remember { mutableStateOf(currentColor) }
89 | val scrollState = rememberScrollState()
90 |
91 | Column(
92 | modifier = modifier.fillMaxSize()
93 | .padding(16.dp)
94 | .verticalScroll(scrollState),
95 | horizontalAlignment = Alignment.CenterHorizontally
96 | ) {
97 | Box(
98 | modifier = Modifier.size(80.dp)
99 | .background(color = pickedColor)
100 | .clip(shape = RoundedCornerShape(8.dp))
101 | )
102 |
103 | Spacer(Modifier.size(8.dp))
104 |
105 | val currentColorHexString = currentColor.value.toString(16).substring(0, 8)
106 | val text = remember { mutableStateOf("#$currentColorHexString") }
107 |
108 | TextField(
109 | modifier = Modifier.width(120.dp),
110 | value = text.value,
111 | onValueChange = {
112 | text.value = it
113 |
114 | val newColor = getColorFromText(it)
115 | if (newColor != null) pickedColor = newColor
116 | },
117 | placeholder = { Text("Please enter a color hex") }
118 | )
119 |
120 | Spacer(modifier = Modifier.size(16.dp))
121 |
122 | if (recentlyUsedColors.isNotEmpty()) {
123 | M3OnSurfaceText(
124 | "Recently used colors",
125 | MaterialTheme.typography.labelSmall
126 | )
127 |
128 | RecentlyUsedColors(
129 | modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
130 | colors = recentlyUsedColors,
131 | onColorClick = {
132 | text.value = it.hexCode
133 | val newColor = getColorFromText(it.hexCode)
134 | if (newColor != null) pickedColor = newColor
135 | }
136 | )
137 | }
138 |
139 | Spacer(modifier = Modifier.size(32.dp))
140 |
141 | Button(
142 | onClick = { onOkClicked(pickedColor) }
143 | ) {
144 | Text("OK")
145 | }
146 | }
147 | }
148 |
149 | @Composable
150 | fun M3OnSurfaceText(
151 | text: String,
152 | style: TextStyle,
153 | ) {
154 | Text(
155 | color = MaterialTheme.colorScheme.onSurface,
156 | text = text,
157 | style = style,
158 | )
159 | }
160 |
161 | @Composable
162 | private fun RecentlyUsedColors(
163 | colors: List,
164 | onColorClick: (color: Color) -> Unit,
165 | modifier: Modifier = Modifier,
166 | ) {
167 | Row(
168 | modifier = modifier,
169 | horizontalArrangement = Arrangement.Center
170 | ) {
171 | colors.forEach {
172 | Box(
173 | modifier = Modifier
174 | .size(30.dp)
175 | .padding(8.dp)
176 | .clickable { onColorClick(it) }
177 | .clip(CircleShape)
178 | .background(it)
179 | )
180 | }
181 | }
182 | }
183 |
184 | private fun getColorFromText(hexCode: String): Color? {
185 | return try {
186 | val hexString = hexCode.removePrefix("#").lowercase()
187 | val updatedHexString = if (hexString.length == 6) "ff$hexString" else hexString
188 |
189 | Color(updatedHexString.toLong(16))
190 | } catch (e: Exception) {
191 | null
192 | }
193 | }
194 |
195 | internal val Color.hexCode: String
196 | get() = String.format("#%08X", toArgb())
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015 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 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 |
118 |
119 | # Determine the Java command to use to start the JVM.
120 | if [ -n "$JAVA_HOME" ] ; then
121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
122 | # IBM's JDK on AIX uses strange locations for the executables
123 | JAVACMD=$JAVA_HOME/jre/sh/java
124 | else
125 | JAVACMD=$JAVA_HOME/bin/java
126 | fi
127 | if [ ! -x "$JAVACMD" ] ; then
128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
129 |
130 | Please set the JAVA_HOME variable in your environment to match the
131 | location of your Java installation."
132 | fi
133 | else
134 | JAVACMD=java
135 | if ! command -v java >/dev/null 2>&1
136 | then
137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
138 |
139 | Please set the JAVA_HOME variable in your environment to match the
140 | location of your Java installation."
141 | fi
142 | fi
143 |
144 | # Increase the maximum file descriptors if we can.
145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
146 | case $MAX_FD in #(
147 | max*)
148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
149 | # shellcheck disable=SC2039,SC3045
150 | MAX_FD=$( ulimit -H -n ) ||
151 | warn "Could not query maximum file descriptor limit"
152 | esac
153 | case $MAX_FD in #(
154 | '' | soft) :;; #(
155 | *)
156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
157 | # shellcheck disable=SC2039,SC3045
158 | ulimit -n "$MAX_FD" ||
159 | warn "Could not set maximum file descriptor limit to $MAX_FD"
160 | esac
161 | fi
162 |
163 | # Collect all arguments for the java command, stacking in reverse order:
164 | # * args from the command line
165 | # * the main class name
166 | # * -classpath
167 | # * -D...appname settings
168 | # * --module-path (only if needed)
169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
170 |
171 | # For Cygwin or MSYS, switch paths to Windows format before running java
172 | if "$cygwin" || "$msys" ; then
173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
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, 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 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
214 | "$@"
215 |
216 | # Stop when "xargs" is not available.
217 | if ! command -v xargs >/dev/null 2>&1
218 | then
219 | die "xargs is not available"
220 | fi
221 |
222 | # Use "xargs" to parse quoted args.
223 | #
224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
225 | #
226 | # In Bash we could simply go:
227 | #
228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
229 | # set -- "${ARGS[@]}" "$@"
230 | #
231 | # but POSIX shell has neither arrays nor command substitution, so instead we
232 | # post-process each arg (as a line of input to sed) to backslash-escape any
233 | # character that might be a shell metacharacter, then use eval to reverse
234 | # that process (while maintaining the separation between arguments), and wrap
235 | # the whole thing up as a single "set" statement.
236 | #
237 | # This will of course break if any of these variables contains a newline or
238 | # an unmatched quote.
239 | #
240 |
241 | eval "set -- $(
242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
243 | xargs -n1 |
244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
245 | tr '\n' ' '
246 | )" '"$@"'
247 |
248 | exec "$JAVACMD" "$@"
249 |
--------------------------------------------------------------------------------
/src/main/kotlin/Main.kt:
--------------------------------------------------------------------------------
1 | import ColorFileType.Kotlin
2 | import ColorFileType.XML
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.defaultMinSize
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.shape.RoundedCornerShape
13 | import androidx.compose.material.AlertDialog
14 | import androidx.compose.material.Button
15 | import androidx.compose.material.ExperimentalMaterialApi
16 | import androidx.compose.material.Text
17 | import androidx.compose.material3.ColorScheme
18 | import androidx.compose.material3.MaterialTheme
19 | import androidx.compose.material3.darkColorScheme
20 | import androidx.compose.material3.lightColorScheme
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.getValue
23 | import androidx.compose.runtime.mutableStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.setValue
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.ExperimentalComposeUiApi
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.input.key.Key
31 | import androidx.compose.ui.input.key.KeyShortcut
32 | import androidx.compose.ui.platform.testTag
33 | import androidx.compose.ui.res.painterResource
34 | import androidx.compose.ui.text.style.TextAlign
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.window.MenuBar
37 | import androidx.compose.ui.window.Tray
38 | import androidx.compose.ui.window.Window
39 | import androidx.compose.ui.window.WindowPosition
40 | import androidx.compose.ui.window.application
41 | import androidx.compose.ui.window.rememberWindowState
42 | import java.awt.Dimension
43 |
44 | private const val windowMinWidth = 500
45 | private const val windowMinHeight = 600
46 |
47 | @OptIn(ExperimentalComposeUiApi::class)
48 | fun main() = application {
49 |
50 | Tray(
51 | icon = painterResource("icon.png"),
52 | menu = {
53 | Item("Quit App", onClick = ::exitApplication)
54 | }
55 | )
56 |
57 | Window(
58 | onCloseRequest = ::exitApplication,
59 | title = "Material 3 Playground",
60 | state = rememberWindowState(
61 | width = 1200.dp, height = 800.dp, position = WindowPosition(
62 | Alignment.Center
63 | )
64 | ),
65 | resizable = true,
66 | icon = painterResource("icon.png")
67 | ) {
68 | var darkmode by remember { mutableStateOf(false) }
69 |
70 | MenuBar {
71 | Menu(text = "UI Mode", mnemonic = 'U') {
72 | val darkModeMenuText = if (darkmode) {
73 | "Toggle Dark Mode Off"
74 | } else {
75 | "Toggle Dark Mode On"
76 | }
77 | Item(
78 | text = darkModeMenuText,
79 | shortcut = KeyShortcut(key = Key.D, meta = true),
80 | onClick = {
81 | darkmode = !darkmode
82 | },
83 | )
84 | }
85 | }
86 |
87 | window.minimumSize = Dimension(windowMinWidth, windowMinHeight)
88 |
89 |
90 | Material3Playground(darkmode = darkmode)
91 | }
92 | }
93 |
94 | @OptIn(ExperimentalMaterialApi::class)
95 | @Composable
96 | internal fun Material3Playground(darkmode: Boolean) {
97 | var lightColorScheme by remember { mutableStateOf(lightColorScheme()) }
98 | var darkColorScheme by remember { mutableStateOf(darkColorScheme()) }
99 |
100 | MaterialTheme(
101 | colorScheme = if (darkmode) darkColorScheme else lightColorScheme
102 | ) {
103 | Row(
104 | modifier = Modifier
105 | .fillMaxSize()
106 | .background(color = MaterialTheme.colorScheme.surface)
107 | ) {
108 |
109 | val currentColorPalette = MaterialTheme.colorScheme
110 | var shouldShowFileChooser by remember { mutableStateOf(false) }
111 | var fileSaverDialog: String? by remember { mutableStateOf(null) }
112 | var fileTypeDialog by remember { mutableStateOf(false) }
113 | var savableFileType by remember { mutableStateOf(Kotlin) }
114 |
115 | ComponentScope(
116 | onColorPicked = { colorName, color ->
117 | val updatedColorPalette = updateColorPalette(
118 | currentColorPalette = currentColorPalette,
119 | colorName = colorName,
120 | color = color,
121 | )
122 |
123 | if (darkmode) {
124 | darkColorScheme = updatedColorPalette
125 | } else {
126 | lightColorScheme = updatedColorPalette
127 | }
128 | },
129 | onPrintColors = {
130 | fileTypeDialog = true
131 | }
132 | )
133 |
134 | if (shouldShowFileChooser) {
135 | FileChooserDialog(
136 | title = "Select directory to save file",
137 | onResult = {
138 | saveColorsToFile(
139 | currentColorPalette, file = it, onResult = { success, message ->
140 | // Implement success / failure screens for this when VM is implemented,
141 | // and we can easier manage states.
142 | fileSaverDialog = message
143 | },
144 | fileType = savableFileType
145 | )
146 | shouldShowFileChooser = false
147 | },
148 | onCancel = {
149 | shouldShowFileChooser = false
150 | }
151 | )
152 | }
153 |
154 | if (fileSaverDialog != null) {
155 | val title = when (savableFileType) {
156 | Kotlin -> "Saving Colors to Colors.kt"
157 | XML -> "Saving Colors to Colors.xml"
158 | }
159 | AlertDialog(
160 | modifier = Modifier.defaultMinSize(minWidth = 400.dp, minHeight = 150.dp)
161 | .background(color = MaterialTheme.colorScheme.surface).testTag(TestTags.FileSaverDialog.dialog),
162 | shape = RoundedCornerShape(8.dp),
163 | title = {
164 | Text(
165 | modifier = Modifier.fillMaxWidth().padding(12.dp),
166 | text = title,
167 | textAlign = TextAlign.Center,
168 | style = MaterialTheme.typography.headlineMedium
169 | )
170 | },
171 | buttons = {
172 | Button(
173 | modifier = Modifier.align(Alignment.CenterVertically),
174 | onClick = {
175 | fileSaverDialog = null
176 | }
177 | ) {
178 | Text(
179 | text = "OK",
180 | textAlign = TextAlign.Center,
181 | style = MaterialTheme.typography.labelMedium,
182 | color = MaterialTheme.colorScheme.onPrimary
183 | )
184 | }
185 | },
186 | onDismissRequest = {
187 | fileSaverDialog = null
188 | },
189 | text = {
190 | Text(
191 | modifier = Modifier.fillMaxWidth().padding(12.dp),
192 | text = fileSaverDialog ?: "",
193 | textAlign = TextAlign.Center,
194 | style = MaterialTheme.typography.bodyMedium
195 | )
196 | }
197 | )
198 | }
199 |
200 | if (fileTypeDialog) {
201 | ChooseFileTypeDialog(
202 | onTypeChosen = { type ->
203 | savableFileType = type
204 | shouldShowFileChooser = true
205 | fileTypeDialog = false
206 | },
207 | onDismiss = {
208 | fileTypeDialog = false
209 | }
210 | )
211 | }
212 |
213 | }
214 | }
215 | }
216 |
217 | @OptIn(ExperimentalMaterialApi::class)
218 | @Composable
219 | private fun ChooseFileTypeDialog(onTypeChosen: (colorType: ColorFileType) -> Unit, onDismiss: () -> Unit) {
220 | AlertDialog(
221 | modifier = Modifier.defaultMinSize(minWidth = 400.dp, minHeight = 150.dp)
222 | .background(color = MaterialTheme.colorScheme.surface).testTag(TestTags.FileSaverDialog.dialog),
223 | shape = RoundedCornerShape(8.dp),
224 | title = {
225 | Text(
226 | modifier = Modifier.fillMaxWidth().padding(12.dp),
227 | text = "Do you want to save colors as XML or Kotlin?",
228 | textAlign = TextAlign.Center,
229 | style = MaterialTheme.typography.headlineMedium
230 | )
231 | },
232 | buttons = {
233 | Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
234 | Button(
235 | modifier = Modifier.align(Alignment.CenterVertically),
236 | onClick = {
237 | onTypeChosen(Kotlin)
238 | }
239 | ) {
240 | Text(
241 | text = "Kotlin",
242 | textAlign = TextAlign.Center,
243 | style = MaterialTheme.typography.labelMedium,
244 | color = MaterialTheme.colorScheme.onPrimary
245 | )
246 | }
247 | Spacer(Modifier.size(20.dp))
248 | Button(
249 | modifier = Modifier.align(Alignment.CenterVertically),
250 | onClick = {
251 | onTypeChosen(XML)
252 | }
253 | ) {
254 | Text(
255 | text = "XML",
256 | textAlign = TextAlign.Center,
257 | style = MaterialTheme.typography.labelMedium,
258 | color = MaterialTheme.colorScheme.onPrimary
259 | )
260 | }
261 | }
262 | },
263 | onDismissRequest = {
264 | onDismiss.invoke()
265 | },
266 | text = {
267 | Text(
268 | modifier = Modifier.fillMaxWidth().padding(12.dp),
269 | text = "Please choose a file type to save the color files as",
270 | textAlign = TextAlign.Center,
271 | style = MaterialTheme.typography.bodyMedium
272 | )
273 | }
274 | )
275 | }
276 |
277 | private fun updateColorPalette(
278 | currentColorPalette: ColorScheme,
279 | colorName: String,
280 | color: Color
281 | ): ColorScheme {
282 | return when (colorName) {
283 | "primary" -> currentColorPalette.copy(primary = color)
284 | "onPrimary" -> currentColorPalette.copy(onPrimary = color)
285 | "primaryContainer" -> currentColorPalette.copy(primaryContainer = color)
286 | "onPrimaryContainer" -> currentColorPalette.copy(onPrimaryContainer = color)
287 | "inversePrimary" -> currentColorPalette.copy(inversePrimary = color)
288 | "secondary" -> currentColorPalette.copy(secondary = color)
289 | "onSecondary" -> currentColorPalette.copy(onSecondary = color)
290 | "secondaryContainer" -> currentColorPalette.copy(secondaryContainer = color)
291 | "onSecondaryContainer" -> currentColorPalette.copy(onSecondaryContainer = color)
292 | "tertiary" -> currentColorPalette.copy(tertiary = color)
293 | "onTertiary" -> currentColorPalette.copy(onTertiary = color)
294 | "tertiaryContainer" -> currentColorPalette.copy(tertiaryContainer = color)
295 | "onTertiaryContainer" -> currentColorPalette.copy(onTertiaryContainer = color)
296 | "background" -> currentColorPalette.copy(background = color)
297 | "onBackground" -> currentColorPalette.copy(onBackground = color)
298 | "surface" -> currentColorPalette.copy(surface = color)
299 | "onSurface" -> currentColorPalette.copy(onSurface = color)
300 | "surfaceVariant" -> currentColorPalette.copy(surfaceVariant = color)
301 | "onSurfaceVariant" -> currentColorPalette.copy(onSurfaceVariant = color)
302 | "surfaceTint" -> currentColorPalette.copy(surfaceTint = color)
303 | "inverseSurface" -> currentColorPalette.copy(inverseSurface = color)
304 | "inverseOnSurface" -> currentColorPalette.copy(inverseOnSurface = color)
305 | "error" -> currentColorPalette.copy(error = color)
306 | "onError" -> currentColorPalette.copy(onError = color)
307 | "errorContainer" -> currentColorPalette.copy(errorContainer = color)
308 | "onErrorContainer" -> currentColorPalette.copy(onErrorContainer = color)
309 | "outline" -> currentColorPalette.copy(outline = color)
310 | else -> throw IllegalArgumentException("Color name is wrong $colorName")
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/src/main/kotlin/ComponentScope.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.foundation.background
3 | import androidx.compose.foundation.layout.PaddingValues
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxHeight
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.lazy.grid.GridCells
9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.AppShortcut
12 | import androidx.compose.material.icons.filled.CallToAction
13 | import androidx.compose.material.icons.filled.CheckBox
14 | import androidx.compose.material.icons.filled.CreditCard
15 | import androidx.compose.material.icons.filled.Download
16 | import androidx.compose.material.icons.filled.Poll
17 | import androidx.compose.material.icons.filled.Slideshow
18 | import androidx.compose.material.icons.filled.SpatialAudio
19 | import androidx.compose.material.icons.filled.SwitchLeft
20 | import androidx.compose.material.icons.filled.TextFields
21 | import androidx.compose.material.icons.filled.TouchApp
22 | import androidx.compose.material3.FloatingActionButton
23 | import androidx.compose.material3.Icon
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.NavigationRail
26 | import androidx.compose.material3.NavigationRailItem
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.getValue
30 | import androidx.compose.runtime.mutableStateOf
31 | import androidx.compose.runtime.remember
32 | import androidx.compose.runtime.setValue
33 | import androidx.compose.ui.Modifier
34 | import androidx.compose.ui.graphics.Color
35 | import androidx.compose.ui.platform.testTag
36 | import androidx.compose.ui.text.style.TextAlign
37 | import androidx.compose.ui.unit.dp
38 | import components.m3Cards
39 | import components.m3Checkbox
40 | import components.m3Chips
41 | import components.m3Divider
42 | import components.m3NavigationBars
43 | import components.m3ProgressBar
44 | import components.m3Slider
45 | import components.m3Switch
46 | import components.m3Tab
47 | import components.m3TextFields
48 | import components.m3TopAppBars
49 | import m3components.components.m3Buttons
50 |
51 | enum class Page {
52 | Buttons, AppBars, Cards, TextFields, Chips, Switch, Checkbox, Sliders, ProgressBars, Dividers
53 | }
54 |
55 | @Composable
56 | fun ComponentScope(
57 | onColorPicked: (name: String, color: Color) -> Unit,
58 | onPrintColors: () -> Unit,
59 | ) {
60 | var selectedPage by remember { mutableStateOf(Page.Buttons) }
61 |
62 | Row(
63 | Modifier
64 | .fillMaxSize()
65 | .background(color = MaterialTheme.colorScheme.background)
66 | ) {
67 | NavigationRail(
68 | modifier = Modifier.fillMaxHeight().padding(4.dp).testTag(TestTags.Components.navRail),
69 | header = {
70 | FloatingActionButton(onClick = onPrintColors) {
71 | Icon(imageVector = Icons.Default.CallToAction, contentDescription = "Save Current Colors to File")
72 | }
73 | }
74 | ) {
75 |
76 | NavigationRailItem(
77 | modifier = Modifier.testTag(TestTags.NavRailButtons.buttons),
78 | selected = selectedPage == Page.Buttons,
79 | onClick = {
80 | selectedPage = Page.Buttons
81 | },
82 | icon = {
83 | Icon(imageVector = Icons.Default.TouchApp, contentDescription = "Buttons")
84 | },
85 | label = {
86 | Text(
87 | text = "Buttons",
88 | style = MaterialTheme.typography.labelMedium
89 | )
90 | }
91 | )
92 |
93 | NavigationRailItem(
94 | modifier = Modifier.testTag(TestTags.NavRailButtons.appBars),
95 | selected = selectedPage == Page.AppBars,
96 | onClick = {
97 | selectedPage = Page.AppBars
98 | },
99 | icon = {
100 | Icon(imageVector = Icons.Default.AppShortcut, contentDescription = "App Bars")
101 | },
102 | label = {
103 | Text(
104 | text = "App Bars",
105 | style = MaterialTheme.typography.labelMedium
106 | )
107 | }
108 | )
109 |
110 | NavigationRailItem(
111 | modifier = Modifier.testTag(TestTags.NavRailButtons.cards),
112 | selected = selectedPage == Page.Cards,
113 | onClick = {
114 | selectedPage = Page.Cards
115 | },
116 | icon = {
117 | Icon(imageVector = Icons.Default.CreditCard, contentDescription = "Cards")
118 | },
119 | label = {
120 | Text(
121 | text = "Cards",
122 | style = MaterialTheme.typography.labelMedium
123 | )
124 | }
125 | )
126 |
127 | NavigationRailItem(
128 | modifier = Modifier.testTag(TestTags.NavRailButtons.textFields),
129 | selected = selectedPage == Page.TextFields,
130 | onClick = {
131 | selectedPage = Page.TextFields
132 | },
133 | icon = {
134 | Icon(imageVector = Icons.Default.TextFields, contentDescription = "TextFields")
135 | },
136 | label = {
137 | Text(
138 | text = "TextFields",
139 | style = MaterialTheme.typography.labelMedium
140 | )
141 | }
142 | )
143 |
144 | NavigationRailItem(
145 | modifier = Modifier.testTag(TestTags.NavRailButtons.chips),
146 | selected = selectedPage == Page.Chips,
147 | onClick = {
148 | selectedPage = Page.Chips
149 | },
150 | icon = {
151 | Icon(imageVector = Icons.Default.SpatialAudio, contentDescription = "Chips")
152 | },
153 | label = {
154 | Text(
155 | text = "Chips",
156 | style = MaterialTheme.typography.labelMedium
157 | )
158 | }
159 | )
160 | NavigationRailItem(
161 | modifier = Modifier.testTag(TestTags.NavRailButtons.switch),
162 | selected = selectedPage == Page.Switch,
163 | onClick = {
164 | selectedPage = Page.Switch
165 | },
166 | icon = {
167 | Icon(imageVector = Icons.Default.SwitchLeft, contentDescription = "Switch")
168 | },
169 | label = {
170 | Text(
171 | text = "Switch",
172 | style = MaterialTheme.typography.labelMedium
173 | )
174 | }
175 | )
176 | NavigationRailItem(
177 | modifier = Modifier.testTag(TestTags.NavRailButtons.checkBox),
178 | selected = selectedPage == Page.Checkbox,
179 | onClick = {
180 | selectedPage = Page.Checkbox
181 | },
182 | icon = {
183 | Icon(imageVector = Icons.Default.CheckBox, contentDescription = "Checkbox")
184 | },
185 | label = {
186 | Text(
187 | text = "Checkbox",
188 | style = MaterialTheme.typography.labelMedium
189 | )
190 | }
191 | )
192 | NavigationRailItem(
193 | modifier = Modifier.testTag(TestTags.NavRailButtons.sliders),
194 | selected = selectedPage == Page.Sliders,
195 | onClick = {
196 | selectedPage = Page.Sliders
197 | },
198 | icon = {
199 | Icon(imageVector = Icons.Default.Slideshow, contentDescription = "Sliders")
200 | },
201 | label = {
202 | Text(
203 | text = "Sliders",
204 | style = MaterialTheme.typography.labelMedium
205 | )
206 | }
207 | )
208 |
209 | NavigationRailItem(
210 | modifier = Modifier.testTag(TestTags.NavRailButtons.progressBar),
211 | selected = selectedPage == Page.ProgressBars,
212 | onClick = {
213 | selectedPage = Page.ProgressBars
214 | },
215 | icon = {
216 | Icon(imageVector = Icons.Default.Download, contentDescription = "Progress Bars")
217 | },
218 | label = {
219 | Text(
220 | textAlign = TextAlign.Center,
221 | text = "ProgressBars",
222 | style = MaterialTheme.typography.labelMedium
223 | )
224 | }
225 | )
226 |
227 | NavigationRailItem(
228 | modifier = Modifier.testTag(TestTags.NavRailButtons.dividers),
229 | selected = selectedPage == Page.Dividers,
230 | onClick = {
231 | selectedPage = Page.Dividers
232 | },
233 | icon = {
234 | Icon(imageVector = Icons.Default.Poll, contentDescription = "Dividers")
235 | },
236 | label = {
237 | Text(
238 | text = "Dividers",
239 | style = MaterialTheme.typography.labelMedium
240 | )
241 | }
242 | )
243 | }
244 |
245 | // Components
246 | M3Components(
247 | modifier = Modifier.fillMaxSize()
248 | .weight(2f)
249 | .padding(16.dp),
250 | page = selectedPage,
251 | )
252 |
253 | // Color picking area
254 | ColorPickers(
255 | modifier = Modifier.fillMaxSize()
256 | .weight(1f)
257 | .padding(16.dp),
258 | onColorPicked = onColorPicked
259 | )
260 | }
261 | }
262 |
263 | @Composable
264 | private fun M3Components(modifier: Modifier, page: Page) {
265 | LazyVerticalGrid(
266 | modifier = modifier,
267 | columns = GridCells.Adaptive(300.dp),
268 | contentPadding = PaddingValues(20.dp)
269 | ) {
270 | when (page) {
271 | Page.Buttons -> {
272 | m3Buttons()
273 | }
274 |
275 | Page.AppBars -> {
276 | m3TopAppBars()
277 | m3NavigationBars()
278 | m3Tab()
279 | }
280 |
281 | Page.Cards -> {
282 | m3Cards()
283 | }
284 |
285 | Page.TextFields -> {
286 | m3TextFields()
287 | }
288 |
289 | Page.Chips -> {
290 | m3Chips()
291 | }
292 |
293 | Page.Switch -> {
294 | m3Switch()
295 | }
296 |
297 | Page.Checkbox -> {
298 | m3Checkbox()
299 | }
300 |
301 | Page.Sliders -> {
302 | m3Slider()
303 | }
304 |
305 | Page.ProgressBars -> {
306 | m3ProgressBar()
307 | }
308 |
309 | Page.Dividers -> {
310 | m3Divider()
311 | }
312 | }
313 | }
314 | }
315 |
316 | @Composable
317 | private fun ColorPickers(
318 | modifier: Modifier,
319 | onColorPicked: (name: String, color: Color) -> Unit,
320 | ) {
321 | // Do not use MutableList since it will not be stable.
322 | var recentlyUsedColors: List by remember { mutableStateOf(listOf()) }
323 |
324 | val colorPickedCallback: (colorName: String, pickedColor: Color) -> Unit =
325 | { colorName, pickedColor ->
326 | onColorPicked(colorName, pickedColor)
327 | recentlyUsedColors = recentlyUsedColors.toMutableList().also {
328 | if (!it.contains(pickedColor)) {
329 | it.add(pickedColor)
330 | }
331 | }
332 | }
333 |
334 | LazyVerticalGrid(
335 | modifier = modifier,
336 | columns = GridCells.Adaptive(minSize = 150.dp),
337 | contentPadding = PaddingValues(40.dp)
338 | ) {
339 |
340 | item {
341 | ThemeColorPicker(
342 | colorName = "primary",
343 | currentColor = MaterialTheme.colorScheme.primary,
344 | onColorPicked = colorPickedCallback,
345 | recentlyUsedColors = recentlyUsedColors,
346 | )
347 | }
348 | item {
349 | ThemeColorPicker(
350 | colorName = "onPrimary",
351 | currentColor = MaterialTheme.colorScheme.onPrimary,
352 | onColorPicked = colorPickedCallback,
353 | recentlyUsedColors = recentlyUsedColors,
354 | )
355 | }
356 | item {
357 | ThemeColorPicker(
358 | colorName = "primaryContainer",
359 | currentColor = MaterialTheme.colorScheme.primaryContainer,
360 | onColorPicked = colorPickedCallback,
361 | recentlyUsedColors = recentlyUsedColors,
362 | )
363 | }
364 | item {
365 | ThemeColorPicker(
366 | colorName = "onPrimaryContainer",
367 | currentColor = MaterialTheme.colorScheme.onPrimaryContainer,
368 | onColorPicked = colorPickedCallback,
369 | recentlyUsedColors = recentlyUsedColors,
370 | )
371 | }
372 | item {
373 | ThemeColorPicker(
374 | colorName = "inversePrimary",
375 | currentColor = MaterialTheme.colorScheme.inversePrimary,
376 | onColorPicked = colorPickedCallback,
377 | recentlyUsedColors = recentlyUsedColors,
378 | )
379 | }
380 | item {
381 | ThemeColorPicker(
382 | colorName = "secondary",
383 | currentColor = MaterialTheme.colorScheme.secondary,
384 | onColorPicked = colorPickedCallback,
385 | recentlyUsedColors = recentlyUsedColors,
386 | )
387 | }
388 | item {
389 | ThemeColorPicker(
390 | colorName = "onSecondary",
391 | currentColor = MaterialTheme.colorScheme.onSecondary,
392 | onColorPicked = colorPickedCallback,
393 | recentlyUsedColors = recentlyUsedColors,
394 | )
395 | }
396 | item {
397 | ThemeColorPicker(
398 | colorName = "secondaryContainer",
399 | currentColor = MaterialTheme.colorScheme.secondaryContainer,
400 | onColorPicked = colorPickedCallback,
401 | recentlyUsedColors = recentlyUsedColors,
402 | )
403 | }
404 | item {
405 | ThemeColorPicker(
406 | colorName = "onSecondaryContainer",
407 | currentColor = MaterialTheme.colorScheme.onSecondaryContainer,
408 | onColorPicked = colorPickedCallback,
409 | recentlyUsedColors = recentlyUsedColors,
410 | )
411 | }
412 | item {
413 | ThemeColorPicker(
414 | colorName = "tertiary",
415 | currentColor = MaterialTheme.colorScheme.tertiary,
416 | onColorPicked = colorPickedCallback,
417 | recentlyUsedColors = recentlyUsedColors,
418 | )
419 | }
420 | item {
421 | ThemeColorPicker(
422 | colorName = "onTertiary",
423 | currentColor = MaterialTheme.colorScheme.onTertiary,
424 | onColorPicked = colorPickedCallback,
425 | recentlyUsedColors = recentlyUsedColors,
426 | )
427 | }
428 | item {
429 | ThemeColorPicker(
430 | colorName = "tertiaryContainer",
431 | currentColor = MaterialTheme.colorScheme.tertiaryContainer,
432 | onColorPicked = colorPickedCallback,
433 | recentlyUsedColors = recentlyUsedColors,
434 | )
435 | }
436 | item {
437 | ThemeColorPicker(
438 | colorName = "onTertiaryContainer",
439 | currentColor = MaterialTheme.colorScheme.onTertiaryContainer,
440 | onColorPicked = colorPickedCallback,
441 | recentlyUsedColors = recentlyUsedColors,
442 | )
443 | }
444 | item {
445 | ThemeColorPicker(
446 | colorName = "background",
447 | currentColor = MaterialTheme.colorScheme.background,
448 | onColorPicked = colorPickedCallback,
449 | recentlyUsedColors = recentlyUsedColors,
450 | )
451 | }
452 | item {
453 | ThemeColorPicker(
454 | colorName = "onBackground",
455 | currentColor = MaterialTheme.colorScheme.onBackground,
456 | onColorPicked = colorPickedCallback,
457 | recentlyUsedColors = recentlyUsedColors,
458 | )
459 | }
460 | item {
461 | ThemeColorPicker(
462 | colorName = "surface",
463 | currentColor = MaterialTheme.colorScheme.surface,
464 | onColorPicked = colorPickedCallback,
465 | recentlyUsedColors = recentlyUsedColors,
466 | )
467 | }
468 | item {
469 | ThemeColorPicker(
470 | colorName = "onSurface",
471 | currentColor = MaterialTheme.colorScheme.onSurface,
472 | onColorPicked = colorPickedCallback,
473 | recentlyUsedColors = recentlyUsedColors,
474 | )
475 | }
476 | item {
477 | ThemeColorPicker(
478 | colorName = "surfaceVariant",
479 | currentColor = MaterialTheme.colorScheme.surfaceVariant,
480 | onColorPicked = colorPickedCallback,
481 | recentlyUsedColors = recentlyUsedColors,
482 | )
483 | }
484 | item {
485 | ThemeColorPicker(
486 | colorName = "onSurfaceVariant",
487 | currentColor = MaterialTheme.colorScheme.onSurfaceVariant,
488 | onColorPicked = colorPickedCallback,
489 | recentlyUsedColors = recentlyUsedColors,
490 | )
491 | }
492 | item {
493 | ThemeColorPicker(
494 | colorName = "surfaceTint",
495 | currentColor = MaterialTheme.colorScheme.surfaceTint,
496 | onColorPicked = colorPickedCallback,
497 | recentlyUsedColors = recentlyUsedColors,
498 | )
499 | }
500 | item {
501 | ThemeColorPicker(
502 | colorName = "inverseSurface",
503 | currentColor = MaterialTheme.colorScheme.inverseSurface,
504 | onColorPicked = colorPickedCallback,
505 | recentlyUsedColors = recentlyUsedColors,
506 | )
507 | }
508 | item {
509 | ThemeColorPicker(
510 | colorName = "inverseOnSurface",
511 | currentColor = MaterialTheme.colorScheme.inverseOnSurface,
512 | onColorPicked = colorPickedCallback,
513 | recentlyUsedColors = recentlyUsedColors,
514 | )
515 | }
516 | item {
517 | ThemeColorPicker(
518 | colorName = "error",
519 | currentColor = MaterialTheme.colorScheme.error,
520 | onColorPicked = colorPickedCallback,
521 | recentlyUsedColors = recentlyUsedColors,
522 | )
523 | }
524 | item {
525 | ThemeColorPicker(
526 | colorName = "onError",
527 | currentColor = MaterialTheme.colorScheme.onError,
528 | onColorPicked = colorPickedCallback,
529 | recentlyUsedColors = recentlyUsedColors,
530 | )
531 | }
532 | item {
533 | ThemeColorPicker(
534 | colorName = "errorContainer",
535 | currentColor = MaterialTheme.colorScheme.errorContainer,
536 | onColorPicked = colorPickedCallback,
537 | recentlyUsedColors = recentlyUsedColors,
538 | )
539 | }
540 | item {
541 | ThemeColorPicker(
542 | colorName = "onErrorContainer",
543 | currentColor = MaterialTheme.colorScheme.onErrorContainer,
544 | onColorPicked = colorPickedCallback,
545 | recentlyUsedColors = recentlyUsedColors,
546 | )
547 | }
548 | item {
549 | ThemeColorPicker(
550 | colorName = "outline",
551 | currentColor = MaterialTheme.colorScheme.outline,
552 | onColorPicked = colorPickedCallback,
553 | recentlyUsedColors = recentlyUsedColors,
554 | )
555 | }
556 | }
557 | }
558 |
--------------------------------------------------------------------------------