Skip to content

Commit 955ec1a

Browse files
authored
Merge pull request #403 from kdroidFilter/feat/composable-tray-menu-dsl
feat(menu): add @composable tray menu DSL
2 parents 6e15bd2 + 70f22a6 commit 955ec1a

6 files changed

Lines changed: 830 additions & 124 deletions

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.kdroid.composetray.demo
2+
3+
import androidx.compose.runtime.getValue
4+
import androidx.compose.runtime.mutableStateOf
5+
import androidx.compose.runtime.remember
6+
import androidx.compose.runtime.setValue
7+
import androidx.compose.ui.window.Window
8+
import androidx.compose.ui.window.application
9+
import com.kdroid.composetray.tray.api.Tray
10+
import com.kdroid.composetray.utils.SingleInstanceManager
11+
import composenativetray.demo.generated.resources.Res
12+
import composenativetray.demo.generated.resources.icon
13+
import composenativetray.demo.generated.resources.icon2
14+
import org.jetbrains.compose.resources.painterResource
15+
16+
/**
17+
* Showcases the @Composable menu DSL. No need to hoist `painterResource(...)` above
18+
* `application { … }` — every menu/submenu lambda is composable.
19+
*/
20+
fun main() = application {
21+
var isWindowVisible by remember { mutableStateOf(true) }
22+
var enableHeavyMode by remember { mutableStateOf(false) }
23+
24+
val isSingleInstance = SingleInstanceManager.isSingleInstance(onRestoreRequest = {
25+
isWindowVisible = true
26+
})
27+
if (!isSingleInstance) {
28+
exitApplication()
29+
return@application
30+
}
31+
32+
Tray(
33+
icon = painterResource(Res.drawable.icon),
34+
tooltip = "Composable Menu Demo",
35+
primaryAction = { isWindowVisible = true },
36+
menuContent = {
37+
Item(label = "Open", icon = painterResource(Res.drawable.icon)) {
38+
isWindowVisible = true
39+
}
40+
41+
SubMenu(
42+
label = "Advanced",
43+
icon = painterResource(Res.drawable.icon2),
44+
) {
45+
Item(label = "Reload", icon = painterResource(Res.drawable.icon)) {
46+
println("Reload")
47+
}
48+
Item(label = "Reset", icon = painterResource(Res.drawable.icon2)) {
49+
println("Reset")
50+
}
51+
}
52+
53+
Divider()
54+
55+
CheckableItem(
56+
label = "Heavy mode",
57+
icon = painterResource(Res.drawable.icon2),
58+
checked = enableHeavyMode,
59+
onCheckedChange = { enableHeavyMode = it },
60+
)
61+
62+
Divider()
63+
64+
Item(label = "Exit", icon = painterResource(Res.drawable.icon2)) {
65+
dispose()
66+
exitApplication()
67+
}
68+
},
69+
)
70+
71+
Window(
72+
onCloseRequest = { isWindowVisible = false },
73+
title = "Composable Menu Demo",
74+
visible = isWindowVisible,
75+
icon = painterResource(Res.drawable.icon),
76+
) {
77+
window.toFront()
78+
}
79+
}

demo/src/jvmMain/kotlin/com/kdroid/composetray/demo/PainterResourceWorkaroundDemo.kt

