├── .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 | 11 | 16 | 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