├── .github
└── workflows
│ ├── detekt.yml
│ ├── gh-pages-test.yml
│ ├── gh-pages.yml
│ └── release.yml
├── .gitignore
├── .run
└── desktop.run.xml
├── LICENSE
├── README.md
├── config
└── detekt
│ └── detekt.yml
├── docs
├── README.md
├── gtk.md
└── screenshots
│ ├── README.md
│ ├── basic_window.png
│ ├── dynamic_window.gif
│ ├── gio_resources.png
│ ├── list_view.png
│ ├── tags.gif
│ ├── uppercase_entry.png
│ └── welcome.png
├── examples
├── build.gradle.kts
└── src
│ └── main
│ ├── gresources
│ ├── README.md
│ ├── icons
│ │ ├── cat-symbolic.svg
│ │ └── heart-filled-symbolic.svg
│ ├── images
│ │ └── lulu.jpg
│ ├── resources.gresource.xml
│ └── style.css
│ ├── kotlin
│ ├── 1_BasicWindow.kt
│ ├── 2_DynamicWindow.kt
│ ├── 3_Uppercase_Entry.kt
│ ├── 4_DynamicTags.kt
│ ├── 5_ListView.kt
│ └── 6_GIOResources.kt
│ └── resources
│ └── .gitignore
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── lib
├── build.gradle.kts
└── src
│ ├── main
│ └── kotlin
│ │ └── io
│ │ └── github
│ │ └── compose4gtk
│ │ ├── Application.kt
│ │ ├── GioResource.kt
│ │ ├── GtkApplier.kt
│ │ ├── GtkComposeNode.kt
│ │ ├── GtkComposition.kt
│ │ ├── GtkDispatcher.kt
│ │ ├── adw
│ │ ├── Adw.kt
│ │ └── components
│ │ │ ├── ApplicationWindow.kt
│ │ │ ├── Avatar.kt
│ │ │ ├── Banner.kt
│ │ │ ├── BottomSheet.kt
│ │ │ ├── Carousel.kt
│ │ │ ├── Clamp.kt
│ │ │ ├── Dialog.kt
│ │ │ ├── HeaderBar.kt
│ │ │ ├── OverlaySplitView.kt
│ │ │ ├── StatusPage.kt
│ │ │ ├── ToastOverlay.kt
│ │ │ ├── ToolbarView.kt
│ │ │ └── WindowTitle.kt
│ │ ├── gtk
│ │ ├── Gtk.kt
│ │ ├── ImageSource.kt
│ │ └── components
│ │ │ ├── Box.kt
│ │ │ ├── Button.kt
│ │ │ ├── Calendar.kt
│ │ │ ├── CenterBox.kt
│ │ │ ├── CheckButton.kt
│ │ │ ├── Entry.kt
│ │ │ ├── FlowBox.kt
│ │ │ ├── Frame.kt
│ │ │ ├── GridView.kt
│ │ │ ├── GtkApplicationWindow.kt
│ │ │ ├── Image.kt
│ │ │ ├── Label.kt
│ │ │ ├── ListBox.kt
│ │ │ ├── ListView.kt
│ │ │ ├── Overlay.kt
│ │ │ ├── Picture.kt
│ │ │ ├── Popover.kt
│ │ │ ├── Revealer.kt
│ │ │ ├── ScrolledWindow.kt
│ │ │ ├── Separator.kt
│ │ │ ├── Spinner.kt
│ │ │ ├── Switch.kt
│ │ │ └── WindowControls.kt
│ │ ├── modifier
│ │ ├── Align.kt
│ │ ├── Click.kt
│ │ ├── Css.kt
│ │ ├── Expand.kt
│ │ ├── Hover.kt
│ │ ├── Margin.kt
│ │ ├── Modifier.kt
│ │ ├── Sensitive.kt
│ │ ├── Size.kt
│ │ └── Tooltip.kt
│ │ └── shared
│ │ └── components
│ │ └── ApplicationWindow.kt
│ └── test
│ ├── gresources
│ ├── README.md
│ ├── icons
│ │ ├── cat-symbolic.svg
│ │ └── heart-filled-symbolic.svg
│ ├── images
│ │ └── lulu.jpg
│ ├── resources.gresource.xml
│ └── style.css
│ ├── kotlin
│ ├── Avatar.kt
│ ├── Banner.kt
│ ├── BottomSheet.kt
│ ├── Buttons.kt
│ ├── Calendar.kt
│ ├── Carousel.kt
│ ├── CheckButton.kt
│ ├── CheckButtonGroup.kt
│ ├── Dialogs.kt
│ ├── FancyWindow.kt
│ ├── GridView.kt
│ ├── Label.kt
│ ├── LinkButton.kt
│ ├── ListView.kt
│ ├── NestedWindow.kt
│ ├── Overlay.kt
│ ├── Resources.kt
│ ├── StatusPage.kt
│ ├── Switch.kt
│ ├── Toggle.kt
│ ├── ToolbarView.kt
│ ├── Uppercase.kt
│ └── Welcome.kt
│ └── resources
│ └── .gitignore
└── settings.gradle.kts
/.github/workflows/detekt.yml:
--------------------------------------------------------------------------------
1 | name: Detekt
2 |
3 | on:
4 | push:
5 | branches: [ "main" ]
6 | pull_request:
7 | branches: [ "main" ]
8 |
9 | jobs:
10 | build:
11 | name: Detekt
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: set up JDK 22
16 | uses: actions/setup-java@v4
17 | with:
18 | java-version: '22'
19 | distribution: 'temurin'
20 | cache: gradle
21 |
22 | - name: Run detekt
23 | run: ./gradlew detekt
24 |
25 | - name: Archive detekt results
26 | if: ${{ always() }}
27 | uses: actions/upload-artifact@v4
28 | with:
29 | path: lib/build/reports/detekt/*
30 |
31 | - name: Adding markdown
32 | if: ${{ always() }}
33 | run: cat lib/build/reports/detekt/*.md >> $GITHUB_STEP_SUMMARY
34 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages-test.yml:
--------------------------------------------------------------------------------
1 | name: Build and archive Dokka documentation
2 |
3 | on:
4 | pull_request:
5 | branches: [ "main" ]
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: set up JDK 22
13 | uses: actions/setup-java@v4
14 | with:
15 | java-version: '22'
16 | distribution: 'temurin'
17 | cache: gradle
18 |
19 | - name: Build Dokka HTML
20 | run: ./gradlew dokkaGeneratePublicationHtml
21 |
22 | - name: Archive Dokka results
23 | if: ${{ always() }}
24 | uses: actions/upload-artifact@v4
25 | with:
26 | path: lib/build/dokka/html/*
27 |
--------------------------------------------------------------------------------
/.github/workflows/gh-pages.yml:
--------------------------------------------------------------------------------
1 | name: Deploy Dokka Documentation to GitHub Pages
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v4
13 | - name: set up JDK 22
14 | uses: actions/setup-java@v4
15 | with:
16 | java-version: '22'
17 | distribution: 'temurin'
18 | cache: gradle
19 |
20 | - name: Build Dokka HTML
21 | run: ./gradlew dokkaGeneratePublicationHtml
22 |
23 | - name: Setup Pages
24 | uses: actions/configure-pages@v5
25 |
26 | - name: Upload artifact
27 | uses: actions/upload-pages-artifact@v3
28 | with:
29 | path: lib/build/dokka/html
30 |
31 | deploy:
32 | permissions:
33 | contents: read
34 | pages: write
35 | id-token: write
36 | needs: build
37 | runs-on: ubuntu-latest
38 | environment:
39 | name: github-pages
40 | url: ${{ steps.deployment.outputs.page_url }}
41 | steps:
42 | - name: Deploy to GitHub Pages
43 | id: deployment
44 | uses: actions/deploy-pages@v4
45 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Publish to Maven Central
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*"
7 |
8 | jobs:
9 | build:
10 | permissions: write-all
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v4
15 | - name: Set up JDK 22
16 | uses: actions/setup-java@v4
17 | with:
18 | java-version: '22'
19 | distribution: 'temurin'
20 | - name: Setup Gradle
21 | uses: gradle/actions/setup-gradle@v4
22 | - name: Release
23 | run: |
24 | ./gradlew jreleaserFullRelease
25 | env:
26 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }}
27 | JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_TOKEN }}
28 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
29 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
30 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
31 | JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | build/
3 | !gradle/wrapper/gradle-wrapper.jar
4 | !**/src/main/**/build/
5 | !**/src/test/**/build/
6 |
7 | ### JVM crash file
8 | hs_err_pid*.log
9 |
10 | ### IntelliJ IDEA ###
11 | .idea
12 |
13 | ### Eclipse ###
14 | .apt_generated
15 | .classpath
16 | .factorypath
17 | .project
18 | .settings
19 | .springBeans
20 | .sts4-cache
21 | bin/
22 | !**/src/main/**/bin/
23 | !**/src/test/**/bin/
24 |
25 | ### NetBeans ###
26 | /nbproject/private/
27 | /nbbuild/
28 | /dist/
29 | /nbdist/
30 | /.nb-gradle/
31 |
32 | ### VS Code ###
33 | .vscode/
34 |
35 | ### Mac OS ###
36 | .DS_Store
--------------------------------------------------------------------------------
/.run/desktop.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
16 |
17 |
18 | true
19 |
20 |
21 |
--------------------------------------------------------------------------------
/config/detekt/detekt.yml:
--------------------------------------------------------------------------------
1 | complexity:
2 | # Compose functions tend to be longer and have a lot of parameters
3 | LongMethod:
4 | threshold: 100
5 | LongParameterList:
6 | ignoreDefaultParameters: true
7 |
8 | naming:
9 | # Compose functions are uppercase
10 | FunctionNaming:
11 | functionPattern: "[a-zA-Z0-9]*"
12 | # Ofter files with Composable function start with accessory interfaces
13 | MatchingDeclarationName:
14 | active: false
15 |
16 | style:
17 | # Library is still in alpha, let's allow TODOs
18 | ForbiddenComment:
19 | comments: [ ]
20 | WildcardImport:
21 | excludeImports:
22 | - io.github.compose4gtk.adw.components.*
23 | - io.github.compose4gtk.gtk.components.*
24 |
25 | formatting:
26 | NoWildcardImports:
27 | packagesToUseImportOnDemandProperty:
28 | "io.github.compose4gtk.adw.components.*,io.github.compose4gtk.gtk.components.*"
29 | TrailingCommaOnCallSite:
30 | active: true
31 | TrailingCommaOnDeclarationSite:
32 | active: true
33 | Filename:
34 | # Examples have a number prepended to the file name, to ensure users can read them in order
35 | excludes: '**/examples/**'
36 |
37 | Compose:
38 | active: true
39 | ModifierMissing:
40 | excludes: '**/test/**'
41 | MutableStateParam:
42 | excludes: '**/test/**'
43 | CompositionLocalAllowlist:
44 | active: true
45 | allowedCompositionLocals:
46 | - LocalApplication
47 | - LocalApplicationWindow
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # README
2 |
3 | This folder contains the documentation necessary to generate https://compose4gtk.github.io/compose-4-gtk.
--------------------------------------------------------------------------------
/docs/gtk.md:
--------------------------------------------------------------------------------
1 | # Package io.github.compose4gtk
2 |
3 | The main package of Compose 4 GTK.
4 |
--------------------------------------------------------------------------------
/docs/screenshots/README.md:
--------------------------------------------------------------------------------
1 | ## Screenshots
2 |
3 | These screenshots are taken from a vanilla installation of Fedora.
4 |
5 | To create GIFs, use the following command:
6 |
7 | ```bash
8 | ffmpeg -i input.mp4 -vf "fps=10,palettegen" palette.png
9 | ffmpeg -i input.mp4 -i palette.png -filter_complex "[0:v][1:v]paletteuse" output.gif
10 | ```
--------------------------------------------------------------------------------
/docs/screenshots/basic_window.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/basic_window.png
--------------------------------------------------------------------------------
/docs/screenshots/dynamic_window.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/dynamic_window.gif
--------------------------------------------------------------------------------
/docs/screenshots/gio_resources.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/gio_resources.png
--------------------------------------------------------------------------------
/docs/screenshots/list_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/list_view.png
--------------------------------------------------------------------------------
/docs/screenshots/tags.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/tags.gif
--------------------------------------------------------------------------------
/docs/screenshots/uppercase_entry.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/uppercase_entry.png
--------------------------------------------------------------------------------
/docs/screenshots/welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/docs/screenshots/welcome.png
--------------------------------------------------------------------------------
/examples/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.kotlin.jvm)
3 | alias(libs.plugins.compose.compiler)
4 | alias(libs.plugins.detekt)
5 | application
6 | }
7 |
8 | kotlin {
9 | jvmToolchain(22)
10 | }
11 |
12 | repositories {
13 | mavenCentral()
14 | google()
15 | }
16 |
17 | dependencies {
18 | implementation(project(":lib"))
19 | detektPlugins(libs.detekt.formatting)
20 | detektPlugins(libs.detekt.compose)
21 | }
22 |
23 | tasks.named("assemble") {
24 | dependsOn("compileGResources")
25 | }
26 |
27 | detekt {
28 | config.setFrom(file("../config/detekt/detekt.yml"))
29 | buildUponDefaultConfig = true
30 | }
31 |
32 | tasks.register("compileGResources") {
33 | workingDir("src/main/gresources")
34 | commandLine("glib-compile-resources", "--target=../resources/resources.gresource", "resources.gresource.xml")
35 | }
36 |
37 | tasks.named("processResources") {
38 | dependsOn("compileGResources")
39 | }
40 |
--------------------------------------------------------------------------------
/examples/src/main/gresources/README.md:
--------------------------------------------------------------------------------
1 | # GResources
2 |
3 | The files in this directory are assembled into a `gresource` file, created at build time.
4 |
5 | See the Gradle task `compileTestResources` (declared in `build.gradle.kts`) for more info.
6 |
--------------------------------------------------------------------------------
/examples/src/main/gresources/icons/cat-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/examples/src/main/gresources/icons/heart-filled-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/examples/src/main/gresources/images/lulu.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/examples/src/main/gresources/images/lulu.jpg
--------------------------------------------------------------------------------
/examples/src/main/gresources/resources.gresource.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | style.css
5 | images/lulu.jpg
6 |
7 |
8 | icons/heart-filled-symbolic.svg
9 | icons/cat-symbolic.svg
10 |
11 |
--------------------------------------------------------------------------------
/examples/src/main/gresources/style.css:
--------------------------------------------------------------------------------
1 | .accent-colored {
2 | color: var(--accent-color);
3 | }
4 |
--------------------------------------------------------------------------------
/examples/src/main/kotlin/1_BasicWindow.kt:
--------------------------------------------------------------------------------
1 | import io.github.compose4gtk.adw.adwApplication
2 | import io.github.compose4gtk.adw.components.ApplicationWindow
3 | import io.github.compose4gtk.adw.components.HeaderBar
4 | import io.github.compose4gtk.adw.components.StatusPage
5 | import io.github.compose4gtk.gtk.components.Box
6 | import io.github.compose4gtk.gtk.components.Button
7 | import io.github.compose4gtk.modifier.Modifier
8 | import io.github.compose4gtk.modifier.cssClasses
9 | import org.gnome.gtk.Orientation
10 |
11 | fun main(args: Array) {
12 | adwApplication("my.example.hello-app", args) {
13 | ApplicationWindow("My first window", onClose = ::exitApplication) {
14 | Box(orientation = Orientation.VERTICAL) {
15 | HeaderBar(modifier = Modifier.cssClasses("flat"))
16 | StatusPage(title = "My first component") {
17 | Button("My first button", onClick = { println("Clicked!") })
18 | }
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/src/main/kotlin/2_DynamicWindow.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.getValue
2 | import androidx.compose.runtime.mutableStateOf
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.runtime.setValue
5 | import io.github.compose4gtk.adw.adwApplication
6 | import io.github.compose4gtk.adw.components.ApplicationWindow
7 | import io.github.compose4gtk.adw.components.HeaderBar
8 | import io.github.compose4gtk.gtk.components.Box
9 | import io.github.compose4gtk.gtk.components.Button
10 | import io.github.compose4gtk.gtk.components.Label
11 | import org.gnome.gtk.Orientation
12 |
13 | fun main(args: Array) {
14 | adwApplication("my.example.hello-app", args) {
15 | ApplicationWindow("Test", onClose = ::exitApplication) {
16 | Box(orientation = Orientation.VERTICAL) {
17 | HeaderBar()
18 |
19 | var show by remember { mutableStateOf(false) }
20 | Button(
21 | label = if (show) "Hide" else "Show",
22 | onClick = { show = !show },
23 | )
24 | if (show) {
25 | Label("A random label that can be hidden")
26 | }
27 | }
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/examples/src/main/kotlin/3_Uppercase_Entry.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.getValue
2 | import androidx.compose.runtime.mutableStateOf
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.runtime.setValue
5 | import io.github.compose4gtk.adw.adwApplication
6 | import io.github.compose4gtk.adw.components.ApplicationWindow
7 | import io.github.compose4gtk.adw.components.HeaderBar
8 | import io.github.compose4gtk.gtk.components.Box
9 | import io.github.compose4gtk.gtk.components.Entry
10 | import org.gnome.gtk.Orientation
11 |
12 | fun main(args: Array) {
13 | adwApplication("my.example.hello-app", args) {
14 | ApplicationWindow("Uppercase window", onClose = ::exitApplication) {
15 | Box(orientation = Orientation.VERTICAL) {
16 | HeaderBar()
17 |
18 | var text by remember { mutableStateOf("") }
19 | Entry(
20 | text = text,
21 | placeholderText = "All text will be uppercase",
22 | onTextChange = { text = it.uppercase() },
23 | )
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/examples/src/main/kotlin/4_DynamicTags.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.runtime.getValue
2 | import androidx.compose.runtime.mutableStateOf
3 | import androidx.compose.runtime.remember
4 | import androidx.compose.runtime.setValue
5 | import io.github.compose4gtk.adw.adwApplication
6 | import io.github.compose4gtk.adw.components.ApplicationWindow
7 | import io.github.compose4gtk.adw.components.HeaderBar
8 | import io.github.compose4gtk.adw.components.HorizontalClamp
9 | import io.github.compose4gtk.adw.components.ToastOverlay
10 | import io.github.compose4gtk.gtk.components.Button
11 | import io.github.compose4gtk.gtk.components.Entry
12 | import io.github.compose4gtk.gtk.components.FlowBox
13 | import io.github.compose4gtk.gtk.components.VerticalBox
14 | import io.github.compose4gtk.modifier.Modifier
15 | import io.github.compose4gtk.modifier.margin
16 | import org.gnome.adw.Toast
17 |
18 | fun main(args: Array) {
19 | adwApplication("my.example.hello-app", args) {
20 | ApplicationWindow("Test", onClose = ::exitApplication) {
21 | ToastOverlay {
22 | VerticalBox {
23 | HeaderBar()
24 |
25 | HorizontalClamp {
26 | VerticalBox {
27 | var text by remember { mutableStateOf("") }
28 | Entry(
29 | text = text,
30 | onTextChange = { text = it },
31 | placeholderText = "Inset text here",
32 | modifier = Modifier.margin(margin = 8),
33 | )
34 | FlowBox(homogeneous = true) {
35 | val tokens = text.split(' ').filter { it.isNotBlank() }
36 | for (token in tokens) {
37 | Button(token, modifier = Modifier.margin(margin = 8), onClick = {
38 | dismissAllToasts()
39 | addToast(Toast.builder().setTitle("Clicked on $token").build())
40 | })
41 | }
42 | }
43 | }
44 | }
45 | }
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/examples/src/main/kotlin/5_ListView.kt:
--------------------------------------------------------------------------------
1 | import io.github.compose4gtk.adw.adwApplication
2 | import io.github.compose4gtk.adw.components.ApplicationWindow
3 | import io.github.compose4gtk.adw.components.HeaderBar
4 | import io.github.compose4gtk.gtk.components.Label
5 | import io.github.compose4gtk.gtk.components.ListView
6 | import io.github.compose4gtk.gtk.components.ScrolledWindow
7 | import io.github.compose4gtk.gtk.components.SelectionMode
8 | import io.github.compose4gtk.gtk.components.VerticalBox
9 | import io.github.compose4gtk.modifier.Modifier
10 | import io.github.compose4gtk.modifier.expand
11 |
12 | fun main(args: Array) {
13 | adwApplication("my.example.hello-app", args) {
14 | ApplicationWindow("Test", onClose = ::exitApplication, defaultWidth = 800, defaultHeight = 800) {
15 | VerticalBox {
16 | HeaderBar(title = { Label("ListView with 10 thousand items") })
17 | ScrolledWindow(Modifier.expand()) {
18 | ListView(
19 | items = 10000,
20 | selectionMode = SelectionMode.Multiple,
21 | ) { index ->
22 | Label("Item #$index")
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/examples/src/main/kotlin/6_GIOResources.kt:
--------------------------------------------------------------------------------
1 | import io.github.compose4gtk.adw.adwApplication
2 | import io.github.compose4gtk.adw.components.ApplicationWindow
3 | import io.github.compose4gtk.adw.components.HeaderBar
4 | import io.github.compose4gtk.gtk.ImageSource
5 | import io.github.compose4gtk.gtk.components.IconButton
6 | import io.github.compose4gtk.gtk.components.Picture
7 | import io.github.compose4gtk.gtk.components.VerticalBox
8 | import io.github.compose4gtk.modifier.Modifier
9 | import io.github.compose4gtk.modifier.cssClasses
10 | import io.github.compose4gtk.modifier.expand
11 | import io.github.compose4gtk.useGioResource
12 | import org.gnome.gtk.ContentFit
13 |
14 | fun main(args: Array) {
15 | useGioResource("resources.gresource") {
16 | adwApplication("my.example.hello-app", args) {
17 | ApplicationWindow("Lulù", onClose = ::exitApplication, defaultWidth = 400, defaultHeight = 400) {
18 | VerticalBox {
19 | HeaderBar(
20 | startWidgets = {
21 | IconButton(
22 | // The vector icon is embedded into the gresources file
23 | icon = ImageSource.Icon("heart-filled-symbolic"),
24 | // The "accent-colored" CSS class is defined in the gresources file
25 | modifier = Modifier.cssClasses("accent-colored"),
26 | onClick = { println("TODO: pet the dog") },
27 | )
28 | },
29 | )
30 | Picture(
31 | // The image is embedded into the gresources file
32 | ImageSource.forResource("/my/example/hello-app/images/lulu.jpg"),
33 | contentFit = ContentFit.COVER,
34 | modifier = Modifier.expand(),
35 | )
36 | }
37 | }
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/examples/src/main/resources/.gitignore:
--------------------------------------------------------------------------------
1 | resources.gresource
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
2 | kotlin.code.style=official
3 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
4 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.1.21"
3 | compose = "1.8.1"
4 | javagi = "0.12.2"
5 | dokka = "2.0.0"
6 | kotlin-logging = "7.0.7"
7 | kotlinx-datetime = "0.6.2"
8 | slf4j = "2.0.17"
9 | versioning = "6.4.4"
10 | detekt = "1.23.8"
11 | detekt-formatting = "1.23.8"
12 | detekt-compose = "0.4.22"
13 |
14 | [libraries]
15 | javagi-gtk = { group = "io.github.jwharm.javagi", name = "gtk", version.ref = "javagi" }
16 | javagi-adw = { group = "io.github.jwharm.javagi", name = "adw", version.ref = "javagi" }
17 | kotlin-logging = { group = "io.github.oshai", name = "kotlin-logging-jvm", version.ref = "kotlin-logging"}
18 | kotlinx-datetime = { group = "org.jetbrains.kotlinx", name="kotlinx-datetime", version.ref = "kotlinx-datetime" }
19 | slf4j-api = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j"}
20 | slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j"}
21 | detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt-formatting"}
22 | detekt-compose = { group = "io.nlopez.compose.rules", name = "detekt", version.ref = "detekt-compose"}
23 |
24 | [plugins]
25 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
26 | compose = { id = "org.jetbrains.compose", version.ref = "compose" }
27 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
28 | dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
29 | versioning = { id = "me.qoomon.git-versioning", version.ref = "versioning" }
30 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
31 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/compose4gtk/compose-4-gtk/d7887d0d304db1b1e3c9105f1c7b5ff4140c191d/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/lib/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jreleaser.model.Active
2 |
3 | plugins {
4 | alias(libs.plugins.kotlin.jvm)
5 | alias(libs.plugins.compose)
6 | alias(libs.plugins.compose.compiler)
7 | alias(libs.plugins.dokka)
8 | `maven-publish`
9 | id("org.jreleaser") version "1.18.0"
10 | alias(libs.plugins.versioning)
11 | alias(libs.plugins.detekt)
12 | }
13 |
14 | repositories {
15 | mavenCentral()
16 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
17 | google()
18 | }
19 |
20 | group = "io.github.compose4gtk"
21 | version = "0.0-SNAPSHOT"
22 | gitVersioning.apply {
23 | refs {
24 | tag("v(?.*)") {
25 | version = "\${ref.version}"
26 | }
27 | branch(".+") {
28 | version = "\${ref}-SNAPSHOT"
29 | }
30 | }
31 | rev {
32 | version = "\${commit}"
33 | }
34 | }
35 |
36 | kotlin {
37 | jvmToolchain(22)
38 | }
39 |
40 | java {
41 | sourceCompatibility = JavaVersion.VERSION_22
42 | targetCompatibility = JavaVersion.VERSION_22
43 | }
44 |
45 | dependencies {
46 | api(compose.runtime)
47 | api(libs.javagi.gtk)
48 | api(libs.javagi.adw)
49 | api(libs.kotlinx.datetime)
50 | implementation(libs.kotlin.logging)
51 | implementation(libs.slf4j.api)
52 | detektPlugins(libs.detekt.formatting)
53 | detektPlugins(libs.detekt.compose)
54 |
55 | testImplementation(libs.slf4j.simple)
56 | }
57 |
58 | val readMeToDocIndexTask = tasks.register("readmeToDocIndex") {
59 | group = "dokka"
60 | val inputFile = layout.projectDirectory.file("../README.md")
61 | from(inputFile)
62 | into(layout.buildDirectory.dir("generated-doc"))
63 | filter { line ->
64 | if (line.startsWith("Documentation is available on")) {
65 | ""
66 | } else {
67 | line
68 | .replace(
69 | "# A Kotlin Compose library for Gtk4 and Adw",
70 | "# Module Compose 4 GTK",
71 | )
72 | .replace(
73 | "](examples/",
74 | "](https://github.com/compose4gtk/compose-4-gtk/blob/main/examples/",
75 | )
76 | }
77 | }
78 | rename { "main.md" }
79 | }
80 |
81 | tasks.named("dokkaGeneratePublicationHtml") {
82 | dependsOn.add(readMeToDocIndexTask)
83 | }
84 |
85 | dokka {
86 | moduleName.set("Compose 4 GTK")
87 | dokkaPublications.html {
88 | failOnWarning.set(true)
89 | }
90 | dokkaSourceSets.main {
91 | includes.from(layout.buildDirectory.file("generated-doc/main.md"))
92 | includes.from(layout.projectDirectory.files("../docs/gtk.md"))
93 | }
94 | }
95 |
96 | tasks.register("dokkaHtmlJar") {
97 | dependsOn(tasks.dokkaGeneratePublicationHtml)
98 | from(tasks.dokkaGeneratePublicationHtml.flatMap { it.outputDirectory })
99 | archiveClassifier = "javadoc"
100 | }
101 |
102 | publishing {
103 | publications {
104 | create("mavenJava") {
105 | from(components["kotlin"])
106 | artifactId = "compose-4-gtk"
107 | artifact(tasks.getByName("dokkaHtmlJar"))
108 | artifact(tasks.getByName("kotlinSourcesJar"))
109 |
110 | pom {
111 | name = "compose-4-gtk"
112 | description = "A Kotlin Compose library for Gtk4 and Adw"
113 | inceptionYear = "2023"
114 | url = "https://github.com/compose4gtk/compose-4-gtk"
115 | licenses {
116 | license {
117 | name = "GNU General Public License v3.0"
118 | url = "https://www.gnu.org/licenses/gpl-3.0.en.html"
119 | }
120 | }
121 | developers {
122 | developer {
123 | name = "Marco Marangoni"
124 | email = "marco.marangoni1@gmail.com"
125 | }
126 | }
127 | contributors {
128 | // To add yourself here, please create a PR!
129 | contributor {}
130 | }
131 | scm {
132 | connection = "scm:git:git://github.com/compose4gtk/compose-4-gtk.git"
133 | developerConnection = "scm:git:ssh://github.com:compose4gtk/compose-4-gtk.git"
134 | url = "https://github.com/compose4gtk/compose-4-gtk"
135 | }
136 | }
137 | }
138 | }
139 | repositories {
140 | maven {
141 | setUrl(layout.buildDirectory.dir("staging-deploy"))
142 | }
143 | }
144 | }
145 |
146 | jreleaser {
147 | strict = true
148 | gitRootSearch = true
149 | signing {
150 | active = Active.ALWAYS
151 | armored = true
152 | verify = true
153 | }
154 | release {
155 | github {
156 | skipTag = true
157 | releaseNotes {
158 | enabled = true
159 | }
160 | changelog {
161 | enabled = false
162 | }
163 | }
164 | }
165 | deploy {
166 | maven {
167 | mavenCentral.create("release-deploy") {
168 | active = Active.RELEASE
169 | url = "https://central.sonatype.com/api/v1/publisher"
170 | retryDelay = 60
171 | setAuthorization("Basic")
172 | stagingRepository("build/staging-deploy")
173 | }
174 | // TODO: make snapshots work
175 | // mavenCentral.create("snapshot-deploy") {
176 | // active = Active.SNAPSHOT
177 | // url = "https://central.sonatype.com/api/v1/publisher"
178 | // retryDelay = 60
179 | // setAuthorization("Basic")
180 | // snapshotSupported = true
181 | // stagingRepository("build/staging-deploy")
182 | // }
183 | }
184 | }
185 | }
186 |
187 | tasks.named("publish") {
188 | dependsOn("clean")
189 | }
190 |
191 | tasks.named("jreleaserFullRelease") {
192 | dependsOn("publish")
193 | }
194 |
195 | tasks.register("compileTestGResources") {
196 | workingDir("src/test/gresources")
197 | commandLine("glib-compile-resources", "--target=../resources/resources.gresource", "resources.gresource.xml")
198 | }
199 |
200 | tasks.named("assembleTestResources") {
201 | dependsOn("compileTestGResources")
202 | }
203 |
204 | detekt {
205 | config.setFrom(file("../config/detekt/detekt.yml"))
206 | buildUponDefaultConfig = true
207 | }
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/Application.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Composition
5 | import androidx.compose.runtime.CompositionLocalProvider
6 | import androidx.compose.runtime.MonotonicFrameClock
7 | import androidx.compose.runtime.Recomposer
8 | import androidx.compose.runtime.Stable
9 | import androidx.compose.runtime.getValue
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.runtime.snapshots.Snapshot
13 | import androidx.compose.runtime.staticCompositionLocalOf
14 | import kotlinx.coroutines.CoroutineExceptionHandler
15 | import kotlinx.coroutines.CoroutineScope
16 | import kotlinx.coroutines.channels.Channel
17 | import kotlinx.coroutines.channels.consumeEach
18 | import kotlinx.coroutines.launch
19 | import kotlinx.coroutines.runBlocking
20 | import kotlinx.coroutines.withContext
21 | import kotlinx.coroutines.yield
22 | import org.gnome.gtk.Application
23 | import org.gnome.gtk.Window
24 | import kotlin.system.exitProcess
25 |
26 | private class GtkApplicationComposeNode : GtkComposeNode {
27 | override fun addNode(index: Int, child: GtkComposeNode) {
28 | if (child !is GtkComposeWidget<*> || child.widget !is Window) {
29 | throw UnsupportedOperationException("Only windows can be attached to Application")
30 | }
31 | }
32 |
33 | override fun removeNode(index: Int) = Unit
34 |
35 | override fun clearNodes() = Unit
36 | }
37 |
38 | @Stable
39 | interface ApplicationScope {
40 | val application: Application
41 | fun exitApplication()
42 | }
43 |
44 | val LocalApplication = staticCompositionLocalOf { throw IllegalStateException("not in a GTK application") }
45 |
46 | internal fun Application.initializeApplication(
47 | args: Array,
48 | content: @Composable ApplicationScope.() -> Unit,
49 | ) {
50 | val app = this
51 | val dispatcher = GtkDispatcher
52 | val handler = CoroutineExceptionHandler { _, exception ->
53 | println("CoroutineExceptionHandler got $exception")
54 | }
55 |
56 | runBlocking(dispatcher) {
57 | withContext(handler + YieldFrameClock) {
58 | startSnapshotManager()
59 |
60 | val recomposer = Recomposer(coroutineContext)
61 | var isOpen by mutableStateOf(true)
62 |
63 | val appScope = object : ApplicationScope {
64 | override val application = app
65 | override fun exitApplication() {
66 | isOpen = false
67 | }
68 | }
69 |
70 | launch {
71 | recomposer.runRecomposeAndApplyChanges()
72 | }
73 |
74 | val composition = Composition(GtkApplier(GtkApplicationComposeNode()), recomposer)
75 | app.onActivate {
76 | GtkDispatcher.active = true
77 | composition.setContent {
78 | if (isOpen) {
79 | CompositionLocalProvider(LocalApplication provides app) {
80 | appScope.content()
81 | }
82 | }
83 | }
84 | }
85 | val status = app.run(args)
86 | GtkDispatcher.active = false
87 | try {
88 | recomposer.close()
89 | recomposer.join()
90 | } finally {
91 | composition.dispose()
92 | }
93 | exitProcess(status)
94 | }
95 | }
96 | }
97 |
98 | private fun CoroutineScope.startSnapshotManager() {
99 | val channel = Channel(Channel.CONFLATED)
100 | launch {
101 | channel.consumeEach {
102 | Snapshot.sendApplyNotifications()
103 | }
104 | }
105 | Snapshot.registerGlobalWriteObserver {
106 | val channelResult = channel.trySend(Unit)
107 | check(channelResult.isSuccess) {
108 | channelResult.exceptionOrNull()?.printStackTrace()
109 | "Channel result = $channelResult"
110 | }
111 | }
112 | }
113 |
114 | private object YieldFrameClock : MonotonicFrameClock {
115 | override suspend fun withFrameNanos(
116 | onFrame: (frameTimeNanos: Long) -> R,
117 | ): R {
118 | yield()
119 | return onFrame(System.nanoTime())
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/GioResource.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk
2 |
3 | import org.gnome.gio.Gio
4 | import org.gnome.gio.Resource
5 |
6 | /**
7 | * Loads a Java resource as a `gresource` file.
8 | * Useful to load the GIO resources embedded into a JAR.
9 | */
10 | fun Class<*>.getGioResource(javaResourceName: String): Resource {
11 | val data = getResourceAsStream(javaResourceName).use { inputStream ->
12 | inputStream!!.readAllBytes()
13 | }
14 | return Resource.fromData(data)
15 | }
16 |
17 | /**
18 | * Registers the resource globally, and then invokes [f].
19 | * The resource is unregistered before returning
20 | */
21 | fun Resource.use(f: () -> T): T {
22 | Gio.resourcesRegister(this)
23 | try {
24 | return f()
25 | } finally {
26 | Gio.resourcesUnregister(this)
27 | }
28 | }
29 |
30 | /**
31 | * Loads and registers the given Java resource as a GIO resource, then invokes [f].
32 | */
33 | fun useGioResource(javaResourceName: String, f: () -> T): T {
34 | val resource = f.javaClass.getGioResource(javaResourceName)
35 | return resource.use(f)
36 | }
37 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/GtkApplier.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk
2 |
3 | import androidx.compose.runtime.AbstractApplier
4 |
5 | internal class GtkApplier(root: GtkComposeNode) : AbstractApplier(root) {
6 | override fun insertBottomUp(index: Int, instance: GtkComposeNode) = Unit
7 | override fun insertTopDown(index: Int, instance: GtkComposeNode) {
8 | current.addNode(index, instance)
9 | }
10 |
11 | override fun remove(index: Int, count: Int) {
12 | repeat(count) {
13 | current.removeNode(index)
14 | }
15 | }
16 |
17 | override fun move(from: Int, to: Int, count: Int) {
18 | TODO("Move not yet implemented")
19 | }
20 |
21 | override fun onClear() {
22 | current.clearNodes()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/GtkComposeNode.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk
2 |
3 | import io.github.compose4gtk.modifier.Modifier
4 | import org.gnome.gobject.GObject
5 | import org.gnome.gtk.Widget
6 |
7 | /**
8 | * A base node in the Compose tree.
9 | *
10 | * TODO: can we achieve the same using composition instead of inheritance?
11 | */
12 | interface GtkComposeNode {
13 | fun addNode(index: Int, child: GtkComposeNode)
14 | fun removeNode(index: Int)
15 | fun clearNodes()
16 | }
17 |
18 | /**
19 | * A node in the Compose tree that corresponds to a GTK [Widget].
20 | */
21 | abstract class GtkComposeWidget(val widget: W) : GtkComposeNode {
22 | var modifier: Modifier = Modifier
23 |
24 | fun applyModifier(modifier: Modifier) {
25 | this.modifier.undo(widget)
26 | this.modifier = modifier
27 | this.modifier.apply(widget)
28 | }
29 | }
30 |
31 | /**
32 | * A node in the Compose tree that corresponds to a GTK [Widget] that can contain other widgets.
33 | */
34 | abstract class GtkComposeContainer(widget: W) : GtkComposeWidget(widget) {
35 |
36 | final override fun addNode(index: Int, child: GtkComposeNode) {
37 | require(child is GtkComposeWidget<*>) {
38 | "GtkComposeContainer only works with GtkComposeWidget"
39 | }
40 | addNode(index, child)
41 | }
42 |
43 | protected abstract fun addNode(index: Int, child: GtkComposeWidget)
44 | }
45 |
46 | internal open class LeafComposeNode(widget: G) : GtkComposeWidget(widget) {
47 | override fun addNode(index: Int, child: GtkComposeNode) = throw UnsupportedOperationException()
48 | override fun removeNode(index: Int) = throw UnsupportedOperationException()
49 | override fun clearNodes() = throw UnsupportedOperationException()
50 | }
51 |
52 | internal open class SingleChildComposeNode(
53 | widget: W,
54 | val set: W.(Widget?) -> Unit,
55 | ) : GtkComposeContainer(widget) {
56 | private val stack = mutableListOf()
57 |
58 | private fun recompute() {
59 | val widget = stack.lastOrNull()
60 | this.widget.set(widget)
61 | }
62 |
63 | override fun addNode(index: Int, child: GtkComposeWidget) {
64 | check(stack.isEmpty()) {
65 | "${widget.javaClass.simpleName} can have at most one child node"
66 | }
67 | stack.add(child.widget)
68 | recompute()
69 | }
70 |
71 | override fun removeNode(index: Int) {
72 | stack.removeFirst()
73 | recompute()
74 | }
75 |
76 | override fun clearNodes() {
77 | stack.clear()
78 | recompute()
79 | }
80 | }
81 |
82 | internal class VirtualComposeNode(
83 | val nodeCreator: (G) -> GtkComposeNode,
84 | ) : GtkComposeNode {
85 | private var parentCreator: GtkComposeNode? = null
86 | private val children = mutableListOf()
87 |
88 | override fun addNode(index: Int, child: GtkComposeNode) {
89 | children.add(index, child)
90 | parentCreator?.addNode(index, child)
91 | }
92 |
93 | override fun removeNode(index: Int) {
94 | children.removeAt(index)
95 | parentCreator?.removeNode(index)
96 | }
97 |
98 | override fun clearNodes() {
99 | children.clear()
100 | parentCreator?.clearNodes()
101 | }
102 |
103 | fun setParent(parent: G?) {
104 | parentCreator?.clearNodes()
105 | parentCreator = if (parent != null) {
106 | nodeCreator(parent).also {
107 | children.forEachIndexed { index, child -> it.addNode(index, child) }
108 | }
109 | } else {
110 | null
111 | }
112 | }
113 | }
114 |
115 | internal class VirtualComposeNodeContainer(widget: W) : GtkComposeWidget(widget) {
116 | private val children = mutableListOf>()
117 | override fun addNode(index: Int, child: GtkComposeNode) {
118 | @Suppress("UNCHECKED_CAST")
119 | child as VirtualComposeNode
120 | children.add(index, child)
121 | child.setParent(widget)
122 | }
123 |
124 | override fun removeNode(index: Int) {
125 | children.removeAt(index).setParent(null)
126 | }
127 |
128 | override fun clearNodes() {
129 | children.forEach { it.setParent(null) }
130 | children.clear()
131 | }
132 | }
133 |
134 | internal abstract class GtkContainerComposeNode(widget: W) : GtkComposeContainer(widget) {
135 | private val _children = mutableListOf()
136 | protected val children: List = _children
137 | override fun addNode(index: Int, child: GtkComposeWidget) {
138 | val childWidget = child.widget
139 | _children.add(index, childWidget)
140 | }
141 |
142 | override fun removeNode(index: Int) {
143 | _children.removeAt(index)
144 | }
145 |
146 | override fun clearNodes() {
147 | _children.clear()
148 | }
149 |
150 | companion object {
151 | fun appendOnly(
152 | widget: W,
153 | add: W.(Widget) -> Unit,
154 | remove: W.(Widget) -> Unit,
155 | ) = object : GtkContainerComposeNode(widget) {
156 | override fun addNode(index: Int, child: GtkComposeWidget) {
157 | val toReinsert = children.drop(index)
158 | super.addNode(index, child)
159 | toReinsert.forEach { widget.remove(it) }
160 | widget.add(child.widget)
161 | toReinsert.forEach { widget.add(it) }
162 | }
163 |
164 | override fun removeNode(index: Int) {
165 | widget.remove(children[index])
166 | super.removeNode(index)
167 | }
168 |
169 | override fun clearNodes() {
170 | children.forEach { widget.remove(it) }
171 | super.clearNodes()
172 | }
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/GtkComposition.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.Composition
5 | import androidx.compose.runtime.DisposableEffect
6 | import androidx.compose.runtime.remember
7 | import androidx.compose.runtime.rememberCompositionContext
8 |
9 | @Composable
10 | fun gtkSubComposition(
11 | createNode: () -> T,
12 | content: @Composable (T) -> Unit,
13 | ): T {
14 | val compositionContext = rememberCompositionContext()
15 | val node = remember { createNode() }
16 | val composition = remember {
17 | Composition(
18 | GtkApplier(node),
19 | compositionContext,
20 | )
21 | }
22 | remember(content) {
23 | composition.setContent {
24 | content(node)
25 | }
26 | }
27 | DisposableEffect(Unit) {
28 | onDispose {
29 | composition.dispose()
30 | }
31 | }
32 | return node
33 | }
34 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/GtkDispatcher.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk
2 |
3 | import kotlinx.coroutines.CoroutineDispatcher
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.Runnable
6 | import org.gnome.glib.GLib
7 | import kotlin.coroutines.CoroutineContext
8 |
9 | @Suppress("UnusedReceiverParameter")
10 | val Dispatchers.Gtk get() = GtkDispatcher
11 |
12 | // TODO: implement Delay
13 | object GtkDispatcher : CoroutineDispatcher() {
14 | internal var active = false
15 | override fun dispatch(context: CoroutineContext, block: Runnable) {
16 | if (active) {
17 | GLib.idleAddOnce {
18 | block.run()
19 | }
20 | } else {
21 | block.run()
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/Adw.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw
2 |
3 | import androidx.compose.runtime.Composable
4 | import io.github.compose4gtk.ApplicationScope
5 | import io.github.compose4gtk.adw.components.ApplicationWindow
6 | import io.github.compose4gtk.initializeApplication
7 | import org.gnome.gio.Application
8 | import org.gnome.gio.ApplicationFlags
9 |
10 | @Deprecated(
11 | "Use adwApplication instead",
12 | replaceWith = ReplaceWith(
13 | expression = "adwApplication(appId, args) { content() }",
14 | imports = ["io.github.compose4gtk.adw.adwApplication"],
15 | ),
16 | )
17 | fun application(
18 | appId: String,
19 | args: Array,
20 | content: @Composable ApplicationScope.() -> Unit,
21 | ) = adwApplication(appId, args, content = content)
22 |
23 | /**
24 | * This is the entry point of LibAdwaita applications.
25 | *
26 | * This will start an application. [ApplicationWindow] can be added inside the [content] lambda.
27 | *
28 | * @param appId the GTK application id. If not null, it must be valid, see [Application.idIsValid].
29 | * @param args the application arguments. Usually the same as the ones in your `main`. See [Application.run].
30 | * @param flags the flags used when creating the application. See [ApplicationFlags].
31 | * @param content the lambda where your application is defined. You can start by adding an [ApplicationWindow].
32 | */
33 | fun adwApplication(
34 | appId: String?,
35 | args: Array,
36 | flags: Set = setOf(ApplicationFlags.DEFAULT_FLAGS),
37 | content: @Composable ApplicationScope.() -> Unit,
38 | ) {
39 | val app = org.gnome.adw.Application(appId, flags)
40 | app.initializeApplication(args, content)
41 | }
42 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/ApplicationWindow.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.State
5 | import androidx.compose.runtime.mutableStateOf
6 | import androidx.compose.runtime.remember
7 | import io.github.compose4gtk.modifier.Modifier
8 | import io.github.compose4gtk.shared.components.InitializeApplicationWindow
9 | import org.gnome.adw.ApplicationWindow
10 | import org.gnome.adw.Breakpoint
11 | import org.gnome.adw.BreakpointCondition
12 | import org.gnome.gtk.CssProvider
13 |
14 | interface ApplicationWindowScope {
15 | /**
16 | * This is a very ugly workaround, until https://gitlab.gnome.org/GNOME/libadwaita/-/issues/1018 is solved.
17 | *
18 | * Be careful: once added, breakpoints won't be removed. Avoid calling this function dynamically
19 | */
20 | @Composable
21 | fun RememberBreakpoint(
22 | condition: BreakpointCondition,
23 | matches: (Boolean) -> Unit,
24 | )
25 |
26 | @Composable
27 | fun rememberBreakpoint(
28 | condition: BreakpointCondition,
29 | ): State {
30 | val matches = remember { mutableStateOf(false) }
31 | RememberBreakpoint(condition) {
32 | matches.value = it
33 | }
34 | return matches
35 | }
36 | }
37 |
38 | private class ApplicationWindowScopeImpl(val window: ApplicationWindow) : ApplicationWindowScope {
39 |
40 | @Composable
41 | override fun RememberBreakpoint(
42 | condition: BreakpointCondition,
43 | matches: (Boolean) -> Unit,
44 | ): Unit = remember {
45 | val breakpoint = Breakpoint.builder()
46 | .setCondition(condition)
47 | .onApply { matches(true) }
48 | .onUnapply { matches(false) }
49 | .build()
50 | window.addBreakpoint(breakpoint)
51 | }
52 | }
53 |
54 | @Composable
55 | fun ApplicationWindow(
56 | title: String?,
57 | onClose: () -> Unit,
58 | modifier: Modifier = Modifier,
59 | styles: List = emptyList(),
60 | decorated: Boolean = true,
61 | defaultHeight: Int = 0,
62 | defaultWidth: Int = 0,
63 | deletable: Boolean = true,
64 | fullscreen: Boolean = false,
65 | maximized: Boolean = false,
66 | handleMenubarAccel: Boolean = true,
67 | modal: Boolean = false,
68 | resizable: Boolean = true,
69 | init: ApplicationWindow.() -> Unit = {},
70 | content: @Composable ApplicationWindowScope.() -> Unit,
71 | ) {
72 | InitializeApplicationWindow>(
73 | builder = {
74 | ApplicationWindow.builder()
75 | },
76 | modifier = modifier,
77 | title = title,
78 | styles = styles,
79 | deletable = deletable,
80 | onClose = onClose,
81 | decorated = decorated,
82 | defaultHeight = defaultHeight,
83 | defaultWidth = defaultWidth,
84 | fullscreen = fullscreen,
85 | maximized = maximized,
86 | handleMenubarAccel = handleMenubarAccel,
87 | modal = modal,
88 | resizable = resizable,
89 | init = init,
90 | setContent = { this.content = it },
91 | content = { window ->
92 | val scope = remember { ApplicationWindowScopeImpl(window) }
93 | scope.apply {
94 | content()
95 | }
96 | },
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/Avatar.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeWidget
7 | import io.github.compose4gtk.LeafComposeNode
8 | import io.github.compose4gtk.gtk.ImageSource
9 | import io.github.compose4gtk.gtk.setImage
10 | import io.github.compose4gtk.gtk.setPaintable
11 | import io.github.compose4gtk.modifier.Modifier
12 | import org.gnome.adw.Avatar
13 |
14 | @Composable
15 | fun Avatar(
16 | image: ImageSource?,
17 | text: String,
18 | modifier: Modifier = Modifier,
19 | showInitials: Boolean = false,
20 | size: Int = -1,
21 | ) {
22 | ComposeNode, GtkApplier>({
23 | LeafComposeNode(Avatar.builder().build())
24 | }) {
25 | set(modifier) { applyModifier(it) }
26 | set(image) { img ->
27 | this.widget.setImage(
28 | img,
29 | getCurrentPaintable = { this.customImage },
30 | setIcon = { this.iconName = it },
31 | setPaintable = { this.customImage = it },
32 | )
33 | }
34 | set(text) { this.widget.text = it }
35 | set(showInitials) { this.widget.showInitials = it }
36 | set(size) { this.widget.size = it }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/Banner.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.LeafComposeNode
7 | import io.github.compose4gtk.modifier.Modifier
8 | import io.github.jwharm.javagi.gobject.SignalConnection
9 | import org.gnome.adw.Banner
10 |
11 | private class AdwBannerComposeNode(
12 | gObject: Banner,
13 | ) : LeafComposeNode(gObject) {
14 | var onButtonClicked: SignalConnection? = null
15 | }
16 |
17 | @Composable
18 | fun Banner(
19 | modifier: Modifier = Modifier,
20 | title: String? = null,
21 | buttonLabel: String? = null,
22 | onButtonClick: (() -> Unit)? = null,
23 | revealed: Boolean = true,
24 | useMarkup: Boolean = true,
25 | ) {
26 | ComposeNode({
27 | AdwBannerComposeNode(Banner.builder().build())
28 | }) {
29 | set(modifier) { applyModifier(it) }
30 | set(title) { this.widget.title = it }
31 | set(buttonLabel) { this.widget.buttonLabel = it }
32 | set(onButtonClick) {
33 | this.onButtonClicked?.disconnect()
34 | if (it != null) {
35 | this.onButtonClicked = this.widget.onButtonClicked {
36 | it()
37 | }
38 | } else {
39 | this.onButtonClicked = null
40 | }
41 | }
42 | set(revealed) { this.widget.revealed = it }
43 | set(useMarkup) { this.widget.useMarkup = it }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/BottomSheet.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import io.github.compose4gtk.GtkApplier
10 | import io.github.compose4gtk.GtkComposeNode
11 | import io.github.compose4gtk.GtkComposeWidget
12 | import io.github.compose4gtk.SingleChildComposeNode
13 | import io.github.compose4gtk.VirtualComposeNode
14 | import io.github.compose4gtk.VirtualComposeNodeContainer
15 | import io.github.compose4gtk.modifier.Modifier
16 | import org.gnome.adw.BottomSheet
17 |
18 | @Composable
19 | fun BottomSheet(
20 | open: Boolean,
21 | modifier: Modifier = Modifier,
22 | align: Float = 0.5f,
23 | canClose: Boolean = true,
24 | canOpen: Boolean = true,
25 | fullWidth: Boolean = true,
26 | modal: Boolean = true,
27 | showDragHandle: Boolean = true,
28 | onClose: () -> Unit = {},
29 | onOpen: () -> Unit = {},
30 | bottomBar: @Composable () -> Unit = {},
31 | sheet: @Composable () -> Unit = {},
32 | content: @Composable () -> Unit = {},
33 | ) {
34 | val bottomSheet = remember { BottomSheet.builder().build() }
35 | var lastOpen by remember { mutableStateOf(open) }
36 |
37 | ComposeNode, GtkApplier>(
38 | factory = {
39 | VirtualComposeNodeContainer(bottomSheet)
40 | },
41 | update = {
42 | set(open) { this.widget.open = it }
43 | set(modifier) { applyModifier(it) }
44 | set(align) { this.widget.align = it }
45 | set(canClose) { this.widget.canClose = it }
46 | set(canOpen) { this.widget.canOpen = it }
47 | set(fullWidth) { this.widget.fullWidth = it }
48 | set(modal) { this.widget.modal = it }
49 | set(showDragHandle) { this.widget.showDragHandle = it }
50 |
51 | if (lastOpen != open) {
52 | bottomSheet.open = open
53 | lastOpen = open
54 | }
55 |
56 | bottomSheet.onNotify("open") {
57 | val currentWidgetOpen = bottomSheet.open
58 | if (currentWidgetOpen != lastOpen) {
59 | if (currentWidgetOpen) {
60 | onOpen()
61 | } else {
62 | onClose()
63 | }
64 | bottomSheet.open = open
65 | }
66 | }
67 | },
68 | content = {
69 | BottomBar {
70 | bottomBar()
71 | }
72 | Sheet {
73 | sheet()
74 | }
75 | Content {
76 | content()
77 | }
78 | },
79 | )
80 | }
81 |
82 | @Composable
83 | private fun BottomBar(
84 | content: @Composable () -> Unit,
85 | ) {
86 | ComposeNode(
87 | factory = {
88 | VirtualComposeNode { bottomSheet ->
89 | SingleChildComposeNode(
90 | bottomSheet,
91 | set = { bottomBar = it },
92 | )
93 | }
94 | },
95 | update = {},
96 | content = content,
97 | )
98 | }
99 |
100 | @Composable
101 | private fun Sheet(
102 | content: @Composable () -> Unit,
103 | ) {
104 | ComposeNode(
105 | factory = {
106 | VirtualComposeNode { bottomSheet ->
107 | SingleChildComposeNode(
108 | bottomSheet,
109 | set = { sheet = it },
110 | )
111 | }
112 | },
113 | update = {},
114 | content = content,
115 | )
116 | }
117 |
118 | @Composable
119 | private fun Content(
120 | content: @Composable () -> Unit,
121 | ) {
122 | ComposeNode(
123 | factory = {
124 | VirtualComposeNode { bottomSheet ->
125 | SingleChildComposeNode(
126 | bottomSheet,
127 | set = { setContent(it) },
128 | )
129 | }
130 | },
131 | update = {},
132 | content = content,
133 | )
134 | }
135 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/Clamp.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeWidget
7 | import io.github.compose4gtk.SingleChildComposeNode
8 | import io.github.compose4gtk.modifier.Modifier
9 | import org.gnome.gtk.Orientation
10 | import org.gnome.adw.Clamp as GtkClamp
11 |
12 | @Composable
13 | fun VerticalClamp(
14 | modifier: Modifier = Modifier,
15 | maximumSize: Int = 600,
16 | tighteningThreshold: Int = 400,
17 | content: @Composable () -> Unit,
18 | ) {
19 | Clamp(modifier, Orientation.VERTICAL, maximumSize, tighteningThreshold, content)
20 | }
21 |
22 | @Composable
23 | fun HorizontalClamp(
24 | modifier: Modifier = Modifier,
25 | maximumSize: Int = 600,
26 | tighteningThreshold: Int = 400,
27 | content: @Composable () -> Unit,
28 | ) {
29 | Clamp(modifier, Orientation.HORIZONTAL, maximumSize, tighteningThreshold, content)
30 | }
31 |
32 | @Composable
33 | fun Clamp(
34 | modifier: Modifier = Modifier,
35 | orientation: Orientation = Orientation.HORIZONTAL,
36 | maximumSize: Int = 600,
37 | tighteningThreshold: Int = 400,
38 | content: @Composable () -> Unit,
39 | ) {
40 | ComposeNode, GtkApplier>(
41 | factory = {
42 | SingleChildComposeNode(
43 | GtkClamp.builder().build(),
44 | set = { child = it },
45 | )
46 | },
47 | update = {
48 | set(modifier) { applyModifier(it) }
49 | set(orientation) { this.widget.orientation = it }
50 | set(maximumSize) { this.widget.maximumSize = it }
51 | set(tighteningThreshold) { this.widget.tighteningThreshold = it }
52 | },
53 | content = content,
54 | )
55 | }
56 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/HeaderBar.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeNode
7 | import io.github.compose4gtk.GtkComposeWidget
8 | import io.github.compose4gtk.GtkContainerComposeNode
9 | import io.github.compose4gtk.SingleChildComposeNode
10 | import io.github.compose4gtk.VirtualComposeNode
11 | import io.github.compose4gtk.VirtualComposeNodeContainer
12 | import io.github.compose4gtk.modifier.Modifier
13 | import org.gnome.adw.CenteringPolicy
14 | import org.gnome.adw.HeaderBar
15 | import org.gnome.gtk.Widget
16 |
17 | @Composable
18 | fun HeaderBar(
19 | modifier: Modifier = Modifier,
20 | centeringPolicy: CenteringPolicy = CenteringPolicy.LOOSE,
21 | showEndTitleButtons: Boolean = true,
22 | showStartTitleButtons: Boolean = true,
23 | title: (@Composable () -> Unit)? = null,
24 | startWidgets: @Composable () -> Unit = {},
25 | endWidgets: @Composable () -> Unit = {},
26 | ) {
27 | ComposeNode, GtkApplier>(
28 | {
29 | VirtualComposeNodeContainer(HeaderBar.builder().build())
30 | },
31 | update = {
32 | set(modifier) { applyModifier(it) }
33 | set(centeringPolicy) { this.widget.centeringPolicy = it }
34 | set(showEndTitleButtons) { this.widget.showEndTitleButtons = it }
35 | set(showStartTitleButtons) { this.widget.showStartTitleButtons = it }
36 | },
37 | content = {
38 | Pack({ packStart(it) }, startWidgets)
39 | if (title != null) {
40 | Title {
41 | title()
42 | }
43 | }
44 | Pack({ packEnd(it) }, endWidgets)
45 | },
46 | )
47 | }
48 |
49 | @Composable
50 | private fun Pack(
51 | packer: HeaderBar.(Widget) -> Unit,
52 | content: @Composable () -> Unit = {},
53 | ) {
54 | ComposeNode(
55 | {
56 | VirtualComposeNode { header ->
57 | GtkContainerComposeNode.appendOnly(
58 | header,
59 | add = { packer(it) },
60 | remove = { remove(it) },
61 | )
62 | }
63 | },
64 | update = {},
65 | content = content,
66 | )
67 | }
68 |
69 | @Composable
70 | private fun Title(
71 | title: @Composable () -> Unit = {},
72 | ) {
73 | ComposeNode(
74 | {
75 | VirtualComposeNode { header ->
76 | SingleChildComposeNode(
77 | header,
78 | set = { titleWidget = it },
79 | )
80 | }
81 | },
82 | update = {},
83 | content = title,
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/OverlaySplitView.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import androidx.compose.runtime.getValue
6 | import androidx.compose.runtime.mutableStateOf
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.setValue
9 | import io.github.compose4gtk.GtkApplier
10 | import io.github.compose4gtk.GtkComposeNode
11 | import io.github.compose4gtk.GtkComposeWidget
12 | import io.github.compose4gtk.SingleChildComposeNode
13 | import io.github.compose4gtk.VirtualComposeNode
14 | import io.github.compose4gtk.VirtualComposeNodeContainer
15 | import io.github.compose4gtk.modifier.Modifier
16 | import org.gnome.adw.OverlaySplitView
17 | import org.gnome.gtk.PackType
18 |
19 | interface OverlaySplitViewScope {
20 | val showSidebar: Boolean
21 | fun showSidebar()
22 | fun hideSidebar()
23 | }
24 |
25 | private class OverlaySplitViewImpl : OverlaySplitViewScope {
26 | override var showSidebar by mutableStateOf(true)
27 | var overlaySplitView: OverlaySplitView? = null
28 | set(widget) {
29 | require(field == null)
30 | if (widget != null) {
31 | widget.showSidebar = showSidebar
32 | widget.onNotify("show-sidebar") {
33 | showSidebar = widget.showSidebar
34 | }
35 | field = widget
36 | }
37 | }
38 |
39 | override fun showSidebar() {
40 | showSidebar(true)
41 | }
42 |
43 | override fun hideSidebar() {
44 | showSidebar(false)
45 | }
46 |
47 | private fun showSidebar(show: Boolean) {
48 | when (val w = overlaySplitView) {
49 | null -> showSidebar = show
50 | else -> w.showSidebar = show
51 | }
52 | }
53 | }
54 |
55 | /**
56 | * TODO:
57 | * - min/max sidebar width
58 | */
59 | @Composable
60 | fun OverlaySplitView(
61 | sidebar: @Composable () -> Unit,
62 | modifier: Modifier = Modifier,
63 | collapsed: Boolean = false,
64 | pinSidebar: Boolean = false,
65 | sidebarPosition: PackType = PackType.START,
66 | sidebarWidthFraction: Double = 0.25,
67 | enableHideGesture: Boolean = true,
68 | enableShowGesture: Boolean = true,
69 | content: @Composable OverlaySplitViewScope.() -> Unit,
70 | ) {
71 | val scope = remember { OverlaySplitViewImpl() }
72 | ComposeNode, GtkApplier>(
73 | factory = {
74 | val splitView = OverlaySplitView
75 | .builder()
76 | .build()
77 | VirtualComposeNodeContainer(splitView)
78 | },
79 | update = {
80 | set(modifier) { applyModifier(it) }
81 | set(collapsed) { this.widget.collapsed = it }
82 | set(pinSidebar) { this.widget.pinSidebar = it }
83 | set(sidebarPosition) { this.widget.sidebarPosition = it }
84 | set(sidebarWidthFraction) { this.widget.sidebarWidthFraction = it }
85 | set(enableHideGesture) { this.widget.enableHideGesture = it }
86 | set(enableShowGesture) { this.widget.enableShowGesture = it }
87 | set(scope) { scope.overlaySplitView = this.widget }
88 | },
89 | content = {
90 | Sidebar {
91 | sidebar()
92 | }
93 | Content {
94 | scope.apply {
95 | content()
96 | }
97 | }
98 | },
99 | )
100 | }
101 |
102 | @Composable
103 | private fun Content(
104 | content: @Composable () -> Unit,
105 | ) {
106 | ComposeNode(
107 | factory = {
108 | VirtualComposeNode { overlay ->
109 | SingleChildComposeNode(
110 | overlay,
111 | set = { setContent(it) },
112 | )
113 | }
114 | },
115 | update = { },
116 | content = content,
117 | )
118 | }
119 |
120 | @Composable
121 | private fun Sidebar(
122 | content: @Composable () -> Unit,
123 | ) {
124 | ComposeNode(
125 | factory = {
126 | VirtualComposeNode { overlay ->
127 | SingleChildComposeNode(
128 | overlay,
129 | set = { sidebar = it },
130 | )
131 | }
132 | },
133 | update = { },
134 | content = content,
135 | )
136 | }
137 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/StatusPage.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeWidget
7 | import io.github.compose4gtk.SingleChildComposeNode
8 | import io.github.compose4gtk.gtk.ImageSource
9 | import io.github.compose4gtk.gtk.setImage
10 | import io.github.compose4gtk.modifier.Modifier
11 | import org.gnome.adw.StatusPage
12 |
13 | @Composable
14 | fun StatusPage(
15 | title: String,
16 | modifier: Modifier = Modifier,
17 | description: String? = null,
18 | icon: ImageSource? = null,
19 | content: @Composable () -> Unit = {},
20 | ) {
21 | ComposeNode, GtkApplier>(
22 | factory = {
23 | SingleChildComposeNode(
24 | StatusPage.builder().build(),
25 | set = { child = it },
26 | )
27 | },
28 | update = {
29 | set(title) { this.widget.title = it }
30 | set(modifier) { applyModifier(it) }
31 | set(description) { this.widget.description = it }
32 | set(icon) { img ->
33 | this.widget.setImage(
34 | img,
35 | getCurrentPaintable = { this.paintable },
36 | setIcon = { this.iconName = it },
37 | setPaintable = { this.paintable = it },
38 | )
39 | }
40 | },
41 | content = content,
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/ToastOverlay.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import androidx.compose.runtime.remember
6 | import io.github.compose4gtk.GtkApplier
7 | import io.github.compose4gtk.GtkComposeWidget
8 | import io.github.compose4gtk.SingleChildComposeNode
9 | import io.github.compose4gtk.modifier.Modifier
10 | import org.gnome.adw.Toast
11 | import org.gnome.adw.ToastOverlay
12 |
13 | interface ToastOverlayScope {
14 | /**
15 | * Shows a Toast
16 | */
17 | fun addToast(toast: Toast)
18 | fun dismissAllToasts()
19 | }
20 |
21 | private class ToastOverlayScopeImpl : ToastOverlayScope {
22 | var toastOverlay: ToastOverlay? = null
23 | override fun addToast(toast: Toast) {
24 | toastOverlay!!.addToast(toast)
25 | }
26 |
27 | override fun dismissAllToasts() {
28 | toastOverlay?.dismissAll()
29 | }
30 | }
31 |
32 | @Composable
33 | fun ToastOverlay(
34 | modifier: Modifier = Modifier,
35 | content: @Composable ToastOverlayScope.() -> Unit,
36 | ) {
37 | val overlayScope = remember { ToastOverlayScopeImpl() }
38 | ComposeNode, GtkApplier>(
39 | factory = {
40 | val toastOverlay = ToastOverlay.builder().build()
41 | SingleChildComposeNode(
42 | toastOverlay,
43 | set = { toastOverlay.child = it },
44 | )
45 | },
46 | update = {
47 | set(modifier) { applyModifier(it) }
48 | set(overlayScope) { it.toastOverlay = this.widget }
49 | },
50 | content = {
51 | overlayScope.content()
52 | },
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/ToolbarView.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeNode
7 | import io.github.compose4gtk.GtkComposeWidget
8 | import io.github.compose4gtk.GtkContainerComposeNode
9 | import io.github.compose4gtk.SingleChildComposeNode
10 | import io.github.compose4gtk.VirtualComposeNode
11 | import io.github.compose4gtk.VirtualComposeNodeContainer
12 | import io.github.compose4gtk.modifier.Modifier
13 | import org.gnome.adw.ToolbarStyle
14 | import org.gnome.adw.ToolbarView
15 | import org.gnome.gtk.Widget
16 |
17 | @Composable
18 | fun ToolbarView(
19 | modifier: Modifier = Modifier,
20 | // Top bar
21 | topBarStyle: ToolbarStyle = ToolbarStyle.FLAT,
22 | revealTopBars: Boolean = true,
23 | extendContentToTopEdge: Boolean = false,
24 | topBar: @Composable () -> Unit = {},
25 | // Bottom bar
26 | bottomBarStyle: ToolbarStyle = ToolbarStyle.FLAT,
27 | revealBottomBars: Boolean = true,
28 | extendContentToBottomEdge: Boolean = false,
29 | bottomBar: @Composable () -> Unit = {},
30 | // Content
31 | content: @Composable () -> Unit,
32 | ) {
33 | ComposeNode, GtkApplier>(
34 | {
35 | VirtualComposeNodeContainer(ToolbarView.builder().build())
36 | },
37 | update = {
38 | set(modifier) { applyModifier(it) }
39 | // Top bar
40 | set(topBarStyle) { this.widget.topBarStyle = it }
41 | set(revealTopBars) { this.widget.revealTopBars = it }
42 | set(extendContentToTopEdge) { this.widget.extendContentToTopEdge = it }
43 | // Bottom bar
44 | set(bottomBarStyle) { this.widget.bottomBarStyle = it }
45 | set(revealBottomBars) { this.widget.revealBottomBars = it }
46 | set(extendContentToBottomEdge) { this.widget.extendContentToBottomEdge = it }
47 | },
48 | content = {
49 | ToolbarViewBar({ addTopBar(it) }, topBar)
50 | ToolbarViewContent(content)
51 | ToolbarViewBar({ addBottomBar(it) }, bottomBar)
52 | },
53 | )
54 | }
55 |
56 | @Composable
57 | private fun ToolbarViewBar(
58 | appendBar: ToolbarView.(Widget) -> Unit,
59 | content: @Composable () -> Unit = {},
60 | ) {
61 | ComposeNode(
62 | {
63 | VirtualComposeNode { toolbar ->
64 | GtkContainerComposeNode.appendOnly(
65 | toolbar,
66 | add = { appendBar(it) },
67 | remove = { remove(it) },
68 | )
69 | }
70 | },
71 | update = {},
72 | content = content,
73 | )
74 | }
75 |
76 | @Composable
77 | private fun ToolbarViewContent(
78 | content: @Composable () -> Unit = {},
79 | ) {
80 | ComposeNode(
81 | {
82 | VirtualComposeNode { toolbar ->
83 | SingleChildComposeNode(
84 | widget = toolbar,
85 | set = { this.content = it },
86 | )
87 | }
88 | },
89 | update = {},
90 | content = content,
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/adw/components/WindowTitle.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.adw.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeWidget
7 | import io.github.compose4gtk.LeafComposeNode
8 | import io.github.compose4gtk.modifier.Modifier
9 | import org.gnome.adw.WindowTitle
10 |
11 | @Composable
12 | fun WindowTitle(
13 | title: String,
14 | modifier: Modifier = Modifier,
15 | subtitle: String? = null,
16 | ) {
17 | ComposeNode, GtkApplier>({
18 | LeafComposeNode(WindowTitle.builder().build())
19 | }) {
20 | set(title) { this.widget.title = it }
21 | set(modifier) { applyModifier(it) }
22 | set(subtitle) { this.widget.subtitle = it }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/gtk/Gtk.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.gtk
2 |
3 | import androidx.compose.runtime.Composable
4 | import io.github.compose4gtk.ApplicationScope
5 | import io.github.compose4gtk.adw.adwApplication
6 | import io.github.compose4gtk.gtk.components.GtkApplicationWindow
7 | import io.github.compose4gtk.initializeApplication
8 | import org.gnome.gio.Application
9 | import org.gnome.gio.ApplicationFlags
10 | import kotlin.run
11 |
12 | @Deprecated(
13 | "Use adwApplication instead",
14 | replaceWith = ReplaceWith(
15 | expression = "gtkApplication(appId, args) { content() }",
16 | imports = ["io.github.compose4gtk.gtk.gtkApplication"],
17 | ),
18 | )
19 | fun application(
20 | appId: String,
21 | args: Array,
22 | content: @Composable ApplicationScope.() -> Unit,
23 | ) = adwApplication(appId, args, content = content)
24 |
25 | /**
26 | * This is the entry point of LibAdwaita applications.
27 | *
28 | * This will start an application. [GtkApplicationWindow] can be added inside the [content] lambda.
29 | *
30 | * @param appId the GTK application id. If not null, it must be valid, see [Application.idIsValid].
31 | * @param args the application arguments. Usually the same as the ones in your `main`. See [Application.run].
32 | * @param flags the flags used when creating the application. See [ApplicationFlags].
33 | * @param content the lambda where your application is defined. You can start by adding a [GtkApplicationWindow].
34 | */
35 | fun gtkApplication(
36 | appId: String,
37 | args: Array,
38 | flags: Set = setOf(ApplicationFlags.DEFAULT_FLAGS),
39 | content: @Composable ApplicationScope.() -> Unit,
40 | ) {
41 | val app = org.gnome.gtk.Application(appId, flags)
42 | app.initializeApplication(args, content)
43 | }
44 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/gtk/ImageSource.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.gtk
2 |
3 | import org.gnome.adw.SpinnerPaintable
4 | import org.gnome.gdk.Texture
5 | import org.gnome.gtk.Widget
6 | import org.gnome.gdk.Paintable as GtkPaintable
7 |
8 | sealed interface ImageSource {
9 | data class Icon(val iconName: String) : ImageSource
10 |
11 | fun interface PaintableFactory : ImageSource {
12 | fun create(): GtkPaintable
13 | }
14 |
15 | companion object {
16 | val spinner = PaintableFactory {
17 | SpinnerPaintable.builder().build()
18 | }
19 |
20 | fun forFile(file: org.gnome.gio.File) = PaintableFactory {
21 | Texture.fromFile(file)
22 | }
23 |
24 | fun forResource(resourcePath: String) = PaintableFactory {
25 | Texture.fromResource(resourcePath)
26 | }
27 |
28 | fun forTexture(texture: Texture) = PaintableFactory {
29 | texture
30 | }
31 | }
32 | }
33 |
34 | fun W.setImage(
35 | image: ImageSource?,
36 | getCurrentPaintable: W.() -> GtkPaintable?,
37 | setPaintable: W.(GtkPaintable?) -> Unit,
38 | setIcon: W.(String?) -> Unit,
39 | ) {
40 | when (image) {
41 | is ImageSource.Icon -> {
42 | setPaintable(null, getCurrentPaintable, setPaintable)
43 | setIcon(image.iconName)
44 | }
45 |
46 | is ImageSource.PaintableFactory? -> {
47 | setPaintable(image?.create(), getCurrentPaintable, setPaintable)
48 | }
49 | }
50 | }
51 |
52 | fun W.setPaintable(
53 | paintable: GtkPaintable?,
54 | getCurrentPaintable: W.() -> GtkPaintable?,
55 | setPaintable: W.(GtkPaintable?) -> Unit,
56 | ) {
57 | getCurrentPaintable()?.setWidget(null)
58 | setPaintable(paintable)
59 | paintable?.setWidget(this)
60 | }
61 |
62 | private fun GtkPaintable.setWidget(widget: Widget?) {
63 | if (this is SpinnerPaintable) {
64 | this.widget = widget
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/gtk/components/Box.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.gtk.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import io.github.compose4gtk.GtkApplier
6 | import io.github.compose4gtk.GtkComposeWidget
7 | import io.github.compose4gtk.GtkContainerComposeNode
8 | import io.github.compose4gtk.modifier.Modifier
9 | import org.gnome.gtk.Orientation
10 | import org.gnome.gtk.Widget
11 | import org.gnome.gtk.Box as GtkBox
12 |
13 | private class GtkBoxComposeNode(gObject: GtkBox) : GtkContainerComposeNode(gObject) {
14 | override fun addNode(index: Int, child: GtkComposeWidget) {
15 | when (index) {
16 | children.size -> widget.append(child.widget)
17 | 0 -> widget.insertChildAfter(child.widget, null)
18 | else -> widget.insertChildAfter(child.widget, children[index - 1])
19 | }
20 | super.addNode(index, child)
21 | }
22 |
23 | override fun removeNode(index: Int) {
24 | val child = children[index]
25 | widget.remove(child)
26 | super.removeNode(index)
27 | }
28 |
29 | override fun clearNodes() {
30 | children.forEach { widget.remove(it) }
31 | super.clearNodes()
32 | }
33 | }
34 |
35 | @Composable
36 | fun VerticalBox(
37 | modifier: Modifier = Modifier,
38 | spacing: Int = 0,
39 | homogeneous: Boolean = false,
40 | content: @Composable () -> Unit,
41 | ) {
42 | Box(modifier, Orientation.VERTICAL, spacing, homogeneous, content)
43 | }
44 |
45 | @Composable
46 | fun HorizontalBox(
47 | modifier: Modifier = Modifier,
48 | spacing: Int = 0,
49 | homogeneous: Boolean = false,
50 | content: @Composable () -> Unit,
51 | ) {
52 | Box(modifier, Orientation.HORIZONTAL, spacing, homogeneous, content)
53 | }
54 |
55 | @Composable
56 | fun Box(
57 | modifier: Modifier = Modifier,
58 | orientation: Orientation = Orientation.HORIZONTAL,
59 | spacing: Int = 0,
60 | homogeneous: Boolean = false,
61 | content: @Composable () -> Unit,
62 | ) {
63 | ComposeNode, GtkApplier>(
64 | factory = {
65 | GtkBoxComposeNode(GtkBox.builder().build())
66 | },
67 | update = {
68 | set(modifier) { applyModifier(it) }
69 | set(homogeneous) { this.widget.homogeneous = it }
70 | set(orientation) { this.widget.orientation = it }
71 | set(spacing) { this.widget.spacing = it }
72 | },
73 | content = content,
74 | )
75 | }
76 |
--------------------------------------------------------------------------------
/lib/src/main/kotlin/io/github/compose4gtk/gtk/components/Button.kt:
--------------------------------------------------------------------------------
1 | package io.github.compose4gtk.gtk.components
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.ComposeNode
5 | import androidx.compose.runtime.Updater
6 | import io.github.compose4gtk.GtkApplier
7 | import io.github.compose4gtk.GtkComposeWidget
8 | import io.github.compose4gtk.SingleChildComposeNode
9 | import io.github.compose4gtk.gtk.ImageSource
10 | import io.github.compose4gtk.modifier.Modifier
11 | import io.github.jwharm.javagi.gobject.SignalConnection
12 | import org.gnome.gtk.Button
13 | import org.gnome.gtk.LinkButton
14 | import org.gnome.gtk.ToggleButton
15 |
16 | private class GtkButtonComposeNode(gObject: Button) : SingleChildComposeNode