Lines changed: 0 additions & 124 deletions
This file was deleted.
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package com.kdroid.composetray.menu.api
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.ui.graphics.Color
5+
import androidx.compose.ui.graphics.painter.Painter
6+
import androidx.compose.ui.graphics.vector.ImageVector
7+
import com.kdroid.composetray.utils.IconRenderProperties
8+
import org.jetbrains.compose.resources.DrawableResource
9+
10+
/**
11+
* Composable-aware DSL for declaring tray menus.
12+
*
13+
* Unlike [TrayMenuBuilder] — whose submenu lambdas are plain Kotlin lambdas executed by the
14+
* native impls outside of any composition — every method here is `@Composable`. You can call
15+
* `painterResource(...)`, read state, etc., directly inside menu and submenu bodies without
16+
* hoisting:
17+
*
18+
* ```
19+
* Tray(icon = Res.drawable.icon, tooltip = "App") {
20+
* SubMenu(label = "Advanced", icon = painterResource(Res.drawable.advanced)) {
21+
* Item(label = "Reload", icon = painterResource(Res.drawable.reload)) { ... }
22+
* }
23+
* }
24+
* ```
25+
*
26+
* Internally, calls are recorded during composition and replayed into the [TrayMenuBuilder]
27+
* consumed by the native menu impls. Recomposition triggered by state reads inside the lambda
28+
* transparently rebuilds the menu.
29+
*/
30+
interface ComposableTrayMenuScope {
31+
@Composable
32+
fun Item(
33+
label: String,
34+
isEnabled: Boolean = true,
35+
shortcut: KeyShortcut? = null,
36+
onClick: () -> Unit = {},
37+
)
38+
39+
@Composable
40+
fun Item(
41+
label: String,
42+
icon: Painter,
43+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
44+
isEnabled: Boolean = true,
45+
shortcut: KeyShortcut? = null,
46+
onClick: () -> Unit = {},
47+
)
48+
49+
@Composable
50+
fun Item(
51+
label: String,
52+
icon: ImageVector,
53+
iconTint: Color? = null,
54+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
55+
isEnabled: Boolean = true,
56+
shortcut: KeyShortcut? = null,
57+
onClick: () -> Unit = {},
58+
)
59+
60+
@Composable
61+
fun CheckableItem(
62+
label: String,
63+
checked: Boolean,
64+
onCheckedChange: (Boolean) -> Unit,
65+
isEnabled: Boolean = true,
66+
shortcut: KeyShortcut? = null,
67+
)
68+
69+
@Composable
70+
fun CheckableItem(
71+
label: String,
72+
icon: Painter,
73+
checked: Boolean,
74+
onCheckedChange: (Boolean) -> Unit,
75+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
76+
isEnabled: Boolean = true,
77+
shortcut: KeyShortcut? = null,
78+
)
79+
80+
@Composable
81+
fun CheckableItem(
82+
label: String,
83+
icon: ImageVector,
84+
checked: Boolean,
85+
onCheckedChange: (Boolean) -> Unit,
86+
iconTint: Color? = null,
87+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
88+
isEnabled: Boolean = true,
89+
shortcut: KeyShortcut? = null,
90+
)
91+
92+
@Composable
93+
fun SubMenu(
94+
label: String,
95+
isEnabled: Boolean = true,
96+
submenuContent: @Composable ComposableTrayMenuScope.() -> Unit,
97+
)
98+
99+
@Composable
100+
fun SubMenu(
101+
label: String,
102+
icon: Painter,
103+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
104+
isEnabled: Boolean = true,
105+
submenuContent: @Composable ComposableTrayMenuScope.() -> Unit,
106+
)
107+
108+
@Composable
109+
fun SubMenu(
110+
label: String,
111+
icon: ImageVector,
112+
iconTint: Color? = null,
113+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
114+
isEnabled: Boolean = true,
115+
submenuContent: @Composable ComposableTrayMenuScope.() -> Unit,
116+
)
117+
118+
@Composable
119+
fun Item(
120+
label: String,
121+
icon: DrawableResource,
122+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
123+
isEnabled: Boolean = true,
124+
shortcut: KeyShortcut? = null,
125+
onClick: () -> Unit = {},
126+
)
127+
128+
@Composable
129+
fun CheckableItem(
130+
label: String,
131+
icon: DrawableResource,
132+
checked: Boolean,
133+
onCheckedChange: (Boolean) -> Unit,
134+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
135+
isEnabled: Boolean = true,
136+
shortcut: KeyShortcut? = null,
137+
)
138+
139+
@Composable
140+
fun SubMenu(
141+
label: String,
142+
icon: DrawableResource,
143+
iconRenderProperties: IconRenderProperties = IconRenderProperties.forMenuItem(),
144+
isEnabled: Boolean = true,
145+
submenuContent: @Composable ComposableTrayMenuScope.() -> Unit,
146+
)
147+
148+
@Composable
149+
fun Divider()
150+
151+
/**
152+
* Backward-compatibility shim. Under the composable DSL, the tray lifecycle is owned by the
153+
* surrounding `DisposableEffect` in [Tray], so this is a no-op. Existing menus that call
154+
* `dispose()` before `exitApplication()` keep compiling without behavior change.
155+
*/
156+
fun dispose() {
157+
// no-op: lifecycle managed by Tray composable
158+
}
159+
160+
@Deprecated(
161+
message = "Use CheckableItem with separate checked and onCheckedChange parameters",
162+
replaceWith = ReplaceWith("CheckableItem(label, checked, onToggle, isEnabled)"),
163+
)
164+
@Composable
165+
fun CheckableItem(
166+
label: String,
167+
checked: Boolean = false,
168+
isEnabled: Boolean = true,
169+
onToggle: (Boolean) -> Unit,
170+
) {
171+
CheckableItem(label, checked, onToggle, isEnabled, null)
172+
}
173+
}

0 commit comments

Comments
 (0)