This commit is contained in:
NianChen
2023-04-13 18:06:05 +08:00
commit e5873ae6fe
4063 changed files with 267552 additions and 0 deletions

88
fluent/build.gradle.kts Normal file
View 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")
}
}
}
}

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"/>

View 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
)
)

View File

@ -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)

View File

@ -0,0 +1,5 @@
package com.konyaco.fluent
import androidx.compose.runtime.compositionLocalOf
val LocalContentAlpha = compositionLocalOf { 1f }

View File

@ -0,0 +1,6 @@
package com.konyaco.fluent
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
val LocalContentColor = compositionLocalOf { Color.Black }

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}

View File

@ -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)
),
)
}
}

View File

@ -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)
)
}
}
}

View File

@ -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)
}
}
}
}
}
}
}
}
}

View File

@ -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)
})
}

View File

@ -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)

View File

@ -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
)
}
}
}
}

View File

@ -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)
)
}
}
}

View File

@ -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
)
)
}
}
}

View File

@ -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

View File

@ -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
)

View File

@ -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
)
}

View File

@ -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()
}
}
}
)
}
}

View File

@ -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
}

View 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))
}