Skip to content

Fluent context menu #22

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.konyaco.fluent

import androidx.compose.runtime.Composable

@Composable
actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) {
content()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.konyaco.fluent.component

import androidx.compose.runtime.Composable

@Composable
actual fun ProvideFontIcon(content: @Composable () -> Unit) {
content()
}
16 changes: 8 additions & 8 deletions fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -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 {
Expand All @@ -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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.konyaco.fluent

import androidx.compose.runtime.Composable

@Composable
expect fun PlatformCompositionLocalProvider(content: @Composable () -> Unit)

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
) {
Expand All @@ -46,7 +51,10 @@ fun DropdownMenu(
val popupPositionProvider = DropdownMenuPositionProvider(density)

Popup(
focusable = focusable,
onDismissRequest = onDismissRequest,
onKeyEvent = onKeyEvent,
onPreviewKeyEvent = onPreviewKeyEvent,
popupPositionProvider = popupPositionProvider,
) {
DropdownMenuContent(
Expand Down Expand Up @@ -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)
})
}
Original file line number Diff line number Diff line change
@@ -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<FontFamily?> { error("No Font provide for load font icon") }
Original file line number Diff line number Diff line change
@@ -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()
}
}

Original file line number Diff line number Diff line change
@@ -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<ContextMenuItem>) {
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
)
}
Original file line number Diff line number Diff line change
@@ -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
)
}