diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.android.kt new file mode 100644 index 00000000..1a0e2804 --- /dev/null +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.android.kt @@ -0,0 +1,8 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable + +@Composable +actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) { + content() +} \ No newline at end of file diff --git a/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt new file mode 100644 index 00000000..d1ef3903 --- /dev/null +++ b/fluent/src/androidMain/kotlin/com/konyaco/fluent/component/FontIcon.android.kt @@ -0,0 +1,8 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.Composable + +@Composable +actual fun ProvideFontIcon(content: @Composable () -> Unit) { + content() +} \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt index 445cd686..10f8b8fa 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt @@ -1,11 +1,9 @@ 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.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily +import com.konyaco.fluent.component.ProvideFontIcon @Composable fun FluentTheme( @@ -27,9 +25,12 @@ fun FluentTheme( titleLarge = typography.titleLarge.copy(fontFamily = defaultFontFamily), display = typography.display.copy(fontFamily = defaultFontFamily), ) - } ?: typography), - content = content - ) + } ?: typography) + ) { + ProvideFontIcon { + PlatformCompositionLocalProvider(content) + } + } } object FluentTheme { @@ -45,6 +46,5 @@ object FluentTheme { internal val LocalColors = staticCompositionLocalOf { lightColors() } - fun lightColors(accent: Color = Color(0xFF0078D4)): Colors = Colors(generateShades(accent), false) fun darkColors(accent: Color = Color(0xFF0078D4)): Colors = Colors(generateShades(accent), true) \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.kt new file mode 100644 index 00000000..1669aa64 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.kt @@ -0,0 +1,7 @@ +package com.konyaco.fluent + +import androidx.compose.runtime.Composable + +@Composable +expect fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) + diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt index 91132b81..92d0417b 100644 --- a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt @@ -14,10 +14,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf 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.draw.shadow import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.* import androidx.compose.ui.window.Popup @@ -33,6 +35,9 @@ fun DropdownMenu( expanded: Boolean, onDismissRequest: () -> Unit, modifier: Modifier = Modifier, + focusable: Boolean = false, + onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false }, + onKeyEvent: ((KeyEvent) -> Boolean) = { false }, offset: DpOffset = DpOffset(0.dp, 0.dp), // TODO: Offset content: @Composable ColumnScope.() -> Unit ) { @@ -46,7 +51,10 @@ fun DropdownMenu( val popupPositionProvider = DropdownMenuPositionProvider(density) Popup( + focusable = focusable, onDismissRequest = onDismissRequest, + onKeyEvent = onKeyEvent, + onPreviewKeyEvent = onPreviewKeyEvent, popupPositionProvider = popupPositionProvider, ) { DropdownMenuContent( @@ -121,8 +129,8 @@ internal fun DropdownMenuContent( } @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) +fun DropdownMenuItem(onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) { + SubtleButton(modifier = modifier.defaultMinSize(minWidth = 100.dp), onClick = onClick, iconOnly = true, content = { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), content = content) }) } \ No newline at end of file diff --git a/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FontIcon.kt b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FontIcon.kt new file mode 100644 index 00000000..a4734898 --- /dev/null +++ b/fluent/src/commonMain/kotlin/com/konyaco/fluent/component/FontIcon.kt @@ -0,0 +1,43 @@ +package com.konyaco.fluent.component + +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp + +@Composable +fun FontIcon( + glyph: Char, + modifier: Modifier = Modifier, + iconSize: TextUnit = FontIconDefaults.fontSizeStandard, + fallback: (@Composable () -> Unit)? = null, +) { + if (LocalFontIconFontFamily.current != null || fallback == null) { + Text( + text = glyph.toString(), + fontFamily = LocalFontIconFontFamily.current, + fontSize = iconSize, + modifier = Modifier.then(modifier) + .height(with(LocalDensity.current) { iconSize.toDp() }) + ) + } else { + fallback() + } +} + +object FontIconDefaults { + val fontSizeStandard = 16.sp + val fontSizeSmall = 12.sp +} + +@Composable +expect fun ProvideFontIcon( + content: @Composable () -> Unit +) + +val LocalFontIconFontFamily = + staticCompositionLocalOf { error("No Font provide for load font icon") } diff --git a/fluent/src/jvmMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.desktop.kt b/fluent/src/jvmMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.desktop.kt new file mode 100644 index 00000000..5a7bff46 --- /dev/null +++ b/fluent/src/jvmMain/kotlin/com/konyaco/fluent/PlatformCompositionLocalProvider.desktop.kt @@ -0,0 +1,21 @@ +package com.konyaco.fluent + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalContextMenuRepresentation +import androidx.compose.foundation.text.LocalTextContextMenu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import com.konyaco.fluent.component.FluentContextMenuRepresentation +import com.konyaco.fluent.component.FluentTextContextMenu + +@OptIn(ExperimentalFoundationApi::class) +@Composable +actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) { + CompositionLocalProvider( + LocalTextContextMenu provides FluentTextContextMenu, + LocalContextMenuRepresentation provides FluentContextMenuRepresentation + ) { + content() + } +} + diff --git a/fluent/src/jvmMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt b/fluent/src/jvmMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt new file mode 100644 index 00000000..ae8ee139 --- /dev/null +++ b/fluent/src/jvmMain/kotlin/com/konyaco/fluent/component/ContextMenu.desktop.kt @@ -0,0 +1,175 @@ +package com.konyaco.fluent.component + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.TextContextMenu +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.input.key.* +import androidx.compose.ui.platform.LocalLocalization +import androidx.compose.ui.unit.dp +import com.konyaco.fluent.FluentTheme +import com.konyaco.fluent.LocalContentColor +import com.konyaco.fluent.icons.Icons +import com.konyaco.fluent.icons.regular.Copy +import com.konyaco.fluent.icons.regular.Cut +import com.konyaco.fluent.icons.regular.ClipboardPaste + +internal object FluentContextMenuRepresentation : ContextMenuRepresentation { + @Composable + override fun Representation(state: ContextMenuState, items: () -> List) { + val isOpen = state.status is ContextMenuState.Status.Open + DropdownMenu( + focusable = true, + expanded = isOpen, + onDismissRequest = { + state.status = ContextMenuState.Status.Closed + }, + onKeyEvent = { keyEvent -> + items().firstOrNull { + val result = it is FluentContextMenuItem && + keyEvent.type == KeyEventType.KeyDown && + it.keyData != null && + it.keyData.isAltPressed == keyEvent.isAltPressed && + it.keyData.isCtrlPressed == keyEvent.isCtrlPressed && + it.keyData.isShiftPressed == keyEvent.isShiftPressed && + it.keyData.key == keyEvent.key + if (result) { + it.onClick() + state.status = ContextMenuState.Status.Closed + } + result + } != null + } + ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + items().forEach { + if (it is FluentContextMenuItem) { + DropdownMenuItem( + onClick = { + it.onClick() + state.status = ContextMenuState.Status.Closed + } + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.width(24.dp).fillMaxHeight() + ) { + if (it.glyph != null && LocalFontIconFontFamily.current != null) { + FontIcon(it.glyph) + } else if (it.vector != null) { + Icon(it.vector, it.label, modifier = Modifier.size(20.dp)) + } + } + Text(it.label, modifier = Modifier.weight(1f)) + it.keyData?.let { keyData -> + val keyString = remember(keyData) { + buildString { + if (keyData.isAltPressed) { + append("Alt+") + } + if (keyData.isCtrlPressed) { + append("Ctrl+") + } + if (keyData.isShiftPressed) { + append("Shift+") + } + append(keyData.key.toString().removePrefix("Key: ")) + } + } + Text( + text = keyString, + color = LocalContentColor.current.copy(0.6f), + style = FluentTheme.typography.caption, + modifier = Modifier.padding(start = 24.dp, end = 8.dp) + ) + } + } + } else { + DropdownMenuItem( + onClick = { + it.onClick() + state.status = ContextMenuState.Status.Closed + }, + ) { + Spacer(Modifier.width(28.dp)) + Text(it.label) + } + } + } + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +internal object FluentTextContextMenu : TextContextMenu { + + @OptIn(ExperimentalComposeUiApi::class) + @Composable + override fun Area( + textManager: TextContextMenu.TextManager, + state: ContextMenuState, + content: @Composable () -> Unit + ) { + val localization = LocalLocalization.current + val items = { + listOfNotNull( + textManager.cut?.let { + FluentContextMenuItem( + label = localization.cut, + onClick = it, + glyph = '\uE8C6', + vector = Icons.Default.Cut, + keyData = FluentContextMenuItem.KeyData(Key.X, isCtrlPressed = true) + ) + }, + textManager.copy?.let { + FluentContextMenuItem( + label = localization.copy, + onClick = it, + glyph = '\uE8C8', + vector = Icons.Default.Copy, + keyData = FluentContextMenuItem.KeyData(Key.C, isCtrlPressed = true) + ) + }, + textManager.paste?.let { + FluentContextMenuItem( + label = localization.paste, + onClick = it, + glyph = '\uE77F', + vector = Icons.Default.ClipboardPaste, + keyData = FluentContextMenuItem.KeyData(Key.V, isCtrlPressed = true) + ) + }, + textManager.selectAll?.let { + FluentContextMenuItem( + label = localization.selectAll, + onClick = it, + keyData = FluentContextMenuItem.KeyData(Key.A, isCtrlPressed = true), + ) + }, + ) + } + ContextMenuArea(items, state, content = content) + } +} + +class FluentContextMenuItem( + label: String, + onClick: () -> Unit, + val vector: ImageVector? = null, + val keyData: KeyData? = null, + val glyph: Char? = null +) : ContextMenuItem(label, onClick) { + data class KeyData( + val key: Key, + val isAltPressed: Boolean = false, + val isCtrlPressed: Boolean = false, + val isShiftPressed: Boolean = false + ) +} \ No newline at end of file diff --git a/fluent/src/jvmMain/kotlin/com/konyaco/fluent/component/FontIcon.desktop.kt b/fluent/src/jvmMain/kotlin/com/konyaco/fluent/component/FontIcon.desktop.kt new file mode 100644 index 00000000..e8a7ee68 --- /dev/null +++ b/fluent/src/jvmMain/kotlin/com/konyaco/fluent/component/FontIcon.desktop.kt @@ -0,0 +1,20 @@ +package com.konyaco.fluent.component + +import androidx.compose.runtime.* +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.toFontFamily +import androidx.compose.ui.text.platform.Font +import org.jetbrains.skiko.AwtFontManager + +@Composable +actual fun ProvideFontIcon(content: @Composable () -> Unit) { + var fontFamily: FontFamily? by remember { mutableStateOf(null) } + LaunchedEffect(Unit) { + fontFamily = AwtFontManager.DEFAULT.findFontFamilyFile("Segoe Fluent Icons")?.let { Font(it).toFontFamily() } + } + + CompositionLocalProvider( + LocalFontIconFontFamily provides fontFamily, + content = content + ) +} \ No newline at end of file