diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..3048b3d
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,37 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ # JDK 17 satisfies both the Frege source build (-source 1.8) and AGP 8.
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - name: Cache Frege snapshot
+ id: frege-cache
+ uses: actions/cache@v4
+ with:
+ path: libs/frege-compiler-snapshot.jar
+ # Bump when tools/build-frege-snapshot.sh pins a new commit.
+ key: frege-067cad05
+
+ - name: Build Frege compiler from pinned master
+ if: steps.frege-cache.outputs.cache-hit != 'true'
+ run: tools/build-frege-snapshot.sh
+
+ - name: Set up Android SDK
+ uses: android-actions/setup-android@v3
+
+ - name: Assemble debug APK
+ run: ./gradlew :app:assembleDebug --stacktrace
diff --git a/.gitignore b/.gitignore
index 23a7cdc..a66088d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,21 @@
-build/*
-lib/*
+# Gradle / build outputs (any depth: froid/, examples/*/, froid-gradle-plugin/)
+**/build/
+**/.gradle/
+.gradle/
+.kotlin/
+
+# Android Studio / IntelliJ
+.idea/
+*.iml
+local.properties
+captures/
+.cxx/
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Jars are ignored by default, EXCEPT the vendored Frege snapshot + the wrapper.
*.jar
+!libs/frege-compiler-snapshot.jar
+!gradle/wrapper/gradle-wrapper.jar
diff --git a/README.md b/README.md
index c13cb61..9a5aa31 100644
--- a/README.md
+++ b/README.md
@@ -2,40 +2,149 @@
-A library for using the Frege programming language in Android development.
+Write Android apps in the **Frege** programming language (Haskell on the JVM) —
+pure Frege, no Kotlin, with much less ceremony than the original thanks to
+Frege's new [automated FFI](https://github.com/Frege/frege/pull/400).
-## Usage and examples
+froid is two things:
-To get/setup froid read the instructions on the [froid Wiki](https://github.com/mchav/froid/wiki). To write a simple application from scratch follow [this step-by-step tutorial](https://github.com/mchav/froid/wiki/Tutorial).
+1. **A general Frege-on-Android library** — thin handles over `android.view` /
+ `android.widget` / `android.app`, with the boilerplate erased by auto-FFI and
+ the irreducible glue (the Activity, callbacks) hidden in the library. App code
+ writes no Java and no Kotlin.
+2. **A declarative UI in the Elm Architecture, backed by FRP** — built on the
+ View system. You write `update :: Msg -> Model -> Model` and
+ `view :: Signal Model -> Ui Msg`; froid keeps the views in sync.
+
+## A counter, in full
-Simple Activity
----------------
```frege
-module io.github.mchav.fregeandroid.FregeActivity where
+module app.Counter where
import froid.app.Activity
-import froid.content.Context
-import froid.os.Bundle
-import froid.widget.TextView
+import froid.ui.App
+import froid.frp.Signal
+
+native module type Activity where {} -- the one line that makes this an Activity
+
+data Msg = Increment
+
+update :: Msg -> Int -> Int
+update Increment n = n + 1
+
+view :: Signal Int -> Ui Msg
+view model = column
+ [ dynText (fmap (\n -> "Count: " ++ show n) model)
+ , button "Increment" Increment
+ ]
+
+counter :: App Int Msg
+counter = App { initial = 0, update = update, view = view }
+
+onCreate :: Activity -> IO ()
+onCreate this = runApp this counter
+```
+
+No `findViewById`, no listeners by hand, no XML, no Kotlin. Buttons carry the
+`Msg` they dispatch; state evolves in one place (`update`).
+
+## The Elm Architecture (`froid.ui.App`)
+
+- `App { initial, update, view }` — your whole program.
+- `runApp :: Activity -> App model msg -> IO ()` — installs it.
+- Widgets: `text` / `dynText` / `headline` / `button` / `primaryButton` /
+ `flatButton` / `column` / `row` / `gap` / `flexSpacer`. `view` takes a
+ `Signal model`, so only the labels whose text changed are redrawn.
+
+## The FRP core (`froid.frp.Signal`)
+
+A small, discrete, push-based FRP — the natural Frege idiom for reactive UIs:
+
+- `Event a` — a stream of occurrences: `newEvent`, `fmap`, `never`, `mergeE`,
+ `filterE`, `snapshot`/`tag`.
+- `Signal a` — a value over time: `accumS` (Elm's `foldp`), `stepper`, `react`,
+ `fmap`. The accumulator is strict (no `foldl` leak); the UI thread is
+ single-threaded so cells need no locking.
+
+The Elm `App` layer funnels every `Msg` through one event, so app authors rarely
+touch these directly — they're there when you want raw reactive wiring.
+
+## How an Activity works (no glue in app code)
+
+A module becomes an Android Activity with one line, `native module type Activity
+where {}` — its generated class extends `froid.app.FregeActivity` (plain Java in
+the library), which bridges Android's `onCreate` to your
+`onCreate :: Activity -> IO ()`. The manifest points at the module class
+(e.g. `app.Counter`).
+
+## Repository layout
+
+```
+froid/ the library (pure Frege + a tiny Java Activity base)
+ src/frege/froid/… frp/Signal, ui/App, view, widget, content, app
+ src/main/java/… froid.app.FregeActivity (the only Java; precompiled)
+froid-gradle-plugin/ the publishable Gradle plugin (id "io.github.mchav.froid"),
+ included as a build; wires Frege compilation into AGP
+examples/
+ counter/ minimal counter
+ geoquiz/ GeoQuiz — true/false quiz with color-coded feedback
+libs/ vendored Frege compiler snapshot
+```
+
+The top level holds no Android config — `android { … }` lives only in the module
+build files, not at the root.
+
+### Using froid in your own app
+
+```kotlin
+plugins {
+ id("com.android.application")
+ id("io.github.mchav.froid") // wires Frege compilation
+}
+dependencies { implementation("io.github.mchav:froid:…") }
+// write your UI in src/frege/, point the manifest at your module class
+```
+
+## Building
+
+### 1. Build the Frege compiler (one-time)
+
+froid needs the auto-FFI compiler from Frege `master` (PR #400; no release has it
+yet). The script pins a known-good commit and vendors the jar to `libs/`.
+
+**Requirements:** a JDK in the **8–17** range (17 recommended), `make`, `curl`,
+`jar`. `byacc` is not needed.
+
+```bash
+JAVA_HOME=/path/to/jdk17 tools/build-frege-snapshot.sh
+```
-native module type Activity where {}
+### 2. Build and run a sample
-onCreate :: Activity -> Maybe Bundle -> IO ()
-onCreate this bundle = do
- tv <- TextView.new this
- tv.setText "Hello, Android - Love, Frege"
- this.setContentView tv
+```bash
+./gradlew :geoquiz:assembleDebug
+./gradlew :geoquiz:installDebug # onto a running emulator/device
+# or open in Android Studio and press Run
```
-## Example
+`compileFrege` (from `froid-gradle-plugin`) runs the Frege compiler over
+each module's `src/frege`, generating Java that AGP compiles and dexes.
-You can find a more involved example [here](https://github.com/mchav/GeoQuiz-Frege). More will be available soon.
+## Status
+**Builds and runs.** `:geoquiz` and `:counter` assemble to APKs and run on an
+emulator (AGP 8.13, compileSdk 36): the question renders, True/False checks the
+answer with color-coded feedback, and Prev/Next navigate — all driven by the
+Frege FRP.
-## Building froid
+**Roadmap:** more widgets (EditText, images, lists with stable keys); the deeper
+FRP correctness items (subscription disposal for rotation, transactional
+glitch-freedom) are noted but not needed by the current static view tree; folding
+more of the original View bindings back in where useful (they remain in git
+history on `master`).
-Run `compile` and then `package`.
+Frege `master` is pinned at commit `067cad057385d36134812bde3fc1b453e5274855`.
-## Contributing
+## License
-A lot of what there is to do is create the bindings for the other types in `android`. For classes such as adapters/fragments read [this](http://mchav.github.io/functional-inheritance-in-android/) to learn about the design philosophy for subclassing. Any PRs of this nature are welcome.
+Apache License 2.0. froid is © Michael Chavinda and contributors.
diff --git a/build/META-INF/MANIFEST.MF b/build/META-INF/MANIFEST.MF
deleted file mode 100644
index b24b53d..0000000
--- a/build/META-INF/MANIFEST.MF
+++ /dev/null
@@ -1,3 +0,0 @@
-Manifest-Version: 1.0
-Created-By: 1.7.0_65 (Oracle Corporation)
-
diff --git a/compile b/compile
deleted file mode 100755
index aa50de0..0000000
--- a/compile
+++ /dev/null
@@ -1,97 +0,0 @@
-#!/bin/bash
-
-mkdir -p ./lib
-
-if [ ! -e ./lib/frege.jar ]
-then
- echo "Downloading Frege version 3.24..."
- wget -O ./lib/frege.jar https://github.com/Frege/frege/releases/download/3.24alpha/frege3.24-7.100.jar
-fi
-
-# default to Linux installation.
-ANDROID_SDK=$HOME"/Android/Sdk"
-
-unamestr=`uname`
-if [[ "$unamestr" == 'Darwin' ]]; then
- ANDROID_SDK=$HOME"/Library/Android/sdk"
-fi
-
-ANDROID_PLATFORMS=$ANDROID_SDK"/platforms"
-
-# get latest Android Version Number
-function get_latest_android_version() {
- versions=()
-
- for filename in $ANDROID_PLATFORMS/*; do
- version=${filename: -2}
- versions+=($version)
- done
-
- IFS=$'\n'
- echo "${versions[*]}" | sort -nr | head -n1
-
-}
-
-ANDROID_JAR_PATH=$ANDROID_PLATFORMS"/android-"$(get_latest_android_version)
-
-# echo $ANDROID_SDK
-
-rtjar=$(java -verbose 2>/dev/null | head -n 1 | cut -c 9- | rev | cut -c 2- | rev)
-
-# compile vanilla Java Source files.
-javac -cp "./lib/*:$ANDROID_JAR_PATH/*" -bootclasspath $rtjar -source 1.7 -target 1.7 -d ./build ./src/java/*
-
-fregec="java -Xmx6072m -Xss10M -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -cp \"./lib/*:$ANDROID_JAR_PATH/*\" frege.compiler.Main -target 1.7 -d ./build"
-
-# takes a list of files and compiles each of them.
-function compile_files {
- arr=("$@")
- for f in "${arr[@]}"; do
- eval $fregec "$f"
- done
-}
-
-# modules with no dependencies.
-declare -a no_deps=(
-"./src/frege/froid/view/View.fr"
-"./src/frege/froid/view/ViewGroup.fr"
-"./src/frege/froid/widget/CompoundButton.fr"
-"./src/frege/froid/view/Menu.fr"
-"./src/frege/froid/Types.fr"
-"./src/frege/froid/content/res"
-"./src/frege/froid/java/nio/IntBuffer.fr"
-"./src/frege/froid/animation"
-"./src/frege/froid/text/style"
-"./src/frege/froid/util"
-)
-
-# compile modules with no dependencies.
-compile_files "${no_deps[@]}"
-
-# modules with dependencies
-declare -a with_deps=(
-"./src/frege/froid/text/Editable.fr"
-"./src/frege/froid/text"
-"./src/frege/froid/content/Context.fr"
-"./src/frege/froid/content/Intent.fr"
-"./src/frege/froid/media"
-"./src/frege/froid/graphics"
-"./src/frege/froid/os"
-"./src/frege/froid/widget"
-"./src/frege/froid/view"
-"./src/frege/froid/content/res"
-"./src/frege/froid/app/java"
-"./src/frege/froid/app"
-"./src/frege/froid/app/Activity.fr"
-"./src/frege/froid/java"
-"./src/frege/froid/javax"
-"./src/frege/froid/opengl/glSurfaceView/renderer"
-"./src/frege/froid/opengl/glSurfaceView/java"
-"./src/frege/froid/opengl/glSurfaceView"
-"./src/frege/froid/opengl/GLSurfaceView.fr"
-)
-
-# compile modules with dependencies.
-compile_files "${with_deps[@]}"
-
-
diff --git a/compile.ps1 b/compile.ps1
deleted file mode 100644
index b0a2e94..0000000
--- a/compile.ps1
+++ /dev/null
@@ -1,47 +0,0 @@
-(mkdir build\froid\app) 2>&1> $null
-
-$jars = Get-ChildItem -File -Attributes !ReadOnly -path ".\lib" | % { $_.FullName }
-$cpJars = [string]::Join(";", $jars)
-
-($runtime = java -verbose) 2>&1> $null
-
-$s = ($runtime -split '\n')[0]
-$rtjar = $s.Substring(8, $s.length - 9)
-
-# compile frege
-$fregec = "java -Xmx6072m -Xss10M -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -cp `"$cpJars`" frege.compiler.Main -target 1.7 -d .\build"
-
-# compile java files
-javac -cp """$cpJars""" -bootclasspath """$rtjar""" -source 1.7 -target 1.7 -d .\build .\src\java\*
-
-# compile modules with no dependencies
-# Invoke-Expression "$fregec .\src\frege\froid\view\View.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\view\ViewGroup.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\widget\CompoundButton.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\view\Menu.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\Types.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\content\res"
-# Invoke-Expression "$fregec .\src\frege\froid\java\nio\IntBuffer.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\animation"
-# Invoke-Expression "$fregec .\src\frege\froid\text\style"
-# Invoke-Expression "$fregec .\src\frege\froid\util"
-
-# compile modules with already compiled dependencies
-Invoke-Expression "$fregec .\src\frege\froid\media"
-# Invoke-Expression "$fregec .\src\frege\froid\text\Editable.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\text"
-# Invoke-Expression "$fregec .\src\frege\froid\content\Context.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\content\Intent.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\graphics"
-# Invoke-Expression "$fregec .\src\frege\froid\os"
-# Invoke-Expression "$fregec .\src\frege\froid\widget"
-# Invoke-Expression "$fregec .\src\frege\froid\view"
-# Invoke-Expression "$fregec .\src\frege\froid\content\res"
-# Invoke-Expression "$fregec .\src\frege\froid\app\java"
-# Invoke-Expression "$fregec .\src\frege\froid\app"
-# Invoke-Expression "$fregec .\src\frege\froid\app\Activity.fr"
-# Invoke-Expression "$fregec .\src\frege\froid\java"
-# Invoke-Expression "$fregec .\src\frege\froid\javax"
-# Invoke-Expression "$fregec .\src\frege\froid\opengl\glSurfaceView\java"
-# Invoke-Expression "$fregec .\src\frege\froid\opengl\glSurfaceView"
-# Invoke-Expression "$fregec .\src\frege\froid\opengl\GLSurfaceView.fr"
diff --git a/examples/counter/build.gradle.kts b/examples/counter/build.gradle.kts
new file mode 100644
index 0000000..4196880
--- /dev/null
+++ b/examples/counter/build.gradle.kts
@@ -0,0 +1,32 @@
+plugins {
+ id("com.android.application")
+ id("io.github.mchav.froid")
+}
+
+android {
+ namespace = "io.github.mchav.froid.sample"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "io.github.mchav.froid.sample"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation(project(":froid"))
+}
diff --git a/examples/counter/src/frege/app/Counter.fr b/examples/counter/src/frege/app/Counter.fr
new file mode 100644
index 0000000..311a7b6
--- /dev/null
+++ b/examples/counter/src/frege/app/Counter.fr
@@ -0,0 +1,27 @@
+{-
+ A counter, in froid's Elm-architecture API.
+-}
+module app.Counter where
+
+import froid.app.Activity
+import froid.ui.App
+import froid.frp.Signal
+
+native module type Activity where {}
+
+data Msg = Increment
+
+update :: Msg -> Int -> Int
+update Increment n = n + 1
+
+view :: Signal Int -> Ui Msg
+view model = column
+ [ dynText (fmap (\n -> "Count: " ++ show n) model)
+ , button "Increment" Increment
+ ]
+
+counter :: App Int Msg
+counter = App { initial = 0, update = update, view = view }
+
+onCreate :: Activity -> IO ()
+onCreate this = runApp this counter
diff --git a/examples/counter/src/main/AndroidManifest.xml b/examples/counter/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..ec32ed8
--- /dev/null
+++ b/examples/counter/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/counter/src/main/res/values/strings.xml b/examples/counter/src/main/res/values/strings.xml
new file mode 100644
index 0000000..914d46a
--- /dev/null
+++ b/examples/counter/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ froid
+
diff --git a/examples/counter/src/main/res/values/themes.xml b/examples/counter/src/main/res/values/themes.xml
new file mode 100644
index 0000000..9453d5d
--- /dev/null
+++ b/examples/counter/src/main/res/values/themes.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/examples/geoquiz/build.gradle.kts b/examples/geoquiz/build.gradle.kts
new file mode 100644
index 0000000..17c5199
--- /dev/null
+++ b/examples/geoquiz/build.gradle.kts
@@ -0,0 +1,32 @@
+plugins {
+ id("com.android.application")
+ id("io.github.mchav.froid")
+}
+
+android {
+ namespace = "io.github.mchav.froid.geoquiz"
+ compileSdk = 36
+
+ defaultConfig {
+ applicationId = "io.github.mchav.froid.geoquiz"
+ minSdk = 24
+ targetSdk = 36
+ versionCode = 1
+ versionName = "0.1.0"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = false
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+}
+
+dependencies {
+ implementation(project(":froid"))
+}
diff --git a/examples/geoquiz/src/frege/app/GeoQuiz.fr b/examples/geoquiz/src/frege/app/GeoQuiz.fr
new file mode 100644
index 0000000..b90a718
--- /dev/null
+++ b/examples/geoquiz/src/frege/app/GeoQuiz.fr
@@ -0,0 +1,57 @@
+module app.GeoQuiz where
+
+import froid.app.Activity
+import froid.ui.App
+import froid.frp.Signal
+
+native module type Activity where {}
+
+onCreate :: Activity -> IO ()
+onCreate this = runApp this geoQuiz
+
+geoQuiz :: App Model Msg
+geoQuiz = App { initial = Model { idx = 0, feedback = Unanswered }, update = update, view = view }
+
+data Question = Question { prompt :: String, answer :: Bool }
+
+questions :: [Question]
+questions =
+ [ Question { prompt = "The Pacific is the largest ocean", answer = true }
+ , Question { prompt = "The Nile flows through South America", answer = false }
+ , Question { prompt = "Mount Everest is the tallest mountain", answer = true }
+ , Question { prompt = "The capital of Australia is Sydney", answer = false }
+ , Question { prompt = "Lake Baikal is the deepest lake on Earth", answer = true }
+ ]
+
+data Feedback = Unanswered | Correct | Wrong
+data Model = Model { idx :: Int, feedback :: Feedback }
+data Msg = Answer Bool | Move Int
+
+current :: Model -> Question
+current m = questions !! m.idx
+
+wrap :: Int -> Int -> Int
+wrap i n = ((i `mod` n) + n) `mod` n
+
+feedbackStyle :: Feedback -> (String, String)
+feedbackStyle Unanswered = ("", "#FFFFFF")
+feedbackStyle Correct = ("Correct!", "#1B5E20")
+feedbackStyle Wrong = ("Wrong!", "#B71C1C")
+
+update :: Msg -> Model -> Model
+update (Answer guess) m =
+ m.{ feedback = if guess == (current m).answer then Correct else Wrong }
+update (Move d) m =
+ m.{ idx = wrap (m.idx + d) (length questions), feedback = Unanswered }
+
+view :: Signal Model -> Ui Msg
+view model = column
+ [ flexSpacer
+ , headline (fmap (\m -> (current m).prompt) model)
+ , gap 16
+ , dynColoredText (fmap (\m -> feedbackStyle m.feedback) model)
+ , gap 32
+ , row [ primaryButton "True" (Answer true), primaryButton "False" (Answer false) ]
+ , flexSpacer
+ , row [ flatButton "Prev" (Move (-1)), flatButton "Next" (Move 1) ]
+ ]
diff --git a/examples/geoquiz/src/main/AndroidManifest.xml b/examples/geoquiz/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..3138d90
--- /dev/null
+++ b/examples/geoquiz/src/main/AndroidManifest.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/geoquiz/src/main/res/values/strings.xml b/examples/geoquiz/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4c3f911
--- /dev/null
+++ b/examples/geoquiz/src/main/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ GeoQuiz
+
diff --git a/examples/geoquiz/src/main/res/values/themes.xml b/examples/geoquiz/src/main/res/values/themes.xml
new file mode 100644
index 0000000..947f66b
--- /dev/null
+++ b/examples/geoquiz/src/main/res/values/themes.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/froid-gradle-plugin/build.gradle.kts b/froid-gradle-plugin/build.gradle.kts
new file mode 100644
index 0000000..f87745a
--- /dev/null
+++ b/froid-gradle-plugin/build.gradle.kts
@@ -0,0 +1,31 @@
+plugins {
+ `kotlin-dsl`
+ `maven-publish`
+}
+
+group = "io.github.mchav"
+version = "0.1.0"
+
+repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+}
+
+dependencies {
+ // The AGP variant API used by FregeAndroidPlugin. compileOnly is correct for
+ // an included/published Gradle plugin: the real AGP is on the consuming
+ // project's plugin classpath at apply time.
+ compileOnly("com.android.tools.build:gradle-api:8.13.2")
+}
+
+gradlePlugin {
+ plugins {
+ create("froid") {
+ id = "io.github.mchav.froid"
+ implementationClass = "io.github.mchav.froid.gradle.FregeAndroidPlugin"
+ displayName = "froid Frege/Android plugin"
+ description = "Compiles Frege sources in an Android project (the modern froid.gradle)."
+ }
+ }
+}
diff --git a/froid-gradle-plugin/settings.gradle.kts b/froid-gradle-plugin/settings.gradle.kts
new file mode 100644
index 0000000..d14f325
--- /dev/null
+++ b/froid-gradle-plugin/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = "froid-gradle-plugin"
diff --git a/froid-gradle-plugin/src/main/kotlin/io/github/mchav/froid/gradle/FregeAndroidPlugin.kt b/froid-gradle-plugin/src/main/kotlin/io/github/mchav/froid/gradle/FregeAndroidPlugin.kt
new file mode 100644
index 0000000..712b08d
--- /dev/null
+++ b/froid-gradle-plugin/src/main/kotlin/io/github/mchav/froid/gradle/FregeAndroidPlugin.kt
@@ -0,0 +1,115 @@
+package io.github.mchav.froid.gradle
+
+import com.android.build.api.variant.AndroidComponentsExtension
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.attributes.Attribute
+import org.gradle.api.tasks.compile.JavaCompile
+import org.gradle.kotlin.dsl.register
+
+/**
+ * Registers a per-variant `compileFrege` task and feeds its generated
+ * Java into the variant's sources. Apply with `apply()`
+ * AFTER an Android application/library plugin.
+ */
+class FregeAndroidPlugin : Plugin {
+ override fun apply(project: Project) {
+ val android = project.extensions.findByType(AndroidComponentsExtension::class.java)
+ ?: error("Apply an Android application/library plugin before FregeAndroidPlugin.")
+
+ // Android Studio's Kotlin-DSL sync runs `prepareKotlinBuildScriptModel`
+ // qualified to each module's project, but Gradle only registers the real
+ // task on the build root — so sync fails with "task not found in :module".
+ // Register a harmless no-op so the IDE's task lookup resolves; the script
+ // model itself is produced by Gradle's model builder, independently.
+ if (project.tasks.findByName("prepareKotlinBuildScriptModel") == null) {
+ project.tasks.register("prepareKotlinBuildScriptModel")
+ }
+
+ // The Frege compiler+runtime jar. In this repo it's vendored under libs/;
+ // for external consumers it is downloaded once and cached. Adding it to
+ // `implementation` puts the runtime (frege.run8.*) on the compile + runtime
+ // classpath of whichever module applies the plugin — so the published froid
+ // library needs no `api(files(...))`.
+ val fregeJar = resolveFregeCompiler(project)
+ project.dependencies.add("implementation", project.files(fregeJar))
+
+ val fregeSrc = project.layout.projectDirectory.dir("src/frege")
+
+ // Precompile any hand-written Java in src/main/java (e.g. the Activity base
+ // class) BEFORE Frege runs: Frege can't see classes generated within its
+ // own -make pass, so native types pointing at them must already exist.
+ val javaHelpers = project.layout.projectDirectory.dir("src/main/java")
+ val compileHelpers =
+ if (javaHelpers.asFile.exists()) {
+ project.tasks.register("compileFregeJavaHelpers") {
+ source(javaHelpers)
+ include("**/*.java")
+ classpath = project.files(fregeJar, android.sdkComponents.bootClasspath)
+ destinationDirectory.set(project.layout.buildDirectory.dir("frege-java-helpers"))
+ sourceCompatibility = "17"
+ targetCompatibility = "17"
+ }
+ } else {
+ null
+ }
+
+ val artifactType = Attribute.of("artifactType", String::class.java)
+
+ android.onVariants { variant ->
+ val suffix = variant.name.replaceFirstChar { it.uppercase() }
+ // Select the classes-jar artifacts (AGP exposes many variants per
+ // dependency: classes, lint, manifest, …). Without this view the raw
+ // CompileClasspath configuration is ambiguous to resolve.
+ val variantClasspath = project.configurations
+ .getByName("${variant.name}CompileClasspath")
+ .incoming.artifactView {
+ attributes.attribute(artifactType, "android-classes-jar")
+ }.files
+
+ val task = project.tasks.register("compileFrege$suffix") {
+ group = "frege"
+ description = "Compiles Frege sources for the ${variant.name} variant."
+ sourceDir.set(fregeSrc)
+ fregeCompiler.from(fregeJar)
+ compileClasspath.from(android.sdkComponents.bootClasspath)
+ compileClasspath.from(variantClasspath)
+ // Hand-written Java helpers must be on Frege's -fp (and built first).
+ compileHelpers?.let { helpers ->
+ compileClasspath.from(helpers.flatMap { it.destinationDirectory })
+ }
+ target.set("1.8")
+ outputDir.set(
+ project.layout.buildDirectory.dir("generated/frege/${variant.name}/java")
+ )
+ }
+
+ variant.sources.java?.addGeneratedSourceDirectory(task, FregeCompile::outputDir)
+ }
+ }
+
+ private companion object {
+ // The pinned Frege snapshot (compiler + runtime). Built by
+ // tools/build-frege-snapshot.sh; uploaded to a froid GitHub release.
+ const val FREGE_VERSION = "3.25.148-067cad05"
+ const val FREGE_URL =
+ "https://github.com/mchav/froid/releases/download/frege-$FREGE_VERSION/frege-compiler-snapshot.jar"
+ }
+
+ /** Vendored jar if present (this repo), else download once into the Gradle cache. */
+ private fun resolveFregeCompiler(project: Project): java.io.File {
+ val vendored = project.rootProject.file("libs/frege-compiler-snapshot.jar")
+ if (vendored.exists()) return vendored
+ val cached = java.io.File(project.gradle.gradleUserHomeDir, "caches/froid/frege-$FREGE_VERSION.jar")
+ if (!cached.exists()) {
+ project.logger.lifecycle("froid: downloading Frege compiler $FREGE_VERSION …")
+ cached.parentFile.mkdirs()
+ val part = java.io.File(cached.parentFile, "${cached.name}.part")
+ java.net.URI(FREGE_URL).toURL().openStream().use { input ->
+ part.outputStream().use { out -> input.copyTo(out) }
+ }
+ check(part.renameTo(cached)) { "froid: could not finalize $cached" }
+ }
+ return cached
+ }
+}
diff --git a/froid-gradle-plugin/src/main/kotlin/io/github/mchav/froid/gradle/FregeCompile.kt b/froid-gradle-plugin/src/main/kotlin/io/github/mchav/froid/gradle/FregeCompile.kt
new file mode 100644
index 0000000..869c6bd
--- /dev/null
+++ b/froid-gradle-plugin/src/main/kotlin/io/github/mchav/froid/gradle/FregeCompile.kt
@@ -0,0 +1,75 @@
+package io.github.mchav.froid.gradle
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.file.ConfigurableFileCollection
+import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.CacheableTask
+import org.gradle.api.tasks.Classpath
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputDirectory
+import org.gradle.api.tasks.OutputDirectory
+import org.gradle.api.tasks.PathSensitive
+import org.gradle.api.tasks.PathSensitivity
+import org.gradle.api.tasks.TaskAction
+import org.gradle.process.ExecOperations
+import javax.inject.Inject
+
+/**
+ * Runs the vendored Frege compiler (libs/frege-compiler-snapshot.jar) over a
+ * module's `src/main/frege` tree, emitting Java into [outputDir]. AGP then
+ * compiles that Java with javac as part of the variant. This replaces the old
+ * froid.gradle hooks (dexOptions/applicationVariants/bootClasspath), all removed
+ * in AGP 8.
+ */
+@CacheableTask
+abstract class FregeCompile : DefaultTask() {
+
+ @get:InputDirectory
+ @get:PathSensitive(PathSensitivity.RELATIVE)
+ abstract val sourceDir: DirectoryProperty
+
+ /** The Frege compiler+runtime jar; also on the JVM classpath that runs it. */
+ @get:Classpath
+ abstract val fregeCompiler: ConfigurableFileCollection
+
+ /** android.jar + library/compose deps, so Frege can resolve native types. */
+ @get:Classpath
+ abstract val compileClasspath: ConfigurableFileCollection
+
+ @get:Input
+ abstract val target: Property
+
+ @get:OutputDirectory
+ abstract val outputDir: DirectoryProperty
+
+ @get:Inject
+ abstract val execOperations: ExecOperations
+
+ @TaskAction
+ fun compile() {
+ val out = outputDir.get().asFile
+ // Clear stale generated Java (e.g. from a since-deleted .fr) — Frege's
+ // -make leaves outputs for removed sources, which then break javac.
+ out.deleteRecursively()
+ out.mkdirs()
+ val src = sourceDir.get().asFile
+
+ val runClasspath = fregeCompiler + compileClasspath
+ val fregePath = runClasspath.files.joinToString(System.getProperty("path.separator"))
+
+ execOperations.javaexec {
+ classpath = runClasspath
+ mainClass.set("frege.compiler.Main")
+ jvmArgs("-Xss8m")
+ args(
+ "-d", out.absolutePath,
+ "-target", target.get(),
+ "-make",
+ "-sp", src.absolutePath,
+ "-fp", fregePath,
+ src.absolutePath,
+ )
+ }
+ }
+}
diff --git a/froid.gradle b/froid.gradle
deleted file mode 100644
index 8e15c4b..0000000
--- a/froid.gradle
+++ /dev/null
@@ -1,65 +0,0 @@
-project.afterEvaluate {
- extensions.compileFrege = {
- description = 'Compile Frege to Java'
- javaexec {
- android.dexOptions.setJavaMaxHeapSize("4g")
- android.defaultConfig.setMultiDexEnabled(true)
-
- def libs = project.rootDir.path + "/app/libs".replace('/' as char, File.separatorChar)
-
- def froid = new File(libs + "/froid.jar".replace('/' as char, File.separatorChar))
- if (!froid.exists()) {
- new URL("https://github.com/mchav/froid/releases/download/v0.0.2/froid_0.0.2.jar")
- .withInputStream{ i ->
- froid.withOutputStream{ it << i }
- }
- }
- def froid_support = new File(libs + "/froid-support.jar".replace('/' as char, File.separatorChar))
- if (!froid_support.exists()) {
- new URL("https://github.com/mchav/froid-support/releases/download/v0.0.1/froid-support_0.0.1.jar")
- .withInputStream{ i ->
- froid_support.withOutputStream{ it << i }
- }
- }
- def frege = new File(libs + "/frege-3.24.100.1-jdk7.jar".replace('/' as char, File.separatorChar))
- if (!frege.exists()) {
- new URL("https://github.com/mchav/GeoQuiz-Frege/blob/master/app/libs/frege-3.24.100.1-jdk7.jar?raw=true")
- .withInputStream{ i ->
- frege.withOutputStream{ it << i }
- }
- }
-
- def frege_src = new File(project.rootDir.path + "/app/src/main/frege".replace('/' as char, File.separatorChar))
- frege_src.mkdirs()
-
- android.sourceSets.getByName("main").java.setSrcDirs([frege_src] + android.sourceSets.getByName("main").java.getSrcDirs())
-
- main = 'frege.compiler.Main'
-
- def androidJarPath = android.bootClasspath[0].path
- def list = [androidJarPath]
- classpath += files(androidJarPath)
-
- android.applicationVariants.each { variant ->
- variant.getCompileClasspath(null).each { path ->
- list << path
- classpath += files(path)
- }
- }
-
- def appPath = "src/main/frege/".replace('/' as char, File.separatorChar)
- def a = ['-j', '-target', '1.7', '-v', '-inline', '-O', '-d', 'src/main/java', '-make',
- '-fp', list.join(File.pathSeparator),
- '-sp', appPath, appPath ]
- project.logger.debug('Frege compiler args: "' + a.join(' ') + '"')
- args (*a)
- }
- }
- try {
- compileFrege()
- } catch (Exception e) {
- // all error handling is shown on the gradle console.
- System.err << "Frege compile failed.\n"
- System.err << e.toString() + "\n"
- }
-}
diff --git a/froid/build.gradle.kts b/froid/build.gradle.kts
new file mode 100644
index 0000000..043e33c
--- /dev/null
+++ b/froid/build.gradle.kts
@@ -0,0 +1,42 @@
+plugins {
+ id("com.android.library")
+ id("io.github.mchav.froid")
+ `maven-publish`
+}
+
+group = "io.github.mchav"
+version = project.findProperty("froidVersion") as String? ?: "0.1.0"
+
+android {
+ namespace = "io.github.mchav.froid"
+ compileSdk = 36
+
+ defaultConfig {
+ minSdk = 24
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ // Publish the release AAR (consumed as io.github.mchav:froid / via JitPack).
+ publishing {
+ singleVariant("release")
+ }
+}
+
+// The Frege runtime is provided to consumers by the `io.github.mchav.froid`
+// Gradle plugin (it downloads + adds the snapshot jar), so the library AAR needs
+// no Frege dependency of its own.
+
+afterEvaluate {
+ publishing {
+ publications {
+ register("release") {
+ from(components["release"])
+ artifactId = "froid"
+ }
+ }
+ }
+}
diff --git a/froid/src/frege/froid/app/Activity.fr b/froid/src/frege/froid/app/Activity.fr
new file mode 100644
index 0000000..8a9d7e8
--- /dev/null
+++ b/froid/src/frege/froid/app/Activity.fr
@@ -0,0 +1,19 @@
+{-
+ froid.app.Activity — the Activity an app subclasses.
+
+ An app module writes `native module type Activity where {}` (its generated
+ class then extends froid.app.FregeActivity) and a plain
+ `onCreate :: Activity -> IO ()`. No native-module Java in app code — the base
+ class (froid.app.FregeActivity, plain Java in the library) does the bridging.
+-}
+module froid.app.Activity where
+
+import froid.content.Context
+import froid.view.View
+
+data Activity = pure native "froid.app.FregeActivity"
+
+native getApplicationContext :: Activity -> IO Context
+native setContentView :: Activity -> View -> IO ()
+native setTitle :: Activity -> String -> IO ()
+native finish :: Activity -> IO ()
diff --git a/froid/src/frege/froid/content/Context.fr b/froid/src/frege/froid/content/Context.fr
new file mode 100644
index 0000000..cd7e56d
--- /dev/null
+++ b/froid/src/frege/froid/content/Context.fr
@@ -0,0 +1,4 @@
+module froid.content.Context where
+
+data ContextN = native "RealWorld android.content.Context"
+type Context = MutableIO ContextN
diff --git a/froid/src/frege/froid/frp/Signal.fr b/froid/src/frege/froid/frp/Signal.fr
new file mode 100644
index 0000000..6772f1e
--- /dev/null
+++ b/froid/src/frege/froid/frp/Signal.fr
@@ -0,0 +1,95 @@
+{-
+ froid.frp.Signal — a small discrete, push-based FRP core.
+
+ Two types:
+ * Event a — a stream of occurrences you can subscribe to.
+ * Signal a — a value that changes over time (sample the current value,
+ or subscribe to changes).
+
+ This is the foundation of froid's declarative widget layer (froid.ui.Widget):
+ button clicks are Events, widget contents are Signals. Single-threaded (the
+ Android UI thread), so cells are a plain native holder — no locking.
+-}
+module froid.frp.Signal where
+
+-- A mutable cell, backed by a java.util.concurrent.atomic.AtomicReference.
+data Cell a = pure native "java.util.concurrent.atomic.AtomicReference"
+native newCell new :: a -> IO (Cell a)
+native readCell get :: Cell a -> IO a
+native writeCell set :: Cell a -> a -> IO ()
+
+--- A stream of occurrences. Subscribe a handler to be run on each one.
+data Event a = Event { subscribeTo :: (a -> IO ()) -> IO () }
+
+--- A time-varying value: read the current value, and observe changes.
+data Signal a = Signal { sample :: IO a, changes :: Event a }
+
+instance Functor Event where
+ fmap f (Event s) = Event (\h -> s (\a -> h (f a)))
+
+instance Functor Signal where
+ fmap f (Signal smp chs) = Signal (fmap f smp) (fmap f chs)
+
+--- An event that never fires.
+never :: Event a
+never = Event (const (return ()))
+
+--- Merge two event streams: the result fires on each occurrence of either.
+mergeE :: Event a -> Event a -> Event a
+mergeE (Event s1) (Event s2) = Event (\h -> do { s1 h; s2 h })
+
+--- Merge a list of event streams.
+mergeAll :: [Event a] -> Event a
+mergeAll = fold mergeE never
+
+--- Keep only the occurrences satisfying the predicate.
+filterE :: (a -> Bool) -> Event a -> Event a
+filterE p (Event sub) = Event (\h -> sub (\a -> if p a then h a else return ()))
+
+--- Sample a Signal's current value at each Event occurrence, combining the two.
+--- (reflex's `attachWith`.) Use it to read state when a button is pressed.
+snapshot :: (a -> b -> c) -> Event a -> Signal b -> Event c
+snapshot f (Event sub) sig = Event (\h -> sub (\a -> do { b <- sig.sample; h (f a b) }))
+
+--- Sample a Signal at each Event occurrence, discarding the event's own value.
+tag :: Signal b -> Event a -> Event b
+tag sig ev = snapshot (\_ b -> b) ev sig
+
+--- Create an event source: an Event together with a function that fires it.
+newEvent :: IO (Event a, a -> IO ())
+newEvent = do
+ subs <- newCell []
+ -- Append on subscribe (rare) so `fire` (frequent) needs no per-emission reverse.
+ let subscribe h = do
+ hs <- readCell subs
+ writeCell subs (hs ++ [h])
+ let fire a = do
+ hs <- readCell subs
+ mapM_ (\h -> h a) hs
+ return (Event subscribe, fire)
+
+--- Fold an event stream into a Signal (the Elm `foldp`): each occurrence is a
+--- state transition applied to the running value.
+accumS :: b -> Event (b -> b) -> IO (Signal b)
+accumS initial ev = do
+ cell <- newCell initial
+ (changed, fireChanged) <- newEvent
+ ev.subscribeTo (\f -> do
+ old <- readCell cell
+ let new = f old
+ -- Force the accumulator strictly: otherwise the cell stores a growing
+ -- thunk chain f_n (… (f_0 initial)) — a foldl space leak.
+ writeCell cell $! new
+ fireChanged new)
+ return (Signal (readCell cell) changed)
+
+--- A Signal that always holds the most recent event value.
+stepper :: a -> Event a -> IO (Signal a)
+stepper initial ev = accumS initial (fmap const ev)
+
+--- Run a handler with the current value immediately, then on every change.
+react :: Signal a -> (a -> IO ()) -> IO ()
+react sig h = do
+ v <- sig.sample
+ h v
+ sig.changes.subscribeTo h
diff --git a/froid/src/frege/froid/ui/App.fr b/froid/src/frege/froid/ui/App.fr
new file mode 100644
index 0000000..1f206c2
--- /dev/null
+++ b/froid/src/frege/froid/ui/App.fr
@@ -0,0 +1,195 @@
+{-
+ froid.ui.App — the Elm Architecture for froid.
+
+ You give an `App` an initial model, a pure `update :: msg -> model -> model`,
+ and a `view :: Signal model -> Ui msg`. Buttons are placed where they belong
+ and carry the message they dispatch. State lives in one place (`update`).
+ Built on froid.frp.Signal: the model is a `Signal`, so only the labels whose
+ text actually changed are redrawn.
+-}
+module froid.ui.App where
+
+import froid.frp.Signal
+import froid.app.Activity
+import froid.view.View
+import froid.content.Context
+import froid.widget.Widgets
+
+--- A piece of UI: given a Context and a way to dispatch messages, build a View.
+data Ui msg = Ui { build :: Context -> (msg -> IO ()) -> IO View }
+
+--- The Elm Architecture.
+data App model msg = App
+ { initial :: model
+ , update :: msg -> model -> model
+ , view :: Signal model -> Ui msg
+ }
+
+--- Run an App as the content of an Activity.
+runApp :: Activity -> App model msg -> IO ()
+runApp activity app = do
+ (messages, dispatch) <- newEvent
+ model <- accumS app.initial (fmap app.update messages)
+ ctx <- getApplicationContext activity
+ rootView <- (app.view model).build ctx dispatch
+ setContentView activity rootView
+
+--- Convert dp to px for the current screen density.
+dp :: Context -> Int -> IO Int
+dp ctx n = do
+ res <- getResources ctx
+ m <- getDisplayMetrics res
+ px <- applyDimension complexUnitDip (fromIntegral n) m
+ return px.int
+
+bodyTextSize :: Float
+bodyTextSize = 18.0
+
+headlineTextSize :: Float
+headlineTextSize = 26.0
+
+--- A bold, centered headline that wraps nicely — for the main question.
+headline :: Signal String -> Ui msg
+headline sig = Ui (\ctx -> \_ -> do
+ tv <- newTextView ctx
+ setTextSize tv headlineTextSize
+ setTypeface tv typefaceBold
+ setTextColor tv (parseColor "#202124")
+ setTextGravity tv gravityCenterAll
+ maxW <- dp ctx 320
+ setMaxWidth tv maxW
+ bindText tv sig
+ textViewAsView tv)
+
+--- Static text.
+text :: String -> Ui msg
+text s = Ui (\ctx -> \_ -> do
+ tv <- newTextView ctx
+ setTextSize tv bodyTextSize
+ setTextGravity tv gravityCenterAll
+ setText tv s
+ textViewAsView tv)
+
+--- Text bound to a Signal; redraws only when the string changes.
+dynText :: Signal String -> Ui msg
+dynText sig = Ui (\ctx -> \_ -> do
+ tv <- newTextView ctx
+ setTextSize tv bodyTextSize
+ setTextGravity tv gravityCenterAll
+ bindText tv sig
+ textViewAsView tv)
+
+--- Bold dynamic text whose colour also tracks the signal (text, "#RRGGBB").
+dynColoredText :: Signal (String, String) -> Ui msg
+dynColoredText sig = Ui (\ctx -> \_ -> do
+ tv <- newTextView ctx
+ setTextSize tv bodyTextSize
+ setTypeface tv typefaceBold
+ setTextGravity tv gravityCenterAll
+ react sig (\(s, hex) -> do
+ setText tv s
+ setTextColor tv (parseColor hex))
+ textViewAsView tv)
+
+-- distinct-until-changed setText
+private bindText tv sig = do
+ seen <- newCell Nothing
+ react sig (\s -> do
+ prev <- readCell seen
+ if prev == Just s
+ then return ()
+ else do
+ writeCell seen (Just s)
+ setText tv s)
+
+--- A plain button that dispatches `msg` when pressed.
+button :: String -> msg -> Ui msg
+button lbl msg = Ui (\ctx -> \dispatch -> do
+ b <- newButton ctx
+ setText b lbl
+ bv <- buttonAsView b
+ onClick bv (\_ -> dispatch msg)
+ return bv)
+
+-- shared button wiring: build, label, min-height, click → dispatch.
+private mkButton ctx dispatch lbl msg minDp = do
+ b <- newButton ctx
+ setText b lbl
+ btnSetAllCaps b false
+ mh <- dp ctx minDp
+ btnSetMinHeight b mh
+ bv <- buttonAsView b
+ onClick bv (\_ -> dispatch msg)
+ return (b, bv)
+
+--- A filled, primary action button (for the main choices).
+primaryButton :: String -> msg -> Ui msg
+primaryButton lbl msg = Ui (\ctx -> \dispatch -> do
+ (b, bv) <- mkButton ctx dispatch lbl msg 56
+ btnSetTypeface b typefaceBold
+ btnSetTextColor b (parseColor "#FFFFFF")
+ r <- dp ctx 12
+ bg <- newGradientDrawable ()
+ gdSetColor bg (parseColor "#1565C0")
+ gdSetCornerRadius bg (fromIntegral r)
+ btnSetBackground b bg
+ return bv)
+
+--- A flat, outlined, secondary button (for navigation).
+flatButton :: String -> msg -> Ui msg
+flatButton lbl msg = Ui (\ctx -> \dispatch -> do
+ (b, bv) <- mkButton ctx dispatch lbl msg 48
+ btnSetTextColor b (parseColor "#1565C0")
+ r <- dp ctx 12
+ sw <- dp ctx 1
+ bg <- newGradientDrawable ()
+ gdSetColor bg (parseColor "#00FFFFFF") -- transparent fill
+ gdSetStroke bg sw (parseColor "#D0D0D0")
+ gdSetCornerRadius bg (fromIntegral r)
+ btnSetBackground b bg
+ return bv)
+
+--- A flexible spacer that expands to push following content away (e.g. nav to the bottom).
+flexSpacer :: Ui msg
+flexSpacer = Ui (\ctx -> \_ -> do
+ v <- newView ctx
+ lp <- newWeightedLayoutParams matchParent 0 1.0
+ setLayoutParams v lp
+ return v)
+
+--- Empty vertical space of `n` dp.
+gap :: Int -> Ui msg
+gap n = Ui (\ctx -> \_ -> do
+ v <- newView ctx
+ h <- dp ctx n
+ lp <- newLayoutParams matchParent h
+ setLayoutParams v lp
+ return v)
+
+--- The screen root: fills the screen, content vertically centred, padded.
+column :: [Ui msg] -> Ui msg
+column children = Ui (\ctx -> \dispatch -> do
+ ll <- newLinearLayout ctx
+ setOrientation ll vertical
+ setGravity ll gravityCenterAll
+ pad <- dp ctx 24
+ setPadding ll pad pad pad pad
+ mapM_ (\child -> child.build ctx dispatch >>= addView ll) children
+ layoutAsView ll)
+
+--- Horizontal row with equal-width children and an even gap between them.
+row :: [Ui msg] -> Ui msg
+row children = Ui (\ctx -> \dispatch -> do
+ ll <- newLinearLayout ctx
+ setOrientation ll horizontal
+ g <- dp ctx 6
+ mapM_ (\child -> do
+ v <- child.build ctx dispatch
+ lp <- newWeightedLayoutParams 0 wrapContent 1.0
+ setMargins lp g 0 g 0
+ setLayoutParams v lp
+ addView ll v) children
+ rv <- layoutAsView ll
+ rlp <- newLayoutParams matchParent wrapContent
+ setLayoutParams rv rlp
+ return rv)
diff --git a/froid/src/frege/froid/ui/Widget.fr b/froid/src/frege/froid/ui/Widget.fr
new file mode 100644
index 0000000..e51e16d
--- /dev/null
+++ b/froid/src/frege/froid/ui/Widget.fr
@@ -0,0 +1,70 @@
+{-
+ froid.ui.Widget — a small, Compose-like declarative UI layer, built on the
+ Android View system and driven by froid's FRP (froid.frp.Signal).
+
+ A `Widget` is a recipe that, given a Context, materialises a View. Static
+ content is a plain value; reactive content is a `Signal`; user input (a
+ button press) is an `Event`. You compose widgets with `column`/`row`, and
+ the runtime keeps views in sync with their signals — no manual findViewById,
+ no Kotlin, no Jetpack Compose.
+-}
+module froid.ui.Widget where
+
+import froid.frp.Signal
+import froid.app.Activity
+import froid.view.View
+import froid.content.Context
+import froid.widget.Widgets
+
+--- A recipe that builds a View under a Context.
+type Widget = Context -> IO View
+
+--- Build a widget and install it as the activity's content view.
+render :: Activity -> Widget -> IO ()
+render activity widget = do
+ ctx <- getApplicationContext activity
+ view <- widget ctx
+ setContentView activity view
+
+--- Static text.
+label :: String -> Widget
+label s ctx = do
+ tv <- newTextView ctx
+ setText tv s
+ textViewAsView tv
+
+--- Text bound to a Signal: updates whenever the signal changes.
+dynLabel :: Signal String -> Widget
+dynLabel sig ctx = do
+ tv <- newTextView ctx
+ react sig (setText tv)
+ textViewAsView tv
+
+--- A button labelled `lbl`. Returns the widget plus an Event that fires on each
+--- press — wire it into a Signal with `accumS`.
+button :: String -> IO (Widget, Event ())
+button lbl = do
+ (clicks, fire) <- newEvent
+ let build ctx = do
+ b <- newButton ctx
+ setText b lbl
+ bv <- buttonAsView b
+ onClick bv (\_ -> fire ())
+ return bv
+ return (build, clicks)
+
+--- Stack children vertically.
+column :: [Widget] -> Widget
+column ws ctx = do
+ ll <- newLinearLayout ctx
+ setOrientation ll vertical
+ mapM_ (\w -> w ctx >>= addView ll) ws
+ layoutAsView ll
+
+--- Stack children horizontally.
+row :: [Widget] -> Widget
+row ws ctx = do
+ ll <- newLinearLayout ctx
+ setOrientation ll horizontal
+ mapM_ (\w -> w ctx >>= addView ll) ws
+ layoutAsView ll
diff --git a/froid/src/frege/froid/view/View.fr b/froid/src/frege/froid/view/View.fr
new file mode 100644
index 0000000..ddcdcd4
--- /dev/null
+++ b/froid/src/frege/froid/view/View.fr
@@ -0,0 +1,44 @@
+{-
+ froid.view.View — the android.view.View handle and click wiring.
+
+ `data XN = native "RealWorld …"` declares the Java handle (so auto-FFI can
+ generate method bindings on demand); `type X = MutableIO XN` gives the clean
+ name used in signatures. The OnClickListener bridge is plain Java in a native
+ module (the original froid style) — no Kotlin.
+-}
+module froid.view.View where
+
+data ViewN = native "RealWorld android.view.View"
+type View = MutableIO ViewN
+
+data OnClickListenerN = native "RealWorld android.view.View.OnClickListener"
+type OnClickListener = MutableIO OnClickListenerN
+
+native setOnClickListener :: View -> OnClickListener -> IO ()
+native mkOnClickListener "froid.view.View.onClickHandler" :: (View -> IO ()) -> IO OnClickListener
+
+pure native visible "android.view.View.VISIBLE" :: Int
+pure native invisible "android.view.View.INVISIBLE" :: Int
+pure native gone "android.view.View.GONE" :: Int
+
+native setVisibility :: View -> Int -> IO ()
+
+--- Run a Frege handler when the view is clicked.
+onClick :: View -> (View -> IO ()) -> IO ()
+onClick v handler = do
+ listener <- mkOnClickListener handler
+ setOnClickListener v listener
+
+native module where {
+ public static android.view.View.OnClickListener
+ onClickHandler(final frege.run8.Func.U handler) {
+ return new android.view.View.OnClickListener() {
+ @Override
+ public void onClick(android.view.View view) {
+ frege.prelude.PreludeBase.TST.performUnsafe(
+ (frege.run8.Func.U)(handler.apply(frege.run8.Thunk.lazy(view)).call())
+ ).call();
+ }
+ };
+ }
+}
diff --git a/froid/src/frege/froid/widget/Widgets.fr b/froid/src/frege/froid/widget/Widgets.fr
new file mode 100644
index 0000000..471f469
--- /dev/null
+++ b/froid/src/frege/froid/widget/Widgets.fr
@@ -0,0 +1,88 @@
+{-
+ froid.widget.Widgets — thin handles over the common android.widget views.
+
+ With auto-FFI most instance methods (setText, addView, setOrientation, …)
+ need no declaration — they are generated on demand from the calls in the
+ UI layer. Constructors and the View upcasts are declared explicitly.
+-}
+module froid.widget.Widgets where
+
+import froid.content.Context
+import froid.view.View
+
+data TextViewN = native "RealWorld android.widget.TextView"
+type TextView = MutableIO TextViewN
+data ButtonN = native "RealWorld android.widget.Button"
+type Button = MutableIO ButtonN
+data LinearLayoutN = native "RealWorld android.widget.LinearLayout"
+type LinearLayout = MutableIO LinearLayoutN
+
+native newTextView new :: Context -> IO TextView
+native newButton new :: Context -> IO Button
+native newLinearLayout new :: Context -> IO LinearLayout
+
+native setText :: TextView -> String -> IO ()
+ | Button -> String -> IO ()
+native setTextSize :: TextView -> Float -> IO ()
+native setOrientation :: LinearLayout -> Int -> IO ()
+native setGravity :: LinearLayout -> Int -> IO ()
+native setPadding :: LinearLayout -> Int -> Int -> Int -> Int -> IO ()
+native addView :: LinearLayout -> View -> IO ()
+
+pure native horizontal "android.widget.LinearLayout.HORIZONTAL" :: Int
+pure native vertical "android.widget.LinearLayout.VERTICAL" :: Int
+pure native gravityCenter "android.view.Gravity.CENTER_HORIZONTAL" :: Int
+pure native gravityCenterAll "android.view.Gravity.CENTER" :: Int
+
+-- Text styling.
+native setTextColor :: TextView -> Int -> IO ()
+native setTextGravity "setGravity" :: TextView -> Int -> IO ()
+native setMaxWidth :: TextView -> Int -> IO ()
+native setTypeface :: TextView -> Typeface -> IO ()
+
+data Typeface = pure native "android.graphics.Typeface"
+pure native typefaceBold "android.graphics.Typeface.DEFAULT_BOLD" :: Typeface
+
+-- LayoutParams: width/height/weight + margins (the core of real layout).
+data LayoutParamsN = native "RealWorld android.widget.LinearLayout.LayoutParams"
+type LayoutParams = MutableIO LayoutParamsN
+native newLayoutParams "new android.widget.LinearLayout.LayoutParams" :: Int -> Int -> IO LayoutParams
+native newWeightedLayoutParams "new android.widget.LinearLayout.LayoutParams" :: Int -> Int -> Float -> IO LayoutParams
+native setMargins :: LayoutParams -> Int -> Int -> Int -> Int -> IO ()
+native setLayoutParams :: View -> LayoutParams -> IO ()
+pure native matchParent "android.view.ViewGroup.LayoutParams.MATCH_PARENT" :: Int
+pure native wrapContent "android.view.ViewGroup.LayoutParams.WRAP_CONTENT" :: Int
+
+-- An empty View, used as a weighted spacer.
+native newView "new android.view.View" :: Context -> IO View
+native setBackgroundColor :: View -> Int -> IO ()
+pure native parseColor "android.graphics.Color.parseColor" :: String -> Int
+
+-- Button styling + drawables (primary/secondary buttons).
+native btnSetAllCaps "setAllCaps" :: Button -> Bool -> IO ()
+native btnSetTypeface "setTypeface" :: Button -> Typeface -> IO ()
+native btnSetTextColor "setTextColor" :: Button -> Int -> IO ()
+native btnSetMinHeight "setMinimumHeight" :: Button -> Int -> IO ()
+native btnSetBackground "setBackground" :: Button -> GradientDrawable -> IO ()
+
+data GradientDrawableN = native "RealWorld android.graphics.drawable.GradientDrawable"
+type GradientDrawable = MutableIO GradientDrawableN
+native newGradientDrawable "new android.graphics.drawable.GradientDrawable" :: () -> IO GradientDrawable
+native gdSetColor "setColor" :: GradientDrawable -> Int -> IO ()
+native gdSetCornerRadius "setCornerRadius" :: GradientDrawable -> Float -> IO ()
+native gdSetStroke "setStroke" :: GradientDrawable -> Int -> Int -> IO ()
+
+-- dp -> px (so spacing is density-independent).
+data ResourcesN = native "android.content.res.Resources"
+type Resources = MutableIO ResourcesN
+data DisplayMetricsN = native "android.util.DisplayMetrics"
+type DisplayMetrics = MutableIO DisplayMetricsN
+native getResources :: Context -> IO Resources
+native getDisplayMetrics :: Resources -> IO DisplayMetrics
+native applyDimension "android.util.TypedValue.applyDimension" :: Int -> Float -> DisplayMetrics -> IO Float
+pure native complexUnitDip "android.util.TypedValue.COMPLEX_UNIT_DIP" :: Int
+
+-- Safe upcasts to View (every widget is-a View).
+native textViewAsView "(android.view.View)" :: TextView -> IO View
+native buttonAsView "(android.view.View)" :: Button -> IO View
+native layoutAsView "(android.view.View)" :: LinearLayout -> IO View
diff --git a/froid/src/main/AndroidManifest.xml b/froid/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..b2d3ea1
--- /dev/null
+++ b/froid/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
diff --git a/froid/src/main/java/froid/app/FregeActivity.java b/froid/src/main/java/froid/app/FregeActivity.java
new file mode 100644
index 0000000..207ecf4
--- /dev/null
+++ b/froid/src/main/java/froid/app/FregeActivity.java
@@ -0,0 +1,42 @@
+package froid.app;
+
+/**
+ * The base Activity that froid apps subclass (via `native module type Activity`).
+ * It bridges Android's onCreate to the app's Frege `onCreate :: Activity -> IO ()`
+ * — so app code never writes a native-module block. This is plain Java and lives
+ * in the library precisely so it can be compiled before the Frege compiler runs
+ * (Frege can't see classes generated within its own build pass).
+ */
+public class FregeActivity extends android.app.Activity {
+
+ @Override
+ public void onCreate(android.os.Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ try {
+ java.lang.reflect.Method onCreate = null;
+ for (java.lang.reflect.Method m : this.getClass().getDeclaredMethods()) {
+ if (m.getName().equals("onCreate") && m.getParameterCount() == 1) {
+ onCreate = m;
+ break;
+ }
+ }
+ if (onCreate == null) {
+ android.util.Log.e("froid",
+ "No `onCreate :: Activity -> IO ()` found in " + this.getClass().getName());
+ return;
+ }
+ Object arg = onCreate.getParameterTypes()[0].isAssignableFrom(android.app.Activity.class)
+ ? this : frege.run8.Thunk.lazy(this);
+ Object io = onCreate.invoke(null, arg);
+ if (io instanceof frege.run8.Lazy) {
+ io = ((frege.run8.Lazy>) io).call();
+ }
+ @SuppressWarnings("unchecked")
+ frege.run8.Func.U action =
+ (frege.run8.Func.U) io;
+ frege.prelude.PreludeBase.TST.performUnsafe(action).call();
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..a294f44
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
+org.gradle.caching=true
+org.gradle.parallel=true
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..2c35211
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37f853b
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..d95bf61
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,252 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
+' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..640d686
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,94 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=-Dfile.encoding=UTF-8 "-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/libs/frege-compiler-snapshot.jar b/libs/frege-compiler-snapshot.jar
new file mode 100644
index 0000000..2da39ce
Binary files /dev/null and b/libs/frege-compiler-snapshot.jar differ
diff --git a/package b/package
deleted file mode 100755
index aaf9b30..0000000
--- a/package
+++ /dev/null
@@ -1,3 +0,0 @@
-cd build
-jar -cMf ../froid_0.0.2.jar ./froid ./META-INF ./me
-
diff --git a/package.bat b/package.bat
deleted file mode 100644
index aedc0cb..0000000
--- a/package.bat
+++ /dev/null
@@ -1,3 +0,0 @@
-@ECHO OFF
-CD build
-jar -cMf ..\froid_0.0.2.jar .\froid .\META-INF .\me
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..9e48613
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,35 @@
+pluginManagement {
+ // The froid Frege plugin lives in its own (publishable) build; include it so
+ // modules can apply it by id without a version.
+ includeBuild("froid-gradle-plugin")
+ repositories {
+ google {
+ content {
+ includeGroupByRegex("com\\.android.*")
+ includeGroupByRegex("com\\.google.*")
+ includeGroupByRegex("androidx.*")
+ }
+ }
+ mavenCentral()
+ gradlePluginPortal()
+ }
+ plugins {
+ id("com.android.application") version "8.13.2"
+ id("com.android.library") version "8.13.2"
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "froid"
+include(":froid")
+include(":counter")
+project(":counter").projectDir = file("examples/counter")
+include(":geoquiz")
+project(":geoquiz").projectDir = file("examples/geoquiz")
diff --git a/src/frege/froid/Types.fr b/src/frege/froid/Types.fr
deleted file mode 100644
index 1433d41..0000000
--- a/src/frege/froid/Types.fr
+++ /dev/null
@@ -1,16 +0,0 @@
-module froid.Types where
-
-import froid.view.View
-
-data ClassCastException = mutable native java.lang.ClassCastException
-
-data SerializableObject = mutable native java.io.Serializable
-
-class ViewSub a where
- fromView' :: View -> IO (ClassCastException | a)
-
-class IsView a where
- toView' :: a -> IO (ClassCastException | View)
-
-class IsViewHolder a where
- itemView' :: a -> IO View
diff --git a/src/frege/froid/animation/Animator.fr b/src/frege/froid/animation/Animator.fr
deleted file mode 100644
index f7f7933..0000000
--- a/src/frege/froid/animation/Animator.fr
+++ /dev/null
@@ -1,39 +0,0 @@
-{-
- Copyright 2016-2017 Michael Chavinda
- This file is part of froid.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
--}
-
-module froid.animation.Animator where
-
-data Animator = mutable native android.animation.Animator where
- private native addListener :: Animator -> AnimatorListenerAdapter -> IO ()
- native start :: Animator -> IO ()
- private native onAnimationEnd' "froid.animation.Animator.onAnimationEndHandler" :: (Animator -> IO ()) -> IO AnimatorListenerAdapter
- onAnimationEnd :: Animator -> (Animator -> IO ()) -> IO ()
- onAnimationEnd animation lambda = do
- listener <- Animator.onAnimationEnd' lambda
- animation.addListener listener
-
-data AnimatorListenerAdapter = mutable native android.animation.AnimatorListenerAdapter
-
-
-native module where {
- public static android.animation.AnimatorListenerAdapter
- onAnimationEndHandler(final Func.U> lambda) {
- return (new android.animation.AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(android.animation.Animator animation) {
- super.onAnimationEnd(animation);
- try {
- PreludeBase.TST.performUnsafe(
- lambda.apply(Thunk.lazy(animation)).call()).call();
- } catch(RuntimeException re) {
- re.printStackTrace();
- throw re;
- }
- }
- });
- }
-}
\ No newline at end of file
diff --git a/src/frege/froid/app/ActionBar.fr b/src/frege/froid/app/ActionBar.fr
deleted file mode 100644
index 4904327..0000000
--- a/src/frege/froid/app/ActionBar.fr
+++ /dev/null
@@ -1,14 +0,0 @@
-{-
- Copyright 2016-2017 Michael Chavinda
- This file is part of froid.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
--}
-
-module froid.app.ActionBar where
-
-data ActionBar = mutable native android.app.ActionBar where
- native setIcon :: ActionBar -> Int -> IO ()
- native setLogo :: ActionBar -> Int -> IO ()
- native setDisplayShowHomeEnabled :: ActionBar -> Bool -> IO ()
- native setDisplayUseLogoEnabled :: ActionBar -> Bool -> IO ()
diff --git a/src/frege/froid/app/Activity.fr b/src/frege/froid/app/Activity.fr
deleted file mode 100644
index c5a9038..0000000
--- a/src/frege/froid/app/Activity.fr
+++ /dev/null
@@ -1,30 +0,0 @@
-{-
- Copyright 2016-2017 Michael Chavinda
- This file is part of froid.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
--}
-
-module froid.app.Activity where
-
-import froid.content.Context
-import froid.content.Intent
-import froid.os.Bundle
-import froid.view.View
-import froid.widget.Toolbar
-
-data Activity = mutable native froid.app.java.NativeActivity.FregeActivity where
- pure native resultOk "android.app.Activity.RESULT_OK" :: Int
- native getApplicationContext :: Activity -> IO Context
- native getIntent :: Activity -> IO Intent -- very few cases when this is null http://stackoverflow.com/questions/37856407/can-activity-getandroid.content.Intent-ever-return-null
- native findViewById :: Activity -> Int -> IO View
- native setContentView :: Activity -> Int -> IO ()
- | Activity -> View -> IO ()
- native setActionBar :: Activity -> Toolbar -> IO ()
- native setResult :: Activity -> Int -> Intent -> IO ()
- native startActivityForResult :: Activity -> Intent -> Int -> IO ()
- -- new methods
- native setOnPause :: Activity -> IO () -> IO ()
- native setOnResume :: Activity -> IO () -> IO ()
- native setOnSavedInstanceState :: Activity -> (Bundle -> IO ()) -> IO ()
- native setOnActivityResult :: Activity -> (Int -> Int -> Maybe Intent -> IO ()) -> IO ()
diff --git a/src/frege/froid/app/Dialog.fr b/src/frege/froid/app/Dialog.fr
deleted file mode 100644
index b0707a6..0000000
--- a/src/frege/froid/app/Dialog.fr
+++ /dev/null
@@ -1,10 +0,0 @@
-{-
- Copyright 2016-2017 Michael Chavinda
- This file is part of froid.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
--}
-
-module froid.app.Dialog where
-
-data Dialog = mutable native android.app.Dialog
\ No newline at end of file
diff --git a/src/frege/froid/app/java/NativeActivity.fr b/src/frege/froid/app/java/NativeActivity.fr
deleted file mode 100644
index 3fa1224..0000000
--- a/src/frege/froid/app/java/NativeActivity.fr
+++ /dev/null
@@ -1,107 +0,0 @@
-{-
- Copyright 2016-2017 Michael Chavinda
- This file is part of froid.
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
--}
-
-module froid.app.java.NativeActivity where
-
-native module where {
- public static class FregeActivity extends android.app.Activity {
- Func.U onPauseLambda = null;
- Func.U onResumeLambda = null;
- Func.U> onSavedInstanceStateLambda = null;
- Func.U, Func.U>>> onActivityResultLambda = null;
-
- // executes io action given as parameter
- public void setOnPause(Func.U lambda) {
- this.onPauseLambda = lambda;
- }
-
- public void setOnResume(Func.U lambda) {
- this.onResumeLambda = lambda;
- }
-
- public void setOnSavedInstanceState(Func.U> lambda) {
- this.onSavedInstanceStateLambda = lambda;
- }
-
- public void setOnActivityResult(Func.U, Func.U>>> onActivityResultLambda) {
- this.onActivityResultLambda = onActivityResultLambda;
- }
-
- // reflection methods
- private Object invokeStaticActivityMethod(String methodName, Object[] args, String signature) {
- java.lang.reflect.Method fregeMethod = null;
- try {
- fregeMethod = this.getClass().getDeclaredMethod(methodName, FregeActivity.class, Lazy.class);
- } catch (NoSuchMethodException nsm) {
- android.util.Log.e("FROID SYSTEM",
- "Method " + methodName + " is not defined. Make sure your onCreate Function is defined as " + signature,
- nsm);
- this.finishAffinity();
- }
-
- Object invokedMethod = null;
-
- try {
- invokedMethod = fregeMethod.invoke(null, args);
- } catch (Exception e) { // TODO: consult Frege documentation
- android.util.Log.e("FROID SYSTEM", "Failed to call " + methodName, e);
- this.finishAffinity();
- }
- return invokedMethod;
- }
-
- private Object run(Object invokedMethod) {
- if (invokedMethod == null) return null;
- Func.U