Skip to content

Commit 01b6209

Browse files
authored
Merge pull request #22 from Konyaco/fluent_context_menu
Fluent context menu
2 parents 555c60e + 2f944de commit 01b6209

File tree

9 files changed

+301
-11
lines changed

9 files changed

+301
-11
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.konyaco.fluent
2+
3+
import androidx.compose.runtime.Composable
4+
5+
@Composable
6+
actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) {
7+
content()
8+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.konyaco.fluent.component
2+
3+
import androidx.compose.runtime.Composable
4+
5+
@Composable
6+
actual fun ProvideFontIcon(content: @Composable () -> Unit) {
7+
content()
8+
}

fluent/src/commonMain/kotlin/com/konyaco/fluent/FluentTheme.kt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
package com.konyaco.fluent
22

3-
import androidx.compose.runtime.Composable
4-
import androidx.compose.runtime.CompositionLocalProvider
5-
import androidx.compose.runtime.ReadOnlyComposable
6-
import androidx.compose.runtime.staticCompositionLocalOf
3+
import androidx.compose.runtime.*
74
import androidx.compose.ui.graphics.Color
85
import androidx.compose.ui.text.font.FontFamily
6+
import com.konyaco.fluent.component.ProvideFontIcon
97

108
@Composable
119
fun FluentTheme(
@@ -27,9 +25,12 @@ fun FluentTheme(
2725
titleLarge = typography.titleLarge.copy(fontFamily = defaultFontFamily),
2826
display = typography.display.copy(fontFamily = defaultFontFamily),
2927
)
30-
} ?: typography),
31-
content = content
32-
)
28+
} ?: typography)
29+
) {
30+
ProvideFontIcon {
31+
PlatformCompositionLocalProvider(content)
32+
}
33+
}
3334
}
3435

