forked from xueque/eimusic-app
init
This commit is contained in:
88
fluent/build.gradle.kts
Normal file
88
fluent/build.gradle.kts
Normal file
@ -0,0 +1,88 @@
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
id("com.android.library")
|
||||
id("maven-publish")
|
||||
}
|
||||
|
||||
group = "com.konyaco"
|
||||
version = "0.0.1-dev4"
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
android()
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
dependencies {
|
||||
api(compose.foundation)
|
||||
api(project(":fluent-icons-core"))
|
||||
api("net.java.dev.jna:jna-platform:latest.release")
|
||||
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
val androidMain by getting {
|
||||
dependencies {
|
||||
api("androidx.appcompat:appcompat:1.5.1")
|
||||
api("androidx.core:core-ktx:1.9.0")
|
||||
}
|
||||
}
|
||||
val jvmMain by getting {
|
||||
dependencies {
|
||||
api(compose.preview)
|
||||
}
|
||||
}
|
||||
val jvmTest by getting{
|
||||
dependencies {
|
||||
implementation("junit:junit:4.13")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//tasks.test {
|
||||
// useJUnitPlatform()
|
||||
//}
|
||||
dependencies {
|
||||
api("net.java.dev.jna:jna-platform:latest.release")
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
namespace = "com.konyaco.fluent"
|
||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
targetSdk = 33
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven {
|
||||
name = "OSSRHSnapshot"
|
||||
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
||||
credentials {
|
||||
username = System.getenv("MAVEN_USERNAME")
|
||||
password = System.getenv("MAVEN_PASSWORD")
|
||||
}
|
||||
}
|
||||
maven {
|
||||
name = "OSSRH"
|
||||
url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
|
||||
credentials {
|
||||
username = System.getenv("MAVEN_USERNAME")
|
||||
password = System.getenv("MAVEN_PASSWORD")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
fluent/src/androidMain/AndroidManifest.xml
Normal file
2
fluent/src/androidMain/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>
|
||||
440
fluent/src/commonMain/kotlin/com/konyaco/fluent/Colors.kt
Normal file
440
fluent/src/commonMain/kotlin/com/konyaco/fluent/Colors.kt
Normal file
@ -0,0 +1,440 @@
|
||||
package com.konyaco.fluent
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import java.awt.color.ColorSpace
|
||||
import java.math.BigInteger
|
||||
|
||||
@Stable
|
||||
class Colors(
|
||||
shades: Shades,
|
||||
darkMode: Boolean
|
||||
) {
|
||||
var darkMode by mutableStateOf(darkMode)
|
||||
internal set
|
||||
var shades by mutableStateOf(shades)
|
||||
internal set
|
||||
var text by mutableStateOf(generateTextColors(shades, darkMode))
|
||||
internal set
|
||||
var control by mutableStateOf(generateControlColors(shades, darkMode))
|
||||
internal set
|
||||
var controlAlt by mutableStateOf(generateControlAltColors(shades, darkMode))
|
||||
internal set
|
||||
var controlSolid by mutableStateOf(generateControlSolidColors(shades, darkMode))
|
||||
internal set
|
||||
var controlStrong by mutableStateOf(generateControlStrongColors(shades, darkMode))
|
||||
internal set
|
||||
var subtleFill by mutableStateOf(generateSubtleFillColors(shades, darkMode))
|
||||
internal set
|
||||
var fillAccent by mutableStateOf(generateFillAccentColors(shades, darkMode))
|
||||
internal set
|
||||
var background by mutableStateOf(generateBackground(shades, darkMode))
|
||||
internal set
|
||||
var stroke by mutableStateOf(generateStroke(shades, darkMode))
|
||||
internal set
|
||||
var borders by mutableStateOf(generateBorders(fillAccent, stroke, darkMode))
|
||||
internal set
|
||||
|
||||
}
|
||||
|
||||
data class Borders(
|
||||
val control: Brush,
|
||||
val accentControl: Brush,
|
||||
val circle: Brush,
|
||||
val textControl: Brush,
|
||||
val textControlFocused: Brush
|
||||
)
|
||||
|
||||
data class Shades(
|
||||
val base: Color,
|
||||
val light1: Color,
|
||||
val light2: Color,
|
||||
val light3: Color,
|
||||
val dark1: Color,
|
||||
val dark2: Color,
|
||||
val dark3: Color,
|
||||
)
|
||||
|
||||
data class TextColor(
|
||||
val text: ColorCompound,
|
||||
val accent: ColorCompound,
|
||||
val onAccent: ColorCompound
|
||||
)
|
||||
|
||||
data class ColorCompound(
|
||||
val primary: Color,
|
||||
val secondary: Color,
|
||||
val tertiary: Color,
|
||||
val disabled: Color
|
||||
)
|
||||
|
||||
data class ControlColors(
|
||||
val default: Color,
|
||||
val secondary: Color,
|
||||
val tertiary: Color,
|
||||
val quaternary: Color,
|
||||
val disabled: Color,
|
||||
val transparent: Color,
|
||||
val inputActive: Color,
|
||||
)
|
||||
|
||||
data class ControlAltColors(
|
||||
val transparent: Color,
|
||||
val secondary: Color,
|
||||
val tertiary: Color,
|
||||
val quaternary: Color,
|
||||
val disabled: Color
|
||||
)
|
||||
|
||||
data class ControlSolidColors(
|
||||
val default: Color
|
||||
)
|
||||
|
||||
data class ControlStrongColors(
|
||||
val default: Color,
|
||||
val disabled: Color
|
||||
)
|
||||
|
||||
data class FillAccentColors(
|
||||
val default: Color,
|
||||
val secondary: Color,
|
||||
val tertiary: Color,
|
||||
val disabled: Color,
|
||||
val selectedTextBackground: Color
|
||||
)
|
||||
|
||||
data class Stroke(
|
||||
val control: Control,
|
||||
val controlStrong: ControlStrong,
|
||||
val surface: Surface
|
||||
) {
|
||||
data class Control(
|
||||
val default: Color,
|
||||
val secondary: Color,
|
||||
val onAccentDefault: Color,
|
||||
val onAccentSecondary: Color,
|
||||
val onAccentTertiary: Color,
|
||||
val disabled: Color,
|
||||
val forStrongFillWhenOnImage: Color
|
||||
)
|
||||
|
||||
data class ControlStrong(
|
||||
val default: Color,
|
||||
val disabled: Color
|
||||
)
|
||||
|
||||
data class Surface(
|
||||
val default: Color,
|
||||
val flyout: Color
|
||||
)
|
||||
}
|
||||
|
||||
data class SubtleFillColors(
|
||||
val transparent: Color,
|
||||
val secondary: Color,
|
||||
val tertiary: Color,
|
||||
val disabled: Color
|
||||
)
|
||||
|
||||
data class Background(
|
||||
val mica: Mica,
|
||||
val layer: Layer
|
||||
) {
|
||||
data class Mica(
|
||||
val base: Color,
|
||||
val baseAlt: Color
|
||||
)
|
||||
|
||||
data class Layer(
|
||||
val default: Color,
|
||||
val alt: Color
|
||||
)
|
||||
}
|
||||
|
||||
fun generateShades(accent: Color): Shades {
|
||||
var shades = generateAccentShades(accent) ?: getAccentShades()[accent] ?: getDefaultShades()
|
||||
// FluentTheme.colors.current = shades.light2
|
||||
return shades
|
||||
}
|
||||
|
||||
internal fun getDefaultShades(): Shades = getAccentShades().entries.first().value
|
||||
|
||||
internal fun generateAccentShades(accent: Color): Shades {
|
||||
val (h, s, l) = accent.hsl
|
||||
return Shades(
|
||||
base = accent,
|
||||
light1 = Color.hsl(h, s, l + 0.1f),
|
||||
light2 = Color.hsl(h, s, l + 0.2f),
|
||||
light3 = Color.hsl(h, s, l + 0.3f),
|
||||
dark1 = Color.hsl(h, s, l - 0.1f),
|
||||
dark2 = Color.hsl(h, s, l - 0.2f),
|
||||
dark3 = Color.hsl(h, s, l - 0.3f),
|
||||
)
|
||||
}
|
||||
|
||||
internal fun getAccentShades() = mapOf(
|
||||
Color(0xFF0078D4) to Shades(
|
||||
base = Color(0xFF0078D4),
|
||||
light1 = Color(0xFF0093F9),
|
||||
light2 = Color(0xFF60CCFE),
|
||||
light3 = Color(0xFF98ECFE),
|
||||
dark1 = Color(0xFF005EB7),
|
||||
dark2 = Color(0xFF003D92),
|
||||
dark3 = Color(0xFF001968)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
val Color.hsl: Triple<Float, Float, Float>
|
||||
get() {
|
||||
var r = this.red
|
||||
var g = this.green
|
||||
var b = this.blue
|
||||
val min = Math.min(r, Math.min(g, b))
|
||||
val max = Math.max(r, Math.max(g, b))
|
||||
val delta = max - min
|
||||
var l = (min + max) / 2.0f
|
||||
var s = if (l > 0 && l < 1) delta / (if (l < 0.5) (2 * l) else (2 - 2 * l)) else 0.0f
|
||||
var h = 0.0f
|
||||
if (delta > 0f) {
|
||||
if (max == r && max != g) h += (g - b) / delta
|
||||
if (max == g && max != b) h += (2 + (b - r) / delta)
|
||||
if (max == b && max != r) h += (4 + (r - g) / delta)
|
||||
h /= 6.0f
|
||||
}
|
||||
h = h * 360
|
||||
return Triple(h, s, l)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun contentColorFor(backgroundColor: Color) =
|
||||
FluentTheme.colors.contentColorFor(backgroundColor).takeOrElse { LocalContentColor.current }
|
||||
|
||||
fun Colors.contentColorFor(backgroundColor: Color): Color {
|
||||
// TODO: Remove this
|
||||
return when (backgroundColor) {
|
||||
shades.base, shades.dark1, shades.dark2, shades.dark3,
|
||||
shades.light1, shades.light2, shades.light3 -> text.onAccent.primary
|
||||
|
||||
else -> text.text.primary
|
||||
}
|
||||
}
|
||||
|
||||
internal fun generateTextColors(shades: Shades, darkMode: Boolean): TextColor =
|
||||
if (darkMode) TextColor(
|
||||
text = ColorCompound(
|
||||
primary = Color(0xFFFFFFFF),
|
||||
secondary = Color(0xC5FFFFFF),
|
||||
tertiary = Color(0x87FFFFFF),
|
||||
disabled = Color(0x5DFFFFFF)
|
||||
),
|
||||
accent = ColorCompound(
|
||||
primary = shades.light3,
|
||||
secondary = shades.light3,
|
||||
tertiary = shades.light2,
|
||||
disabled = Color(0x5DFFFFFF)
|
||||
),
|
||||
onAccent = ColorCompound(
|
||||
primary = Color(0xFF000000),
|
||||
secondary = Color(0x80000000),
|
||||
tertiary = Color(0x87FFFFFF),
|
||||
disabled = Color(0xFFFFFFFF)
|
||||
)
|
||||
)
|
||||
else TextColor(
|
||||
text = ColorCompound(
|
||||
primary = Color(0xE4000000),
|
||||
secondary = Color(0x9B000000),
|
||||
tertiary = Color(0x72000000),
|
||||
disabled = Color(0x5C000000)
|
||||
),
|
||||
accent = ColorCompound(
|
||||
shades.dark2,
|
||||
shades.dark3,
|
||||
shades.dark1,
|
||||
Color(0x5C000000)
|
||||
),
|
||||
onAccent = ColorCompound(
|
||||
primary = Color(0xFFFFFFFF),
|
||||
secondary = Color(0x83FFFFFF),
|
||||
tertiary = Color(0xFFFFFFFF),
|
||||
disabled = Color(0xFFFFFFFF)
|
||||
)
|
||||
)
|
||||
|
||||
internal fun generateControlColors(shades: Shades, darkMode: Boolean): ControlColors =
|
||||
if (darkMode) ControlColors(
|
||||
default = Color(0x0FFFFFFF),
|
||||
secondary = Color(0x15FFFFFF),
|
||||
tertiary = Color(0x0BFFFFFF),
|
||||
quaternary = Color(0x0FFFFFFF),
|
||||
disabled = Color(0x0BFFFFFF),
|
||||
transparent = Color(0x00FFFFFF),
|
||||
inputActive = Color(0xB31E1E1E)
|
||||
)
|
||||
else ControlColors(
|
||||
default = Color(0x83FFFFFF),
|
||||
secondary = Color(0x80F9F9F9),
|
||||
tertiary = Color(0x4DF9F9F9),
|
||||
quaternary = Color(0xC2F3F3F3),
|
||||
disabled = Color(0x4DF9F9F9),
|
||||
transparent = Color(0x00FFFFFF),
|
||||
inputActive = Color(0xFFFFFFFF)
|
||||
)
|
||||
|
||||
internal fun generateControlAltColors(shades: Shades, darkMode: Boolean): ControlAltColors =
|
||||
if (darkMode) ControlAltColors(
|
||||
transparent = Color(0x00FFFFFF),
|
||||
secondary = Color(0x19000000),
|
||||
tertiary = Color(0x0BFFFFFF),
|
||||
quaternary = Color(0x12FFFFFF),
|
||||
disabled = Color(0x00FFFFFF)
|
||||
) else ControlAltColors(
|
||||
transparent = Color(0x00FFFFFF),
|
||||
secondary = Color(0x06000000),
|
||||
tertiary = Color(0x0F000000),
|
||||
quaternary = Color(0x18000000),
|
||||
disabled = Color(0x00FFFFFF)
|
||||
)
|
||||
|
||||
internal fun generateControlSolidColors(shades: Shades, darkMode: Boolean): ControlSolidColors =
|
||||
if (darkMode) ControlSolidColors(default = Color(0xFF454545))
|
||||
else ControlSolidColors(default = Color(0xFFFFFFFF))
|
||||
|
||||
internal fun generateControlStrongColors(shades: Shades, darkMode: Boolean): ControlStrongColors =
|
||||
if (darkMode) ControlStrongColors(
|
||||
default = Color(0x8BFFFFFF),
|
||||
disabled = Color(0x3FFFFFFF)
|
||||
)
|
||||
else ControlStrongColors(
|
||||
default = Color(0x72000000),
|
||||
disabled = Color(0x51000000)
|
||||
)
|
||||
|
||||
internal fun generateSubtleFillColors(shades: Shades, darkMode: Boolean): SubtleFillColors =
|
||||
if (darkMode) SubtleFillColors(
|
||||
transparent = Color(0x00FFFFFF),
|
||||
secondary = Color(0x0FFFFFFF),
|
||||
tertiary = Color(0x0AFFFFFF),
|
||||
disabled = Color(0x00FFFFFF)
|
||||
) else SubtleFillColors(
|
||||
transparent = Color(0x00000000),
|
||||
secondary = Color(0x09000000),
|
||||
tertiary = Color(0x06000000),
|
||||
disabled = Color(0x00000000)
|
||||
)
|
||||
|
||||
internal fun generateFillAccentColors(shades: Shades, darkMode: Boolean): FillAccentColors =
|
||||
if (darkMode) FillAccentColors(
|
||||
default = shades.light2,
|
||||
secondary = shades.light2.copy(0.9f),
|
||||
tertiary = shades.light2.copy(0.8f),
|
||||
disabled = Color(0x28FFFFFF),
|
||||
selectedTextBackground = shades.base
|
||||
)
|
||||
else FillAccentColors(
|
||||
default = shades.dark1,
|
||||
secondary = shades.dark1.copy(0.9f),
|
||||
tertiary = shades.dark1.copy(0.8f),
|
||||
disabled = Color(0x37000000),
|
||||
selectedTextBackground = shades.base
|
||||
)
|
||||
|
||||
internal fun generateBackground(shades: Shades, darkMode: Boolean): Background =
|
||||
if (darkMode) Background(
|
||||
mica = Background.Mica(base = Color(0xFE202020), baseAlt = Color(0xFF0A0A0A)),
|
||||
layer = Background.Layer(default = Color(0x4C3A3A3A), alt = Color(0x0DFFFFFF))
|
||||
)
|
||||
else Background(
|
||||
mica = Background.Mica(base = Color(0xFEF3F3F3), baseAlt = Color(0xFFDADADA)),
|
||||
layer = Background.Layer(default = Color(0x80FFFFFF), alt = Color(0xFFFFFFFF))
|
||||
)
|
||||
|
||||
internal fun generateStroke(shades: Shades, darkMode: Boolean): Stroke =
|
||||
if (darkMode) Stroke(
|
||||
control = Stroke.Control(
|
||||
default = Color(0x12FFFFFF),
|
||||
secondary = Color(0x18FFFFFF),
|
||||
onAccentDefault = Color(0x14FFFFFF),
|
||||
onAccentSecondary = Color(0x23000000),
|
||||
onAccentTertiary = Color(0x37000000),
|
||||
disabled = Color(0x33000000),
|
||||
forStrongFillWhenOnImage = Color(0x6B000000)
|
||||
),
|
||||
controlStrong = Stroke.ControlStrong(
|
||||
default = Color(0x9AFFFFFF),
|
||||
disabled = Color(0x28FFFFFF)
|
||||
),
|
||||
surface = Stroke.Surface(
|
||||
default = Color(0x66757575),
|
||||
flyout = Color(0x33000000)
|
||||
)
|
||||
)
|
||||
else Stroke(
|
||||
control = Stroke.Control(
|
||||
default = Color(0x0F000000),
|
||||
secondary = Color(0x29000000),
|
||||
onAccentDefault = Color(0x14FFFFFF),
|
||||
onAccentSecondary = Color(0x66000000),
|
||||
onAccentTertiary = Color(0x37000000),
|
||||
disabled = Color(0x0F000000),
|
||||
forStrongFillWhenOnImage = Color(0x59FFFFFF)
|
||||
),
|
||||
controlStrong = Stroke.ControlStrong(
|
||||
default = Color(0x9C000000),
|
||||
disabled = Color(0x37000000)
|
||||
),
|
||||
surface = Stroke.Surface(
|
||||
default = Color(0x66757575),
|
||||
flyout = Color(0x0F000000)
|
||||
)
|
||||
)
|
||||
|
||||
private fun generateBorders(fillAccent: FillAccentColors, stroke: Stroke, darkMode: Boolean): Borders =
|
||||
if (darkMode) Borders(
|
||||
control = Brush.verticalGradient(
|
||||
0.0957f to stroke.control.secondary,
|
||||
1f to stroke.control.default
|
||||
),
|
||||
accentControl = Brush.verticalGradient(
|
||||
0.9067f to stroke.control.onAccentDefault,
|
||||
1f to stroke.control.onAccentSecondary,
|
||||
),
|
||||
circle = Brush.verticalGradient(
|
||||
0.5002f to stroke.control.default,
|
||||
0.9545f to stroke.control.secondary
|
||||
),
|
||||
textControl = Brush.verticalGradient(
|
||||
1f to stroke.control.default,
|
||||
1f to stroke.controlStrong.default
|
||||
),
|
||||
textControlFocused = Brush.verticalGradient(
|
||||
0.9395f to stroke.control.default,
|
||||
0.9414f to fillAccent.default
|
||||
)
|
||||
) else Borders(
|
||||
control = Brush.verticalGradient(
|
||||
0.9058f to stroke.control.default,
|
||||
1f to stroke.control.secondary
|
||||
),
|
||||
accentControl = Brush.verticalGradient(
|
||||
0.9067f to stroke.control.onAccentDefault,
|
||||
1f to stroke.control.onAccentSecondary,
|
||||
),
|
||||
circle = Brush.verticalGradient(
|
||||
0f to stroke.control.default,
|
||||
0.5f to stroke.control.secondary
|
||||
),
|
||||
textControl = Brush.verticalGradient(
|
||||
1f to stroke.control.default,
|
||||
1f to stroke.controlStrong.default
|
||||
),
|
||||
textControlFocused = Brush.verticalGradient(
|
||||
0.9395f to stroke.control.default,
|
||||
0.9414f to fillAccent.default
|
||||
)
|
||||
)
|
||||
@ -0,0 +1,41 @@
|
||||
package com.konyaco.fluent
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.sun.jna.platform.win32.Advapi32Util
|
||||
import com.sun.jna.platform.win32.WinReg
|
||||
|
||||
@Composable
|
||||
fun FluentTheme(
|
||||
colors: Colors = FluentTheme.colors,
|
||||
typography: Typography = FluentTheme.typography,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalColors provides colors,
|
||||
LocalTypography provides typography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
object FluentTheme {
|
||||
val colors: Colors
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalColors.current
|
||||
val typography: Typography
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalTypography.current
|
||||
}
|
||||
|
||||
internal val LocalColors = staticCompositionLocalOf { lightColors( Color(0xFF0078D4)) }
|
||||
|
||||
|
||||
//val color = 0xFF0078D4
|
||||
fun lightColors(accent: Color ): Colors = Colors(generateShades(accent), false)
|
||||
fun darkColors(accent: Color): Colors = Colors(generateShades(accent), true)
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
package com.konyaco.fluent
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
|
||||
val LocalContentAlpha = compositionLocalOf { 1f }
|
||||
@ -0,0 +1,6 @@
|
||||
package com.konyaco.fluent
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val LocalContentColor = compositionLocalOf { Color.Black }
|
||||
@ -0,0 +1,67 @@
|
||||
package com.konyaco.fluent
|
||||
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
internal val LocalTypography = staticCompositionLocalOf {
|
||||
Typography(
|
||||
caption = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.Light,
|
||||
fontSize = 12.sp, lineHeight = 16.sp
|
||||
),
|
||||
body = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.Normal,
|
||||
fontSize = 14.sp, lineHeight = 20.sp
|
||||
),
|
||||
bodyStrong = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp, lineHeight = 20.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.Normal,
|
||||
fontSize = 18.sp, lineHeight = 24.sp
|
||||
),
|
||||
subtitle = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 20.sp, lineHeight = 28.sp
|
||||
),
|
||||
title = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 28.sp, lineHeight = 36.sp
|
||||
),
|
||||
titleLarge = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 40.sp, lineHeight = 52.sp
|
||||
),
|
||||
display = TextStyle(
|
||||
color = Color.Black, fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 68.sp, lineHeight = 92.sp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* https://docs.microsoft.com/en-us/windows/apps/design/signature-experiences/typography
|
||||
*/
|
||||
@Immutable
|
||||
class Typography(
|
||||
val caption: TextStyle,
|
||||
val body: TextStyle,
|
||||
val bodyStrong: TextStyle,
|
||||
val bodyLarge: TextStyle,
|
||||
val subtitle: TextStyle,
|
||||
val title: TextStyle,
|
||||
val titleLarge: TextStyle,
|
||||
val display: TextStyle
|
||||
)
|
||||
|
||||
val LocalTextStyle = compositionLocalOf(structuralEqualityPolicy()) { TextStyle.Default }
|
||||
|
||||
@Composable
|
||||
fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
|
||||
val mergedStyle = LocalTextStyle.current.merge(value)
|
||||
CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
package com.konyaco.fluent.animation
|
||||
|
||||
object FluentDuration {
|
||||
val QuickDuration = 83
|
||||
val ShortDuration = 187
|
||||
val MediumDuration = 333
|
||||
val LongDuration = 500
|
||||
val VeryLongDuration = 667
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.konyaco.fluent.animation
|
||||
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
|
||||
object FluentEasing {
|
||||
/**
|
||||
* Direct and subtle
|
||||
*
|
||||
* Usage: Transitions that are functional and utilitarian should use this curve.
|
||||
*/
|
||||
val FastInvokeEasing = CubicBezierEasing(0f, 0f, 0f, 1f)
|
||||
|
||||
/**
|
||||
* Bold and emphasizing
|
||||
*
|
||||
* Usage: Transitions that call attention or reinforce an action should use this curve.
|
||||
*/
|
||||
val StrongInvokeEasing = CubicBezierEasing(0.13f, 1.62f, 0f, 0.92f)
|
||||
|
||||
/**
|
||||
* Direct and subtle
|
||||
*
|
||||
* Usage: Transitions that dismiss a surface without going off screen or within the same area should use this curve combined with a fade out.
|
||||
*/
|
||||
val FastDismissEasing = FastInvokeEasing
|
||||
|
||||
/**
|
||||
* Gentle and mellow
|
||||
*
|
||||
* Usage: Transitions that dismiss a surface off screen while confirming a user action should use this curve.
|
||||
*/
|
||||
val SoftDismissEasing = CubicBezierEasing(1f, 0f, 1f, 1f)
|
||||
|
||||
/**
|
||||
* Direct and guiding
|
||||
*
|
||||
* Usage: Transitions that keep the same element on screen going from one place to another should use this curve.
|
||||
*/
|
||||
val PointToPointEasing = CubicBezierEasing(0.55f, 0.55f, 0f, 1f)
|
||||
|
||||
/**
|
||||
* Quick and efficient
|
||||
*
|
||||
* Usage: Transitions that keep the same element on screen going from one place to another should use this curve.
|
||||
*/
|
||||
val FadeInFadeOutEasing = LinearEasing
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
package com.konyaco.fluent.background
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.LocalContentColor
|
||||
import com.konyaco.fluent.ProvideTextStyle
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
|
||||
@Composable
|
||||
fun Layer(
|
||||
modifier: Modifier = Modifier,
|
||||
shape: Shape = RoundedCornerShape(4.dp),
|
||||
color: Color = FluentTheme.colors.background.layer.default,
|
||||
contentColor: Color = FluentTheme.colors.text.text.primary,
|
||||
border: BorderStroke? = null,
|
||||
outsideBorder: Boolean = false,
|
||||
cornerRadius: Dp = 4.dp,
|
||||
elevation: Dp = 0.dp,
|
||||
circular: Boolean = false, // If layer is circular, use this to remove 1px gap
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
ProvideTextStyle(FluentTheme.typography.body.copy(color = contentColor)) {
|
||||
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||
val innerShape = remember(shape, outsideBorder) {
|
||||
if (shape is RoundedCornerShape && shape != CircleShape && outsideBorder)
|
||||
RoundedCornerShape((cornerRadius - 1.dp).coerceIn(0.dp, Dp.Infinity))
|
||||
else shape
|
||||
}
|
||||
Box(
|
||||
modifier.shadow(elevation, shape, clip = false)
|
||||
.composed { if (border != null) border(border, shape) else this }
|
||||
.composed {
|
||||
// TODO: A better way to implement outside border
|
||||
val density = LocalDensity.current
|
||||
if (outsideBorder) {
|
||||
if (circular) padding(calcCircularPadding(density))
|
||||
else padding(calcPadding(density))
|
||||
} else this
|
||||
}
|
||||
.background(color = color, shape = innerShape)
|
||||
.clip(shape = innerShape), // TODO: A better way to set content corner
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a workaround solution to eliminate 1 pixel gap
|
||||
* when density is not integer or `(density % 1) < 0.5`
|
||||
*/
|
||||
@Stable
|
||||
private fun calcPadding(density: Density): Dp {
|
||||
val remainder = density.density % 1f
|
||||
|
||||
return when {
|
||||
remainder == 0f -> 1.dp
|
||||
remainder < 0.5f -> with(density) {
|
||||
// (1.dp.toPx() + 1).toDp()
|
||||
ceil(1.dp.toPx()).toDp()
|
||||
}
|
||||
|
||||
else -> 1.dp
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private fun calcCircularPadding(density: Density): Dp {
|
||||
val remainder = density.density % 1f
|
||||
|
||||
return with(density) {
|
||||
if (remainder == 0f) (1.dp.toPx() - 1f).toDp() // floor(1.dp.toPx() - 0.5f).toDp()
|
||||
else floor(1.dp.toPx()).toDp()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
package com.konyaco.fluent.background
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.LocalContentColor
|
||||
|
||||
@Composable
|
||||
fun Mica(modifier: Modifier, content: @Composable () -> Unit) {
|
||||
// TODO: Tint opacity and Luminosity opacity
|
||||
Box(modifier.background(FluentTheme.colors.background.mica.base)) {
|
||||
CompositionLocalProvider(LocalContentColor provides FluentTheme.colors.text.text.primary) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,232 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
import com.konyaco.fluent.background.Layer
|
||||
|
||||
@Immutable
|
||||
data class ButtonColors(
|
||||
val default: ButtonColor,
|
||||
val hovered: ButtonColor,
|
||||
val pressed: ButtonColor,
|
||||
val disabled: ButtonColor
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ButtonColor(
|
||||
val fillColor: Color,
|
||||
val contentColor: Color,
|
||||
val borderBrush: Brush
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun Button(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
disabled: Boolean = false,
|
||||
buttonColors: ButtonColors = buttonColors(),
|
||||
interaction: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconOnly: Boolean = false,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Button(modifier, interaction, disabled, buttonColors, false, onClick, iconOnly, content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccentButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
disabled: Boolean = false,
|
||||
buttonColors: ButtonColors = accentButtonColors(),
|
||||
interaction: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconOnly: Boolean = false,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Button(modifier, interaction, disabled, buttonColors, true, onClick, iconOnly, content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SubtleButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
disabled: Boolean = false,
|
||||
buttonColors: ButtonColors = subtleButtonColors(),
|
||||
interaction: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
iconOnly: Boolean = false,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
Button(modifier, interaction, disabled, buttonColors, true, onClick, iconOnly, content)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Button(
|
||||
modifier: Modifier,
|
||||
interaction: MutableInteractionSource,
|
||||
disabled: Boolean,
|
||||
buttonColors: ButtonColors,
|
||||
accentButton: Boolean,
|
||||
onClick: () -> Unit,
|
||||
iconOnly: Boolean,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val hovered by interaction.collectIsHoveredAsState()
|
||||
val pressed by interaction.collectIsPressedAsState()
|
||||
|
||||
val buttonColor = when {
|
||||
disabled -> buttonColors.disabled
|
||||
pressed -> buttonColors.pressed
|
||||
hovered -> buttonColors.hovered
|
||||
else -> buttonColors.default
|
||||
}
|
||||
|
||||
val fillColor by animateColorAsState(
|
||||
buttonColor.fillColor,
|
||||
animationSpec = tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
|
||||
val contentColor by animateColorAsState(
|
||||
buttonColor.contentColor,
|
||||
animationSpec = tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
|
||||
Layer(
|
||||
modifier = modifier.let {
|
||||
if (iconOnly) {
|
||||
it.defaultMinSize(32.dp, 32.dp)
|
||||
} else {
|
||||
it.defaultMinSize(
|
||||
minWidth = 120.dp,
|
||||
minHeight = 32.dp
|
||||
)
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
border = BorderStroke(1.dp, buttonColor.borderBrush),
|
||||
color = fillColor,
|
||||
contentColor = contentColor,
|
||||
outsideBorder = !accentButton,
|
||||
cornerRadius = 4.dp
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.clickable(
|
||||
onClick = onClick,
|
||||
interactionSource = interaction,
|
||||
indication = null
|
||||
)
|
||||
.composed {
|
||||
if (iconOnly) this
|
||||
else padding(horizontal = 12.dp)
|
||||
},
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun buttonColors(): ButtonColors {
|
||||
val colors = FluentTheme.colors
|
||||
|
||||
return remember(colors) {
|
||||
ButtonColors(
|
||||
default = ButtonColor(
|
||||
colors.control.default,
|
||||
colors.text.text.primary,
|
||||
colors.borders.control
|
||||
),
|
||||
hovered = ButtonColor(
|
||||
colors.control.secondary,
|
||||
colors.text.text.primary,
|
||||
colors.borders.control
|
||||
),
|
||||
pressed = ButtonColor(
|
||||
colors.control.tertiary,
|
||||
colors.text.text.secondary,
|
||||
SolidColor(colors.stroke.control.default)
|
||||
),
|
||||
disabled = ButtonColor(
|
||||
colors.control.disabled,
|
||||
colors.text.text.disabled,
|
||||
SolidColor(colors.stroke.control.default)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun accentButtonColors(): ButtonColors {
|
||||
val colors = FluentTheme.colors
|
||||
return remember(colors) {
|
||||
ButtonColors(
|
||||
default = ButtonColor(
|
||||
colors.fillAccent.default,
|
||||
colors.text.onAccent.primary,
|
||||
colors.borders.accentControl
|
||||
),
|
||||
hovered = ButtonColor(
|
||||
colors.fillAccent.secondary,
|
||||
colors.text.onAccent.primary,
|
||||
colors.borders.accentControl
|
||||
),
|
||||
pressed = ButtonColor(
|
||||
colors.fillAccent.tertiary,
|
||||
colors.text.onAccent.secondary,
|
||||
SolidColor(colors.stroke.control.onAccentDefault)
|
||||
),
|
||||
disabled = ButtonColor(
|
||||
colors.fillAccent.disabled,
|
||||
colors.text.onAccent.disabled,
|
||||
SolidColor(Color.Transparent) // Disabled accent button does not have border
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun subtleButtonColors(): ButtonColors {
|
||||
val colors = FluentTheme.colors
|
||||
return remember(colors) {
|
||||
ButtonColors(
|
||||
default = ButtonColor(
|
||||
colors.subtleFill.transparent,
|
||||
colors.text.text.primary,
|
||||
SolidColor(Color.Transparent)
|
||||
),
|
||||
hovered = ButtonColor(
|
||||
colors.subtleFill.secondary,
|
||||
colors.text.text.primary,
|
||||
SolidColor(Color.Transparent)
|
||||
),
|
||||
pressed = ButtonColor(
|
||||
colors.subtleFill.tertiary,
|
||||
colors.text.text.secondary,
|
||||
SolidColor(Color.Transparent)
|
||||
),
|
||||
disabled = ButtonColor(
|
||||
colors.subtleFill.disabled,
|
||||
colors.text.text.disabled,
|
||||
SolidColor(Color.Transparent)
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
import com.konyaco.fluent.background.Layer
|
||||
import com.konyaco.fluent.icons.Icons
|
||||
import com.konyaco.fluent.icons.regular.Checkmark
|
||||
|
||||
@Composable
|
||||
fun CheckBox(
|
||||
checked: Boolean,
|
||||
label: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
onCheckStateChange: (checked: Boolean) -> Unit
|
||||
) {
|
||||
// TODO: Animation, TripleStateCheckbox
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
|
||||
Row(
|
||||
modifier = modifier.composed {
|
||||
if (label != null) Modifier.defaultMinSize(minWidth = 120.dp)
|
||||
else Modifier
|
||||
}.clickable(
|
||||
role = Role.Checkbox,
|
||||
indication = null,
|
||||
interactionSource = interactionSource
|
||||
) { onCheckStateChange(!checked) },
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val colors = FluentTheme.colors
|
||||
val fillColor by animateColorAsState(
|
||||
if (checked) when {
|
||||
!enabled -> colors.fillAccent.disabled
|
||||
pressed -> colors.fillAccent.tertiary
|
||||
hovered -> colors.fillAccent.secondary
|
||||
else -> colors.fillAccent.default
|
||||
} else when {
|
||||
!enabled -> colors.controlAlt.disabled
|
||||
pressed -> colors.controlAlt.quaternary
|
||||
hovered -> colors.controlAlt.tertiary
|
||||
else -> colors.controlAlt.secondary
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
Layer(
|
||||
modifier = Modifier.size(20.dp),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
border = BorderStroke(
|
||||
1.dp, if (checked) when {
|
||||
!enabled -> colors.fillAccent.disabled
|
||||
else -> Color.Transparent
|
||||
} else when {
|
||||
!enabled -> colors.controlStrong.disabled
|
||||
else -> colors.controlStrong.default
|
||||
}
|
||||
),
|
||||
color = fillColor,
|
||||
contentColor = when {
|
||||
!enabled -> colors.text.onAccent.disabled
|
||||
pressed -> colors.text.onAccent.secondary
|
||||
else -> colors.text.onAccent.primary
|
||||
},
|
||||
outsideBorder = !checked,
|
||||
cornerRadius = 4.dp
|
||||
) {
|
||||
// TODO: Animation
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
if (checked) Icon(
|
||||
modifier = Modifier.size(16.dp),
|
||||
imageVector = Icons.Default.Checkmark,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
label?.let {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
modifier = Modifier.offset(y = (-1).dp),
|
||||
text = it,
|
||||
style = FluentTheme.typography.body.copy(color = colors.text.text.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Popup
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
import com.konyaco.fluent.background.Layer
|
||||
import com.konyaco.fluent.background.Mica
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun Dialog(
|
||||
title: String,
|
||||
visible: Boolean,
|
||||
cancelButtonText: String,
|
||||
onCancel: () -> Unit,
|
||||
confirmButtonText: String,
|
||||
onConfirm: () -> Unit
|
||||
) {
|
||||
val visibleState = remember { MutableTransitionState(false) }
|
||||
|
||||
LaunchedEffect(visible) {
|
||||
visibleState.targetState = visible
|
||||
}
|
||||
|
||||
if (!(!visibleState.currentState && !visibleState.targetState)) Popup {
|
||||
Box(
|
||||
Modifier.fillMaxSize()
|
||||
.background(Color.Black.copy(0.12f))
|
||||
.pointerInput(Unit) {},
|
||||
Alignment.Center
|
||||
) {
|
||||
|
||||
val tween = tween<Float>(
|
||||
easing = FluentEasing.FastInvokeEasing,
|
||||
durationMillis = FluentDuration.QuickDuration
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visibleState = visibleState,
|
||||
enter = fadeIn(tween) + scaleIn(tween, initialScale = 1.1f),
|
||||
exit = fadeOut(tween) + scaleOut(tween, targetScale = 1.1f)
|
||||
) {
|
||||
Mica(Modifier.wrapContentSize().clip(RoundedCornerShape(8.dp))) {
|
||||
Layer(
|
||||
Modifier.wrapContentSize().widthIn(200.dp, 600.dp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
cornerRadius = 8.dp
|
||||
) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
Text(title)
|
||||
Spacer(Modifier.height(32.dp))
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
AccentButton(modifier = Modifier.weight(1f), onClick = onConfirm) {
|
||||
Text(confirmButtonText)
|
||||
}
|
||||
Button(modifier = Modifier.weight(1f), onClick = onCancel) {
|
||||
Text(cancelButtonText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,128 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.MutableTransitionState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.*
|
||||
import androidx.compose.ui.window.Popup
|
||||
import androidx.compose.ui.window.PopupPositionProvider
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
import com.konyaco.fluent.background.Layer
|
||||
import com.konyaco.fluent.background.Mica
|
||||
|
||||
@Composable
|
||||
fun DropdownMenu(
|
||||
expanded: Boolean,
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
offset: DpOffset = DpOffset(0.dp, 0.dp), // TODO: Offset
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
val expandedStates = remember { MutableTransitionState(false) }
|
||||
expandedStates.targetState = expanded
|
||||
|
||||
if (expandedStates.currentState || expandedStates.targetState) {
|
||||
val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } // TODO: Transform Origin
|
||||
val density = LocalDensity.current
|
||||
|
||||
val popupPositionProvider = DropdownMenuPositionProvider(density)
|
||||
|
||||
Popup(
|
||||
onDismissRequest = onDismissRequest,
|
||||
popupPositionProvider = popupPositionProvider,
|
||||
) {
|
||||
DropdownMenuContent(
|
||||
expandedStates = expandedStates,
|
||||
transformOriginState = transformOriginState,
|
||||
modifier = modifier,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class DropdownMenuPositionProvider(val density: Density) : PopupPositionProvider {
|
||||
override fun calculatePosition(
|
||||
anchorBounds: IntRect,
|
||||
windowSize: IntSize,
|
||||
layoutDirection: LayoutDirection,
|
||||
popupContentSize: IntSize
|
||||
): IntOffset {
|
||||
val xCenter = (anchorBounds.right + anchorBounds.left) / 2
|
||||
val x = xCenter - (popupContentSize.width / 2)
|
||||
val gap = with(density) { 4.dp.roundToPx() }
|
||||
|
||||
val topSpace = anchorBounds.top
|
||||
val bottomSpace = windowSize.height - anchorBounds.bottom
|
||||
val needSpace = popupContentSize.height + gap
|
||||
|
||||
val popupToTop = bottomSpace < needSpace && topSpace > needSpace
|
||||
|
||||
val y = if(popupToTop) {
|
||||
anchorBounds.top - needSpace
|
||||
} else {
|
||||
anchorBounds.bottom + gap
|
||||
}
|
||||
|
||||
return IntOffset(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun DropdownMenuContent(
|
||||
expandedStates: MutableTransitionState<Boolean>,
|
||||
transformOriginState: MutableState<TransformOrigin>,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable ColumnScope.() -> Unit
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visibleState = expandedStates,
|
||||
enter = expandVertically(
|
||||
tween(FluentDuration.ShortDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
), // TODO: If popup direction is upward, the expanding animation should be bottom-to-top.
|
||||
exit = fadeOut(tween(FluentDuration.ShortDuration, easing = FluentEasing.FastDismissEasing))
|
||||
) {
|
||||
Mica(Modifier.shadow(8.dp, RoundedCornerShape(8.dp)).clip(RoundedCornerShape(8.dp))) {
|
||||
// TODO: Dropdown should use Acrylic material.
|
||||
Layer(
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
border = BorderStroke(1.dp, FluentTheme.colors.stroke.surface.flyout),
|
||||
cornerRadius = 8.dp
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.padding(vertical = 4.dp, horizontal = 4.dp)
|
||||
.width(IntrinsicSize.Max)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun DropdownMenuItem(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
|
||||
SubtleButton(modifier = Modifier.defaultMinSize(minWidth = 100.dp), onClick = onClick, iconOnly = true, content = {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), content = content)
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.paint
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.toolingGraphicsLayer
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.role
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.LocalContentAlpha
|
||||
import com.konyaco.fluent.LocalContentColor
|
||||
|
||||
|
||||
@Composable
|
||||
fun Icon(
|
||||
imageVector: ImageVector,
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier,
|
||||
tint: Color = LocalContentColor.current.copy(LocalContentAlpha.current)
|
||||
) {
|
||||
val painter = rememberVectorPainter(imageVector)
|
||||
Icon(painter, tint, contentDescription, modifier)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun Icon(
|
||||
painter: Painter,
|
||||
tint: Color = LocalContentColor.current.copy(LocalContentAlpha.current),
|
||||
contentDescription: String?,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// TODO: b/149735981 semantics for content description
|
||||
val colorFilter = if (tint == Color.Unspecified) null else ColorFilter.tint(tint)
|
||||
val semantics = if (contentDescription != null) {
|
||||
Modifier.semantics {
|
||||
this.contentDescription = contentDescription
|
||||
this.role = Role.Image
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
Box(
|
||||
modifier.toolingGraphicsLayer()
|
||||
.defaultSizeFor(painter)
|
||||
.paint(
|
||||
painter,
|
||||
colorFilter = colorFilter,
|
||||
contentScale = ContentScale.Fit
|
||||
)
|
||||
.then(semantics)
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.defaultSizeFor(painter: Painter) =
|
||||
this.then(
|
||||
if (painter.intrinsicSize == Size.Unspecified || painter.intrinsicSize.isInfinite()) {
|
||||
DefaultIconSizeModifier
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
|
||||
private fun Size.isInfinite() = width.isInfinite() && height.isInfinite()
|
||||
|
||||
// Default icon size, for icons with no intrinsic size information
|
||||
private val DefaultIconSizeModifier = Modifier.size(16.dp)
|
||||
@ -0,0 +1,149 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.requiredHeight
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
|
||||
/**
|
||||
* Progress bar
|
||||
* @param progress `0f` to `1f`
|
||||
* @param color Line color
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressBar(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = FluentTheme.colors.fillAccent.default
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.defaultMinSize(minWidth = 130.dp, minHeight = 3.dp),
|
||||
propagateMinConstraints = true,
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
Rail()
|
||||
Box(Modifier.matchParentSize()) {
|
||||
Track(progress, color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Rail() {
|
||||
Box(Modifier.requiredHeight(1.dp).background(FluentTheme.colors.controlStrong.default, CircleShape))
|
||||
}
|
||||
|
||||
private val TrackWidth = 3.dp
|
||||
|
||||
@Composable
|
||||
private fun Track(
|
||||
progress: Float,
|
||||
color: Color
|
||||
) {
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
if (progress > 0f) {
|
||||
val half = (TrackWidth / 2).toPx()
|
||||
drawLine(
|
||||
color,
|
||||
start = Offset(half, half),
|
||||
strokeWidth = TrackWidth.toPx(),
|
||||
end = Offset(progress * (size.width - half), half),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val LongWidth = 100.dp
|
||||
private val ShortWidth = 50.dp
|
||||
private val Easing = CubicBezierEasing(0.5f, 0f, 0.5f, 1.0f)
|
||||
|
||||
/**
|
||||
* Undetermined progress bar
|
||||
* @param color Line color
|
||||
*/
|
||||
@Composable
|
||||
fun ProgressBar(
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = FluentTheme.colors.fillAccent.default
|
||||
) {
|
||||
Box(
|
||||
modifier.defaultMinSize(minWidth = 130.dp, minHeight = 3.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
// TODO: In Fluent Design Specification, the undetermined ProgressBar has a rail. But the rail does not present in WinUI3 Gallery
|
||||
// Rail()
|
||||
Box(Modifier.matchParentSize()) {
|
||||
val infinite = rememberInfiniteTransition()
|
||||
val progress by infinite.animateFloat(
|
||||
0f, 1f, InfiniteRepeatableSpec(
|
||||
animation = tween(
|
||||
durationMillis = FluentDuration.VeryLongDuration * 3,
|
||||
easing = Easing
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
/*
|
||||
| totalWidth |
|
||||
| preWidth | size.width |
|
||||
| long | size.width |short| size.width |
|
||||
--------[ ]-----[ display area ]
|
||||
| preWidth | size.width | long | size.width |short|
|
||||
[ display area ]--------[ ]-----
|
||||
*/
|
||||
Canvas(Modifier.fillMaxSize().clip(CircleShape)) {
|
||||
val trackWidth = TrackWidth.toPx()
|
||||
val half = trackWidth / 2
|
||||
|
||||
val shortWidthPx = ShortWidth.toPx()
|
||||
val longWidthPx = LongWidth.toPx()
|
||||
|
||||
val preWidth = shortWidthPx + size.width + longWidthPx
|
||||
val totalWidth = size.width + preWidth
|
||||
|
||||
val shortOffset = (progress * totalWidth + longWidthPx + size.width) - preWidth
|
||||
val shortStart = half + shortOffset
|
||||
val shortEnd = shortStart + shortWidthPx - half
|
||||
|
||||
val longOffset = (progress * totalWidth) - preWidth
|
||||
val longStart = half + longOffset
|
||||
val longEnd = longStart + longWidthPx - half
|
||||
|
||||
// Short
|
||||
drawLine(
|
||||
color,
|
||||
start = Offset(shortStart, half),
|
||||
strokeWidth = TrackWidth.toPx(),
|
||||
end = Offset(shortEnd, half),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
|
||||
// Long
|
||||
drawLine(
|
||||
color,
|
||||
start = Offset(longStart, half),
|
||||
strokeWidth = TrackWidth.toPx(),
|
||||
end = Offset(longEnd, half),
|
||||
cap = StrokeCap.Round
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
|
||||
object ProgressRingSize {
|
||||
val Large = 64.dp
|
||||
val Medium = 32.dp
|
||||
val Small = 16.dp
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProgressRing(
|
||||
progress: Float,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = ProgressRingSize.Medium,
|
||||
width: Dp = size * 3 / 32,
|
||||
color: Color = FluentTheme.colors.fillAccent.default
|
||||
) {
|
||||
ProgressRing(
|
||||
modifier = modifier,
|
||||
start = 0f,
|
||||
length = progress * 360f,
|
||||
width = width,
|
||||
color = color,
|
||||
size = size
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProgressRing(
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = ProgressRingSize.Medium,
|
||||
width: Dp = size * 3 / 32,
|
||||
color: Color = FluentTheme.colors.fillAccent.default
|
||||
) {
|
||||
val length by rememberInfiniteTransition().animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 180f,
|
||||
infiniteRepeatable(
|
||||
animation = tween(
|
||||
easing = LinearEasing,
|
||||
durationMillis = (FluentDuration.VeryLongDuration * 1.5f).toInt()
|
||||
), repeatMode = RepeatMode.Reverse
|
||||
)
|
||||
)
|
||||
|
||||
val progress by rememberInfiniteTransition().animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 360f,
|
||||
infiniteRepeatable(
|
||||
animation = tween(
|
||||
easing = LinearEasing,
|
||||
durationMillis = FluentDuration.VeryLongDuration
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
val state by remember {
|
||||
derivedStateOf {
|
||||
(progress - length) to length
|
||||
}
|
||||
}
|
||||
|
||||
ProgressRing(
|
||||
modifier = modifier,
|
||||
start = state.first,
|
||||
length = state.second,
|
||||
width = width,
|
||||
size = size,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProgressRing(
|
||||
modifier: Modifier,
|
||||
start: Float,
|
||||
length: Float,
|
||||
width: Dp,
|
||||
size: Dp,
|
||||
color: Color
|
||||
) {
|
||||
Box(modifier.size(size)) {
|
||||
val density = LocalDensity.current
|
||||
val widthPx by remember(density) { derivedStateOf { with(density) { width.toPx() } } }
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
drawArc(
|
||||
color = color,
|
||||
startAngle = start - 90f,
|
||||
sweepAngle = length,
|
||||
useCenter = false,
|
||||
size = this.size,
|
||||
style = Stroke(widthPx, cap = StrokeCap.Round)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
import com.konyaco.fluent.background.Layer
|
||||
|
||||
@Composable
|
||||
fun RadioButton(
|
||||
selected: Boolean,
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
label: String? = null,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
// TODO: Extract same logic
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
|
||||
Row(modifier.composed {
|
||||
if (label != null) defaultMinSize(minWidth = 120.dp)
|
||||
else this
|
||||
}.clickable(interactionSource, null) {
|
||||
onClick?.invoke()
|
||||
}) {
|
||||
val fillColor by animateColorAsState(
|
||||
if (selected) when {
|
||||
!enabled -> FluentTheme.colors.fillAccent.disabled
|
||||
pressed -> FluentTheme.colors.fillAccent.tertiary
|
||||
hovered -> FluentTheme.colors.fillAccent.secondary
|
||||
else -> FluentTheme.colors.fillAccent.default
|
||||
} else when {
|
||||
!enabled -> FluentTheme.colors.controlAlt.disabled
|
||||
pressed -> FluentTheme.colors.controlAlt.quaternary
|
||||
hovered -> FluentTheme.colors.controlAlt.tertiary
|
||||
else -> FluentTheme.colors.controlAlt.secondary
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
Layer(
|
||||
modifier = Modifier.size(20.dp),
|
||||
shape = CircleShape,
|
||||
color = fillColor,
|
||||
outsideBorder = true,
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
color = if (selected) when {
|
||||
!enabled -> FluentTheme.colors.fillAccent.disabled
|
||||
else -> FluentTheme.colors.fillAccent.default
|
||||
} else when {
|
||||
!enabled || pressed -> FluentTheme.colors.stroke.controlStrong.disabled
|
||||
else -> FluentTheme.colors.stroke.controlStrong.default
|
||||
}
|
||||
),
|
||||
circular = true
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
// Bullet, Only displays when selected, or is pressed
|
||||
|
||||
val size by animateDpAsState(
|
||||
if (selected) when {
|
||||
pressed -> 6.dp
|
||||
hovered -> 10.dp
|
||||
else -> 8.dp
|
||||
} else when {
|
||||
pressed -> 10.dp
|
||||
else -> 0.dp
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
// Inner
|
||||
Layer(
|
||||
modifier = Modifier.size(if (size == 0.dp || !selected) size else size + 2.dp), // TODO: Remove this 2dp if outside border is provided
|
||||
color = FluentTheme.colors.text.onAccent.primary,
|
||||
border = if (selected) BorderStroke(1.dp, FluentTheme.colors.borders.circle) else null,
|
||||
shape = CircleShape,
|
||||
outsideBorder = true,
|
||||
content = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
label?.let {
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
modifier = Modifier.offset(y = (-1).dp),
|
||||
text = it,
|
||||
style = FluentTheme.typography.body.copy(
|
||||
color = if (enabled) FluentTheme.colors.text.text.primary
|
||||
else FluentTheme.colors.text.text.disabled
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,200 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.times
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
import com.konyaco.fluent.background.Layer
|
||||
|
||||
@Composable
|
||||
fun Slider(
|
||||
value: Float,
|
||||
onValueChange: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true, // TODO
|
||||
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||
steps: Int = 0, // TODO
|
||||
onValueChangeFinished: (() -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
val progress = valueToFraction(value, valueRange.start, valueRange.endInclusive)
|
||||
Slider(
|
||||
modifier = modifier,
|
||||
progress = progress,
|
||||
onProgressChange = {
|
||||
onValueChange(fractionToValue(it, valueRange.start, valueRange.endInclusive))
|
||||
},
|
||||
enabled = enabled,
|
||||
onValueChangeFinished = onValueChangeFinished,
|
||||
interactionSource = interactionSource
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Slider(
|
||||
progress: Float,
|
||||
onProgressChange: (Float) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true, // TODO
|
||||
onValueChangeFinished: (() -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
) {
|
||||
// TODO: Refactor this component
|
||||
val currentOnProgressChange by rememberUpdatedState(onProgressChange)
|
||||
BoxWithConstraints(
|
||||
modifier = modifier.defaultMinSize(minWidth = 120.dp, minHeight = 32.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
propagateMinConstraints = true
|
||||
) {
|
||||
val width by rememberUpdatedState(minWidth)
|
||||
var dragging by remember { mutableStateOf(false) }
|
||||
|
||||
val density by rememberUpdatedState(LocalDensity.current)
|
||||
fun calcProgress(offset: Offset): Float {
|
||||
val radius = with(density) { (ThumbSizeWithBorder / 2).toPx() }
|
||||
return valueToFraction(offset.x, radius, constraints.minWidth - radius).coerceIn(0f, 1f)
|
||||
}
|
||||
|
||||
Rail()
|
||||
|
||||
Box(Modifier.composed {
|
||||
var offset by remember { mutableStateOf(Offset.Zero) }
|
||||
draggable(
|
||||
state = rememberDraggableState {
|
||||
offset = Offset(x = offset.x + it, y = offset.y)
|
||||
currentOnProgressChange(calcProgress(offset))
|
||||
},
|
||||
interactionSource = interactionSource,
|
||||
onDragStarted = {
|
||||
dragging = true
|
||||
offset = it
|
||||
},
|
||||
onDragStopped = {
|
||||
dragging = false
|
||||
onValueChangeFinished?.invoke()
|
||||
},
|
||||
orientation = Orientation.Horizontal
|
||||
)
|
||||
}.pointerInput(Unit) {
|
||||
forEachGesture {
|
||||
awaitPointerEventScope {
|
||||
val down = awaitFirstDown()
|
||||
currentOnProgressChange(calcProgress(down.position))
|
||||
}
|
||||
}
|
||||
}, contentAlignment = Alignment.CenterStart) {
|
||||
Track(progress, width)
|
||||
Thumb(width, progress, dragging)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
private fun fractionToValue(fraction: Float, start: Float, end: Float): Float = (end - start) * fraction + start
|
||||
|
||||
@Stable
|
||||
private fun valueToFraction(
|
||||
value: Float, start: Float, end: Float
|
||||
): Float = (value - start) / (end - start)
|
||||
|
||||
@Stable
|
||||
private fun calcThumbOffset(
|
||||
maxWidth: Dp, thumbSize: Dp, padding: Dp, fraction: Float
|
||||
): Dp {
|
||||
return (maxWidth - thumbSize) * fraction - padding
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Rail() {
|
||||
// Rail
|
||||
Layer(modifier = Modifier.requiredHeight(4.dp),
|
||||
shape = CircleShape,
|
||||
color = FluentTheme.colors.controlStrong.default,
|
||||
border = BorderStroke(
|
||||
1.dp, if (FluentTheme.colors.darkMode) FluentTheme.colors.stroke.controlStrong.default
|
||||
else FluentTheme.colors.controlStrong.default
|
||||
),
|
||||
outsideBorder = true,
|
||||
content = {}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Track(
|
||||
fraction: Float,
|
||||
maxWidth: Dp
|
||||
) {
|
||||
// Track
|
||||
val width = ThumbRadiusWithBorder + (fraction * (maxWidth - ThumbSizeWithBorder))
|
||||
Box(
|
||||
Modifier.width(width)
|
||||
.requiredHeight(4.dp)
|
||||
.background(FluentTheme.colors.fillAccent.default, CircleShape)
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Thumb(
|
||||
maxWidth: Dp, fraction: Float, dragging: Boolean,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
// Thumb
|
||||
val thumbOffset by rememberUpdatedState(calcThumbOffset(maxWidth, ThumbSize, 1.dp, fraction))
|
||||
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
|
||||
Layer(
|
||||
modifier = Modifier.offset { IntOffset(x = thumbOffset.roundToPx(), y = 0) }
|
||||
.size(ThumbSizeWithBorder)
|
||||
.clickable(interactionSource, null, onClick = {}),
|
||||
shape = CircleShape,
|
||||
border = BorderStroke(1.dp, FluentTheme.colors.borders.circle),
|
||||
color = FluentTheme.colors.controlSolid.default,
|
||||
outsideBorder = true
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
// Inner Thumb
|
||||
Box(
|
||||
Modifier.size(
|
||||
animateDpAsState(
|
||||
when {
|
||||
pressed || dragging -> InnerThumbPressedSize
|
||||
hovered -> InnerThumbHoverSize
|
||||
else -> InnerThumbSize
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
).value
|
||||
).background(FluentTheme.colors.fillAccent.default, CircleShape)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ThumbSize = 20.dp
|
||||
private val ThumbSizeWithBorder = ThumbSize + 2.dp
|
||||
private val ThumbRadiusWithBorder = ThumbSizeWithBorder / 2
|
||||
private val InnerThumbSize = 12.dp
|
||||
private val InnerThumbHoverSize = 14.dp
|
||||
private val InnerThumbPressedSize = 10.dp
|
||||
@ -0,0 +1,176 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TransformOrigin
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.animation.FluentDuration
|
||||
import com.konyaco.fluent.animation.FluentEasing
|
||||
|
||||
@Composable
|
||||
fun Switcher(
|
||||
checked: Boolean,
|
||||
onCheckStateChange: (checked: Boolean) -> Unit,
|
||||
text: String? = null,
|
||||
textBefore: Boolean = false,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
// TODO: Draggable
|
||||
// TODO: Extract same logic
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val transition = updateTransition(checked)
|
||||
Row(
|
||||
modifier = Modifier.clickable(indication = null, interactionSource = interactionSource, role = Role.Button) {
|
||||
onCheckStateChange(!checked)
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
if (textBefore) {
|
||||
text?.let {
|
||||
Text(
|
||||
modifier = Modifier.offset(y = (-1).dp),
|
||||
text = it,
|
||||
color = if (enabled) FluentTheme.colors.text.text.primary
|
||||
else FluentTheme.colors.text.text.disabled
|
||||
)
|
||||
Spacer(Modifier.width(12.dp))
|
||||
}
|
||||
}
|
||||
|
||||
val colors = FluentTheme.colors
|
||||
val fillColor by animateColorAsState(
|
||||
if (checked) when {
|
||||
!enabled -> colors.fillAccent.disabled
|
||||
pressed -> colors.fillAccent.tertiary
|
||||
hovered -> colors.fillAccent.secondary
|
||||
else -> colors.fillAccent.default
|
||||
} else when {
|
||||
!enabled -> colors.controlAlt.disabled
|
||||
pressed -> colors.controlAlt.quaternary
|
||||
hovered -> colors.controlAlt.tertiary
|
||||
else -> colors.controlAlt.secondary
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
|
||||
Box(
|
||||
modifier = Modifier.size(40.dp, 20.dp)
|
||||
.border(
|
||||
1.dp, if (checked) when {
|
||||
!enabled -> FluentTheme.colors.fillAccent.disabled
|
||||
else -> Color.Transparent
|
||||
} else when {
|
||||
!enabled -> FluentTheme.colors.controlStrong.disabled
|
||||
else -> FluentTheme.colors.controlStrong.default
|
||||
}, CircleShape
|
||||
)
|
||||
.clip(CircleShape)
|
||||
.background(fillColor)
|
||||
.padding(horizontal = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart
|
||||
) {
|
||||
val height by animateDpAsState(
|
||||
when {
|
||||
pressed || hovered -> 14.dp
|
||||
else -> 12.dp
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
|
||||
val width by animateDpAsState(
|
||||
when {
|
||||
pressed -> 17.dp
|
||||
hovered -> 14.dp
|
||||
else -> 12.dp
|
||||
},
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.FastInvokeEasing)
|
||||
)
|
||||
|
||||
val density = LocalDensity.current
|
||||
val offset by transition.animateDp(
|
||||
transitionSpec = {
|
||||
tween(FluentDuration.QuickDuration, easing = FluentEasing.PointToPointEasing)
|
||||
},
|
||||
targetValueByState = {
|
||||
if (checked) 26.dp - (width / 2) else 0.dp
|
||||
}
|
||||
)
|
||||
|
||||
val offsetX by remember(density) {
|
||||
derivedStateOf { with(density) { offset.toPx() } }
|
||||
}
|
||||
|
||||
// Control
|
||||
Box(
|
||||
Modifier.size(width, height)
|
||||
.graphicsLayer {
|
||||
translationX = offsetX
|
||||
transformOrigin = TransformOrigin.Center
|
||||
}
|
||||
.clip(CircleShape)
|
||||
.background(
|
||||
if (checked) when {
|
||||
!enabled -> FluentTheme.colors.text.onAccent.disabled
|
||||
else -> FluentTheme.colors.text.onAccent.primary
|
||||
}
|
||||
else when {
|
||||
!enabled -> FluentTheme.colors.text.text.disabled
|
||||
else -> FluentTheme.colors.text.text.secondary
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (!textBefore) {
|
||||
text?.let {
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(
|
||||
modifier = Modifier.offset(y = (-1).dp),
|
||||
text = it,
|
||||
style = FluentTheme.typography.body,
|
||||
color = if (enabled) FluentTheme.colors.text.text.primary
|
||||
else FluentTheme.colors.text.text.disabled
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SwitcherColors(
|
||||
val default: SwitcherColor,
|
||||
val hovered: SwitcherColor,
|
||||
val pressed: SwitcherColor,
|
||||
val disabled: SwitcherColor
|
||||
)
|
||||
|
||||
data class SwitcherColor(
|
||||
val fillColor: Color,
|
||||
val textColor: Color,
|
||||
val borderBrush: Brush
|
||||
)
|
||||
@ -0,0 +1,111 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.text.InlineTextContent
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.konyaco.fluent.LocalContentAlpha
|
||||
import com.konyaco.fluent.LocalContentColor
|
||||
import com.konyaco.fluent.LocalTextStyle
|
||||
|
||||
@Composable
|
||||
fun Text(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
Text(
|
||||
AnnotatedString(text),
|
||||
modifier,
|
||||
color,
|
||||
fontSize,
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
fontFamily,
|
||||
letterSpacing,
|
||||
textDecoration,
|
||||
textAlign,
|
||||
lineHeight,
|
||||
overflow,
|
||||
softWrap,
|
||||
maxLines,
|
||||
emptyMap(),
|
||||
onTextLayout,
|
||||
style
|
||||
)
|
||||
}
|
||||
@Composable
|
||||
fun Text(
|
||||
text: AnnotatedString,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
overflow: TextOverflow = TextOverflow.Clip,
|
||||
softWrap: Boolean = true,
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
inlineContent: Map<String, InlineTextContent> = mapOf(),
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current
|
||||
) {
|
||||
val textColor = color.takeOrElse {
|
||||
style.color.takeOrElse {
|
||||
LocalContentColor.current.copy(alpha = LocalContentAlpha.current)
|
||||
}
|
||||
}
|
||||
val mergedStyle = style.merge(
|
||||
TextStyle(
|
||||
color = textColor,
|
||||
fontSize = fontSize,
|
||||
fontWeight = fontWeight,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
fontFamily = fontFamily,
|
||||
textDecoration = textDecoration,
|
||||
fontStyle = fontStyle,
|
||||
letterSpacing = letterSpacing
|
||||
)
|
||||
)
|
||||
BasicText(
|
||||
text,
|
||||
modifier,
|
||||
mergedStyle,
|
||||
onTextLayout,
|
||||
overflow,
|
||||
softWrap,
|
||||
maxLines,
|
||||
inlineContent
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,121 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.konyaco.fluent.FluentTheme
|
||||
import com.konyaco.fluent.LocalTextStyle
|
||||
import com.konyaco.fluent.background.Layer
|
||||
|
||||
@Composable
|
||||
fun TextField(
|
||||
value: TextFieldValue,
|
||||
onValueChange: (TextFieldValue) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
readOnly: Boolean = false,
|
||||
singleLine: Boolean = false,
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||
maxLines: Int = Int.MAX_VALUE,
|
||||
header: (@Composable () -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
val hovered by interactionSource.collectIsHoveredAsState()
|
||||
val pressed by interactionSource.collectIsPressedAsState()
|
||||
val focused by interactionSource.collectIsFocusedAsState()
|
||||
|
||||
Column(modifier) {
|
||||
if (header != null) {
|
||||
header()
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
BasicTextField(
|
||||
modifier = modifier.defaultMinSize(160.dp, 32.dp)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.composed {
|
||||
if (enabled) {
|
||||
val height by rememberUpdatedState(with(LocalDensity.current) {
|
||||
(if (focused) 2.dp else 1.dp).toPx()
|
||||
})
|
||||
val fillColor by rememberUpdatedState(
|
||||
if (focused) FluentTheme.colors.fillAccent.default
|
||||
else FluentTheme.colors.stroke.controlStrong.default
|
||||
)
|
||||
drawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
color = fillColor,
|
||||
topLeft = Offset(0f, size.height - height),
|
||||
size = Size(size.width, height)
|
||||
)
|
||||
}
|
||||
} else this
|
||||
},
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
textStyle = LocalTextStyle.current.copy(
|
||||
color = if (enabled) FluentTheme.colors.text.text.primary
|
||||
else FluentTheme.colors.text.text.disabled
|
||||
),
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
singleLine = singleLine,
|
||||
visualTransformation = visualTransformation,
|
||||
maxLines = maxLines,
|
||||
keyboardActions = keyboardActions,
|
||||
cursorBrush = SolidColor(FluentTheme.colors.text.text.primary),
|
||||
keyboardOptions = keyboardOptions,
|
||||
interactionSource = interactionSource,
|
||||
decorationBox = { innerTextField ->
|
||||
Layer(
|
||||
modifier = Modifier.hoverable(interactionSource),
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
border = BorderStroke(
|
||||
1.dp,
|
||||
if (focused || pressed) SolidColor(FluentTheme.colors.stroke.control.default)
|
||||
else FluentTheme.colors.borders.textControl
|
||||
),
|
||||
color = when {
|
||||
!enabled -> FluentTheme.colors.control.disabled
|
||||
pressed || focused -> FluentTheme.colors.control.inputActive
|
||||
hovered -> FluentTheme.colors.control.secondary
|
||||
else -> FluentTheme.colors.control.default
|
||||
},
|
||||
) {
|
||||
Box(
|
||||
Modifier.offset(y = (-1).dp).padding(start = 12.dp, end = 12.dp, top = 4.dp, bottom = 3.dp),
|
||||
Alignment.CenterStart
|
||||
) {
|
||||
innerTextField()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
package com.konyaco.fluent.component
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.ResourceLoader
|
||||
import androidx.compose.ui.res.loadSvgPainter
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun rememberResourcePainter(resPath: String): Painter {
|
||||
val density = LocalDensity.current
|
||||
val svg = remember(density) {
|
||||
val file = ResourceLoader.Default.load(resPath)
|
||||
loadSvgPainter(file, density)
|
||||
}
|
||||
return svg
|
||||
}
|
||||
10
fluent/src/jvmTest/kotlin/ColorTest.kt
Normal file
10
fluent/src/jvmTest/kotlin/ColorTest.kt
Normal file
@ -0,0 +1,10 @@
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.konyaco.fluent.hsl
|
||||
|
||||
fun main() {
|
||||
var color = Color(0xFF0078D4)
|
||||
val (h,s,l) = color.hsl
|
||||
println(color)
|
||||
println(Color.hsl(h,s,l))
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user