3536
object FluentTheme {
@@ -45,6 +46,5 @@ object FluentTheme {
4546

4647
internal val LocalColors = staticCompositionLocalOf { lightColors() }
4748

48-
4949
fun lightColors(accent: Color = Color(0xFF0078D4)): Colors = Colors(generateShades(accent), false)
5050
fun darkColors(accent: Color = Color(0xFF0078D4)): Colors = Colors(generateShades(accent), true)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.konyaco.fluent
2+
3+
import androidx.compose.runtime.Composable
4+
5+
@Composable
6+
expect fun PlatformCompositionLocalProvider(content: @Composable () -> Unit)
7+

fluent/src/commonMain/kotlin/com/konyaco/fluent/component/Dropdown.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ import androidx.compose.runtime.Composable
1414
import androidx.compose.runtime.MutableState
1515
import androidx.compose.runtime.mutableStateOf
1616
import androidx.compose.runtime.remember
17+
import androidx.compose.ui.Alignment
1718
import androidx.compose.ui.Modifier
1819
import androidx.compose.ui.draw.clip
1920
import androidx.compose.ui.draw.shadow
2021
import androidx.compose.ui.graphics.TransformOrigin
22+
import androidx.compose.ui.input.key.KeyEvent
2123
import androidx.compose.ui.platform.LocalDensity
2224
import androidx.compose.ui.unit.*
2325
import androidx.compose.ui.window.Popup
@@ -33,6 +35,9 @@ fun DropdownMenu(
3335
expanded: Boolean,
3436
onDismissRequest: () -> Unit,
3537
modifier: Modifier = Modifier,
38+
focusable: Boolean = false,
39+
onPreviewKeyEvent: ((KeyEvent) -> Boolean) = { false },
40+
onKeyEvent: ((KeyEvent) -> Boolean) = { false },
3641
offset: DpOffset = DpOffset(0.dp, 0.dp), // TODO: Offset
3742
content: @Composable ColumnScope.() -> Unit
3843
) {
@@ -46,7 +51,10 @@ fun DropdownMenu(
4651
val popupPositionProvider = DropdownMenuPositionProvider(density)
4752

4853
Popup(
54+
focusable = focusable,
4955
onDismissRequest = onDismissRequest,
56+
onKeyEvent = onKeyEvent,
57+
onPreviewKeyEvent = onPreviewKeyEvent,
5058
popupPositionProvider = popupPositionProvider,
5159
) {
5260
DropdownMenuContent(
@@ -121,8 +129,8 @@ internal fun DropdownMenuContent(
121129
}
122130

123131
@Composable
124-
fun DropdownMenuItem(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
125-
SubtleButton(modifier = Modifier.defaultMinSize(minWidth = 100.dp), onClick = onClick, iconOnly = true, content = {
126-
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), content = content)
132+
fun DropdownMenuItem(onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) {
133+
SubtleButton(modifier = modifier.defaultMinSize(minWidth = 100.dp), onClick = onClick, iconOnly = true, content = {
134+
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), content = content)
127135
})
128136
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.konyaco.fluent.component
2+
3+
import androidx.compose.foundation.layout.height
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.staticCompositionLocalOf
6+
import androidx.compose.ui.Modifier
7+
import androidx.compose.ui.platform.LocalDensity
8+
import androidx.compose.ui.text.font.FontFamily
9+
import androidx.compose.ui.unit.TextUnit
10+
import androidx.compose.ui.unit.sp
11+
12+
@Composable
13+
fun FontIcon(
14+
glyph: Char,
15+
modifier: Modifier = Modifier,
16+
iconSize: TextUnit = FontIconDefaults.fontSizeStandard,
17+
fallback: (@Composable () -> Unit)? = null,
18+
) {
19+
if (LocalFontIconFontFamily.current != null || fallback == null) {
20+
Text(
21+
text = glyph.toString(),
22+
fontFamily = LocalFontIconFontFamily.current,
23+
fontSize = iconSize,
24+
modifier = Modifier.then(modifier)
25+
.height(with(LocalDensity.current) { iconSize.toDp() })
26+
)
27+
} else {
28+
fallback()
29+
}
30+
}
31+
32+
object FontIconDefaults {
33+
val fontSizeStandard = 16.sp
34+
val fontSizeSmall = 12.sp
35+
}
36+
37+
@Composable
38+
expect fun ProvideFontIcon(
39+
content: @Composable () -> Unit
40+
)
41+
42+
val LocalFontIconFontFamily =
43+
staticCompositionLocalOf<FontFamily?> { error("No Font provide for load font icon") }
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.konyaco.fluent
2+
3+
import androidx.compose.foundation.ExperimentalFoundationApi
4+
import androidx.compose.foundation.LocalContextMenuRepresentation
5+
import androidx.compose.foundation.text.LocalTextContextMenu
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.CompositionLocalProvider
8+
import com.konyaco.fluent.component.FluentContextMenuRepresentation
9+
import com.konyaco.fluent.component.FluentTextContextMenu
10+
11+
@OptIn(ExperimentalFoundationApi::class)
12+
@Composable
13+
actual fun PlatformCompositionLocalProvider(content: @Composable () -> Unit) {
14+
CompositionLocalProvider(
15+
LocalTextContextMenu provides FluentTextContextMenu,
16+
LocalContextMenuRepresentation provides FluentContextMenuRepresentation
17+
) {
18+
content()
19+
}
20+
}
21+
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package com.konyaco.fluent.component
2+
3+
import androidx.compose.foundation.*
4+
import androidx.compose.foundation.layout.*
5+
import androidx.compose.foundation.text.TextContextMenu
6+
import androidx.compose.runtime.Composable
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.ui.Alignment
9+
import androidx.compose.ui.ExperimentalComposeUiApi
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.graphics.vector.ImageVector
12+
import androidx.compose.ui.input.key.*
13+
import androidx.compose.ui.platform.LocalLocalization
14+
import androidx.compose.ui.unit.dp
15+
import com.konyaco.fluent.FluentTheme
16+
import com.konyaco.fluent.LocalContentColor
17+
import com.konyaco.fluent.icons.Icons
18+
import com.konyaco.fluent.icons.regular.Copy
19+
import com.konyaco.fluent.icons.regular.Cut
20+
import com.konyaco.fluent.icons.regular.ClipboardPaste
21+
22+
internal object FluentContextMenuRepresentation : ContextMenuRepresentation {
23+
@Composable
24+
override fun Representation(state: ContextMenuState, items: () -> List<ContextMenuItem>) {
25+
val isOpen = state.status is ContextMenuState.Status.Open
26+
DropdownMenu(
27+
focusable = true,
28+
expanded = isOpen,
29+
onDismissRequest = {
30+
state.status = ContextMenuState.Status.Closed
31+
},
32+
onKeyEvent = { keyEvent ->
33+
items().firstOrNull {
34+
val result = it is FluentContextMenuItem &&
35+
keyEvent.type == KeyEventType.KeyDown &&
36+
it.keyData != null &&
37+
it.keyData.isAltPressed == keyEvent.isAltPressed &&
38+
it.keyData.isCtrlPressed == keyEvent.isCtrlPressed &&
39+
it.keyData.isShiftPressed == keyEvent.isShiftPressed &&
40+
it.keyData.key == keyEvent.key
41+
if (result) {
42+
it.onClick()
43+
state.status = ContextMenuState.Status.Closed
44+
}
45+
result
46+
} != null
47+
}
48+
) {
49+
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
50+
items().forEach {
51+
if (it is FluentContextMenuItem) {
52+
DropdownMenuItem(
53+
onClick = {
54+
it.onClick()
55+
state.status = ContextMenuState.Status.Closed
56+
}
57+
) {
58+
Box(
59+
contentAlignment = Alignment.Center,
60+
modifier = Modifier.width(24.dp).fillMaxHeight()
61+
) {
62+
if (it.glyph != null && LocalFontIconFontFamily.current != null) {
63+
FontIcon(it.glyph)
64+
} else if (it.vector != null) {
65+
Icon(it.vector, it.label, modifier = Modifier.size(20.dp))
66+
}
67+
}
68+
Text(it.label, modifier = Modifier.weight(1f))
69+
it.keyData?.let { keyData ->
70+
val keyString = remember(keyData) {
71+
buildString {
72+
if (keyData.isAltPressed) {
73+
append("Alt+")
74+
}
75+
if (keyData.isCtrlPressed) {
76+
append("Ctrl+")
77+
}
78+
if (keyData.isShiftPressed) {
79+
append("Shift+")
80+
}
81+
append(keyData.key.toString().removePrefix("Key: "))
82+
}
83+
}
84+
Text(
85+
text = keyString,
86+
color = LocalContentColor.current.copy(0.6f),
87+
style = FluentTheme.typography.caption,
88+
modifier = Modifier.padding(start = 24.dp, end = 8.dp)
89+
)
90+
}
91+
}
92+
} else {
93+
DropdownMenuItem(
94+
onClick = {
95+
it.onClick()
96+
state.status = ContextMenuState.Status.Closed
97+
},
98+
) {
99+
Spacer(Modifier.width(28.dp))
100+
Text(it.label)
101+
}
102+
}
103+
}
104+
}
105+
}
106+
}
107+
}
108+
109+
@OptIn(ExperimentalFoundationApi::class)
110+
internal object FluentTextContextMenu : TextContextMenu {
111+
112+
@OptIn(ExperimentalComposeUiApi::class)
113+
@Composable
114+
override fun Area(
115+
textManager: TextContextMenu.TextManager,
116+
state: ContextMenuState,
117+
content: @Composable () -> Unit
118+
) {
119+
val localization = LocalLocalization.current
120+
val items = {
121+
listOfNotNull(
122+
textManager.cut?.let {
123+
FluentContextMenuItem(
124+
label = localization.cut,
125+
onClick = it,
126+
glyph = '\uE8C6',
127+
vector = Icons.Default.Cut,
128+
keyData = FluentContextMenuItem.KeyData(Key.X, isCtrlPressed = true)
129+
)
130+
},
131+
textManager.copy?.let {
132+
FluentContextMenuItem(
133+
label = localization.copy,
134+
onClick = it,
135+
glyph = '\uE8C8',
136+
vector = Icons.Default.Copy,
137+
keyData = FluentContextMenuItem.KeyData(Key.C, isCtrlPressed = true)
138+
)
139+
},
140+
textManager.paste?.let {
141+
FluentContextMenuItem(
142+
label = localization.paste,
143+
onClick = it,
144+
glyph = '\uE77F',
145+
vector = Icons.Default.ClipboardPaste,
146+
keyData = FluentContextMenuItem.KeyData(Key.V, isCtrlPressed = true)
147+
)
148+
},
149+
textManager.selectAll?.let {
150+
FluentContextMenuItem(
151+
label = localization.selectAll,
152+
onClick = it,
153+
keyData = FluentContextMenuItem.KeyData(Key.A, isCtrlPressed = true),
154+
)
155+
},
156+
)
157+
}
158+
ContextMenuArea(items, state, content = content)
159+
}
160+
}
161+
162+
class FluentContextMenuItem(
163+
label: String,
164+
onClick: () -> Unit,
165+
val vector: ImageVector? = null,
166+
val keyData: KeyData? = null,
167+
val glyph: Char? = null
168+
) : ContextMenuItem(label, onClick) {
169+
data class KeyData(
170+
val key: Key,
171+
val isAltPressed: Boolean = false,
172+
val isCtrlPressed: Boolean = false,
173+
val isShiftPressed: Boolean = false
174+
)
175+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.konyaco.fluent.component
2+
3+
import androidx.compose.runtime.*
4+
import androidx.compose.ui.text.font.FontFamily
5+
import androidx.compose.ui.text.font.toFontFamily
6+
import androidx.compose.ui.text.platform.Font
7+
import org.jetbrains.skiko.AwtFontManager
8+
9+
@Composable
10+
actual fun ProvideFontIcon(content: @Composable () -> Unit) {
11+
var fontFamily: FontFamily? by remember { mutableStateOf(null) }
12+
LaunchedEffect(Unit) {
13+
fontFamily = AwtFontManager.DEFAULT.findFontFamilyFile("Segoe Fluent Icons")?.let { Font(it).toFontFamily() }
14+
}
15+
16+
CompositionLocalProvider(
17+
LocalFontIconFontFamily provides fontFamily,
18+
content = content
19+
)
20+
}

0 commit comments

Comments
 (0)