diff --git a/app/src/processing/app/ui/Welcome.java b/app/ant/processing/app/ui/Welcome.java
similarity index 100%
rename from app/src/processing/app/ui/Welcome.java
rename to app/ant/processing/app/ui/Welcome.java
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 865296d135..c4ffaff4de 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,6 @@
 import org.gradle.kotlin.dsl.support.zipTo
 import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
+import org.jetbrains.compose.ExperimentalComposeLibrary
 import org.jetbrains.compose.desktop.application.dsl.TargetFormat
 import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
 import org.jetbrains.compose.internal.de.undercouch.gradle.tasks.download.Download
@@ -59,7 +60,7 @@ compose.desktop {
         ).map { "-D${it.first}=${it.second}" }.toTypedArray())
 
         nativeDistributions{
-            modules("jdk.jdi", "java.compiler", "jdk.accessibility", "java.management.rmi")
+            modules("jdk.jdi", "java.compiler", "jdk.accessibility", "jdk.zipfs", "java.management.rmi")
             targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
             packageName = "Processing"
 
@@ -109,6 +110,7 @@ dependencies {
     implementation(compose.ui)
     implementation(compose.components.resources)
     implementation(compose.components.uiToolingPreview)
+    implementation(compose.materialIconsExtended)
 
     implementation(compose.desktop.currentOs)
 
@@ -121,6 +123,9 @@ dependencies {
     testImplementation(libs.mockitoKotlin)
     testImplementation(libs.junitJupiter)
     testImplementation(libs.junitJupiterParams)
+
+    @OptIn(ExperimentalComposeLibrary::class)
+    testImplementation(compose.uiTest)
 }
 
 tasks.test {
diff --git a/app/src/main/resources/default.png b/app/src/main/resources/default.png
new file mode 100644
index 0000000000..df13f36105
Binary files /dev/null and b/app/src/main/resources/default.png differ
diff --git a/app/src/main/resources/welcome/intro/bubble.svg b/app/src/main/resources/welcome/intro/bubble.svg
new file mode 100644
index 0000000000..a3997b1e79
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/bubble.svg
@@ -0,0 +1,3 @@
+<svg width="35" height="17" viewBox="0 0 35 17" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path fill-rule="evenodd" clip-rule="evenodd" d="M34.9983 0H5.12802C5.70334 0.287249 6.08141 0.892539 6.00327 1.5786C5.49597 6.031 3.64997 10.219 0.524971 14.262C-0.226487 15.234 0.183131 16.6103 1.18205 17H3.01892C16.3118 15.6483 27.2737 9.0605 34.421 0.456001C34.5812 0.263008 34.7785 0.108197 34.9983 0Z" fill="#0F195A"/>
+</svg>
\ No newline at end of file
diff --git a/app/src/main/resources/welcome/intro/long.svg b/app/src/main/resources/welcome/intro/long.svg
new file mode 100644
index 0000000000..004418ce1f
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/long.svg
@@ -0,0 +1,7 @@
+<svg width="127" height="140" viewBox="0 0 127 140" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M-22 152C19.1706 118.211 52.4455 78.5963 97 25" stroke="#1F34AB" stroke-width="78"/>
+    <ellipse cx="66" cy="35.6" rx="13" ry="12.6" fill="white"/>
+    <ellipse cx="92" cy="58.3998" rx="13" ry="12.6" fill="white"/>
+    <ellipse cx="69.0953" cy="39.8002" rx="3.71429" ry="3.6" fill="#222222"/>
+    <ellipse cx="87.6667" cy="55.3998" rx="3.71429" ry="3.6" fill="#222222"/>
+</svg>
diff --git a/app/src/main/resources/welcome/intro/short.svg b/app/src/main/resources/welcome/intro/short.svg
new file mode 100644
index 0000000000..d08759c01c
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/short.svg
@@ -0,0 +1,17 @@
+<svg width="101" height="128" viewBox="0 0 101 128" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <g filter="url(#filter0_f_2007_98)">
+        <ellipse cx="57" cy="116.5" rx="18" ry="3.5" fill="#0F195A" fill-opacity="0.4"/>
+    </g>
+    <path d="M21 21L80 80" stroke="#82AFFF" stroke-width="58"/>
+    <circle cx="85.5681" cy="66.9495" r="10.5" transform="rotate(121.241 85.5681 66.9495)" fill="white"/>
+    <circle cx="58.4319" cy="75.0504" r="10.5" transform="rotate(121.241 58.4319 75.0504)" fill="white"/>
+    <circle cx="89.1563" cy="69.711" r="3" transform="rotate(121.241 89.1563 69.711)" fill="#222222"/>
+    <circle cx="62.875" cy="78.3304" r="3" transform="rotate(121.241 62.875 78.3304)" fill="#222222"/>
+    <defs>
+        <filter id="filter0_f_2007_98" x="31" y="105" width="52" height="23" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+            <feFlood flood-opacity="0" result="BackgroundImageFix"/>
+            <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
+            <feGaussianBlur stdDeviation="4" result="effect1_foregroundBlur_2007_98"/>
+        </filter>
+    </defs>
+</svg>
diff --git a/app/src/main/resources/welcome/intro/wavy.svg b/app/src/main/resources/welcome/intro/wavy.svg
new file mode 100644
index 0000000000..b244066fa1
--- /dev/null
+++ b/app/src/main/resources/welcome/intro/wavy.svg
@@ -0,0 +1,7 @@
+<svg width="69" height="167" viewBox="0 0 69 167" fill="none" xmlns="http://www.w3.org/2000/svg">
+    <path d="M31.9543 11.7981C31.9542 86.5 102.5 74.5001 119.823 174.031" stroke="#0468FF" stroke-width="63"/>
+    <circle cx="19.1708" cy="33.1238" r="10.5" transform="rotate(-75.5441 19.1708 33.1238)" fill="white"/>
+    <circle cx="42.8116" cy="17.5317" r="10.5" transform="rotate(-75.5441 42.8116 17.5317)" fill="white"/>
+    <circle cx="14.9381" cy="31.5162" r="3" transform="rotate(-75.5441 14.9381 31.5162)" fill="#222222"/>
+    <circle cx="37.6106" cy="15.6745" r="3" transform="rotate(-75.5441 37.6106 15.6745)" fill="#222222"/>
+</svg>
diff --git a/app/src/processing/app/Language.java b/app/src/processing/app/Language.java
index d55c8b710c..bcc4385a53 100644
--- a/app/src/processing/app/Language.java
+++ b/app/src/processing/app/Language.java
@@ -183,6 +183,12 @@ static public Language init() {
     return instance;
   }
 
+  static public void reload(){
+    if(instance == null) return;
+    synchronized (Language.class) {
+      instance = new Language();
+    }
+  }
 
   static private String get(String key) {
     LanguageBundle bundle = init().bundle;
diff --git a/app/src/processing/app/Preferences.kt b/app/src/processing/app/Preferences.kt
index c5645c9bbc..a31cef2bbe 100644
--- a/app/src/processing/app/Preferences.kt
+++ b/app/src/processing/app/Preferences.kt
@@ -2,9 +2,13 @@ package processing.app
 
 import androidx.compose.runtime.*
 import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.flow.debounce
+import kotlinx.coroutines.flow.dropWhile
 import kotlinx.coroutines.launch
 import java.io.File
 import java.io.InputStream
+import java.io.OutputStream
 import java.nio.file.*
 import java.util.Properties
 
@@ -12,28 +16,68 @@ import java.util.Properties
 const val PREFERENCES_FILE_NAME = "preferences.txt"
 const val DEFAULTS_FILE_NAME = "defaults.txt"
 
-fun PlatformStart(){
-    Platform.inst ?: Platform.init()
-}
+class ReactiveProperties: Properties() {
+    val _stateMap = mutableStateMapOf<String, String>()
+
+    override fun setProperty(key: String, value: String) {
+        super.setProperty(key, value)
+        _stateMap[key] = value
+    }
 
+    override fun getProperty(key: String): String? {
+        return _stateMap[key] ?: super.getProperty(key)
+    }
+
+    operator fun get(key: String): String? = getProperty(key)
+
+    operator fun set(key: String, value: String) {
+        setProperty(key, value)
+    }
+}
+val LocalPreferences = compositionLocalOf<ReactiveProperties> { error("No preferences provided") }
+@OptIn(FlowPreview::class)
 @Composable
-fun loadPreferences(): Properties{
-    PlatformStart()
+fun PreferencesProvider(content: @Composable () -> Unit){
+    remember {
+        Platform.init()
+    }
 
     val settingsFolder = Platform.getSettingsFolder()
     val preferencesFile = settingsFolder.resolve(PREFERENCES_FILE_NAME)
-
     if(!preferencesFile.exists()){
+        preferencesFile.mkdirs()
         preferencesFile.createNewFile()
     }
-    watchFile(preferencesFile)
 
-    return Properties().apply {
-        load(ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME) ?: InputStream.nullInputStream())
-        load(preferencesFile.inputStream())
+    val update = watchFile(preferencesFile)
+    val properties = remember(preferencesFile, update) { ReactiveProperties().apply {
+        load((ClassLoader.getSystemResourceAsStream(DEFAULTS_FILE_NAME)?: InputStream.nullInputStream()).reader(Charsets.UTF_8))
+        load(preferencesFile.inputStream().reader(Charsets.UTF_8))
+    }}
+
+    val initialState = remember(properties) { properties._stateMap.toMap() }
+
+    LaunchedEffect(properties) {
+        snapshotFlow { properties._stateMap.toMap() }
+            .dropWhile { it == initialState }
+            .debounce(100)
+            .collect {
+                preferencesFile.outputStream().use { output ->
+                    output.write(
+                        properties.entries
+                            .sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.key.toString() })
+                            .joinToString("\n") { (key, value) -> "$key=$value" }
+                            .toByteArray()
+                    )
+                }
+            }
+    }
+
+    CompositionLocalProvider(LocalPreferences provides properties){
+        content()
     }
-}
 
+}
 @Composable
 fun watchFile(file: File): Any? {
     val scope = rememberCoroutineScope()
@@ -62,12 +106,4 @@ fun watchFile(file: File): Any? {
         }
     }
     return event
-}
-val LocalPreferences = compositionLocalOf<Properties> { error("No preferences provided") }
-@Composable
-fun PreferencesProvider(content: @Composable () -> Unit){
-    val preferences = loadPreferences()
-    CompositionLocalProvider(LocalPreferences provides preferences){
-        content()
-    }
 }
\ No newline at end of file
diff --git a/app/src/processing/app/contrib/ui/ContributionManager.kt b/app/src/processing/app/contrib/ui/ContributionManager.kt
index 2ad472159b..4d21227a4d 100644
--- a/app/src/processing/app/contrib/ui/ContributionManager.kt
+++ b/app/src/processing/app/contrib/ui/ContributionManager.kt
@@ -22,8 +22,9 @@ import androidx.compose.ui.window.application
 import com.charleskorn.kaml.Yaml
 import com.charleskorn.kaml.YamlConfiguration
 import kotlinx.serialization.Serializable
+import processing.app.LocalPreferences
 import processing.app.Platform
-import processing.app.loadPreferences
+import processing.app.ReactiveProperties
 import java.net.URL
 import java.util.*
 import javax.swing.JFrame
@@ -106,7 +107,7 @@ fun contributionsManager(){
     var localContributions by remember { mutableStateOf(listOf<Contribution>()) }
     var error by remember { mutableStateOf<Exception?>(null) }
 
-    val preferences = loadPreferences()
+    val preferences = LocalPreferences.current
 
     LaunchedEffect(preferences){
         try {
@@ -284,9 +285,9 @@ fun contributionsManager(){
 }
 
 
-fun loadContributionProperties(preferences: Properties): List<Pair<Type, Properties>>{
+fun loadContributionProperties(preferences: ReactiveProperties): List<Pair<Type, Properties>>{
     val result = mutableListOf<Pair<Type, Properties>>()
-    val sketchBook = Path(preferences.getProperty("sketchbook.path.four", Platform.getDefaultSketchbookFolder().path))
+    val sketchBook = Path(preferences.getProperty("sketchbook.path.four") ?: Platform.getDefaultSketchbookFolder().path)
     sketchBook.forEachDirectoryEntry{ contributionsFolder ->
         if(!contributionsFolder.isDirectory()) return@forEachDirectoryEntry
         val typeName = contributionsFolder.fileName.toString()
diff --git a/app/src/processing/app/ui/Welcome.kt b/app/src/processing/app/ui/Welcome.kt
new file mode 100644
index 0000000000..492410b881
--- /dev/null
+++ b/app/src/processing/app/ui/Welcome.kt
@@ -0,0 +1,274 @@
+package processing.app.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.material.MaterialTheme.typography
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.scale
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.LayoutDirection
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.min
+import com.formdev.flatlaf.util.SystemInfo
+import processing.app.*
+import processing.app.ui.components.LanguageChip
+import processing.app.ui.components.examples.examples
+import processing.app.ui.theme.*
+import java.awt.Desktop
+import java.io.IOException
+import java.net.URI
+import java.nio.file.*
+import java.util.*
+import javax.swing.SwingUtilities
+
+
+class Welcome @Throws(IOException::class) constructor(base: Base) {
+    init {
+        SwingUtilities.invokeLater {
+            PDEWindow("menu.help.welcome", fullWindowContent = true) {
+                CompositionLocalProvider(LocalBase provides base) {
+                    welcome()
+                }
+            }
+        }
+    }
+
+    companion object {
+        val LocalBase = compositionLocalOf<Base?> { null }
+        @Composable
+        fun welcome() {
+            Column(
+                modifier = Modifier
+                    .background(
+                        Brush.linearGradient(
+                            colorStops = arrayOf(0f to Color.Transparent, 1f to Color("#C0D7FF".toColorInt())),
+                            start = Offset(815f, 0f),
+                            end = Offset(815f * 2, 450f)
+                        )
+                    )
+                    .padding(horizontal = 32.dp)
+                    .padding(bottom = 32.dp)
+                    .padding(top = if (SystemInfo.isMacFullWindowContentSupported) 22.dp else 0.dp)
+                    .height(IntrinsicSize.Max)
+                    .width(IntrinsicSize.Max)
+            ) {
+                Column(
+                    horizontalAlignment = Alignment.End,
+                    modifier = Modifier
+                        .align(Alignment.End)
+                ) {
+                    LanguageChip()
+                }
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(48.dp),
+                ) {
+                    Column {
+                        intro()
+                    }
+                    Box{
+                        Column {
+                            examples()
+                            actions()
+                        }
+                        val locale = LocalLocale.current
+                        Image(
+                            painter = painterResource("welcome/intro/wavy.svg"),
+                            contentDescription = locale["welcome.intro.long"],
+                            modifier = Modifier
+                                .height(200.dp)
+                                .offset (32.dp)
+                                .align(Alignment.BottomEnd)
+                                .scale(when(LocalLayoutDirection.current) {
+                                    LayoutDirection.Rtl -> -1f
+                                    else -> 1f
+                                }, 1f)
+                        )
+                    }
+                }
+            }
+        }
+
+        @Composable
+        fun intro(){
+            val locale = LocalLocale.current
+            Column(
+                verticalArrangement = Arrangement.SpaceBetween,
+                modifier = Modifier
+                    .fillMaxHeight()
+                    .width(IntrinsicSize.Max)
+            ) {
+                Column {
+                    Text(
+                        text = locale["welcome.intro.title"],
+                        style = typography.h4,
+                        modifier = Modifier
+                            .sizeIn(maxWidth = 305.dp)
+                    )
+                    Text(
+                        text = locale["welcome.intro.message"],
+                        style = typography.body1,
+                        modifier = Modifier
+                            .sizeIn(maxWidth = 305.dp)
+                    )
+                }
+                Column(
+                    modifier = Modifier
+                        .offset(y = 32.dp)
+                ){
+                    Text(
+                        text = locale["welcome.intro.suggestion"],
+                        style = typography.body1,
+                        color = colors.onPrimary,
+                        modifier = Modifier
+                            .padding(top = 16.dp)
+                            .clip(RoundedCornerShape(12.dp))
+                            .background(colors.primary)
+                            .padding(horizontal = 24.dp)
+                            .padding(top = 16.dp, bottom = 24.dp)
+                            .sizeIn(maxWidth = 200.dp)
+                    )
+                    Image(
+                        painter = painterResource("welcome/intro/bubble.svg"),
+                        contentDescription = locale["welcome.intro.long"],
+                        modifier = Modifier
+                            .align(Alignment.Start)
+                            .scale(when(LocalLayoutDirection.current) {
+                                LayoutDirection.Rtl -> -1f
+                                else -> 1f
+                            }, 1f)
+                            .padding(start = 64.dp)
+                    )
+                    Row(
+                        horizontalArrangement = Arrangement.SpaceBetween,
+                        modifier = Modifier.
+                            fillMaxWidth()
+                    ) {
+                        Image(
+                            painter = painterResource("welcome/intro/long.svg"),
+                            contentDescription = locale["welcome.intro.long"],
+                            modifier = Modifier
+                                .offset(x = -32.dp)
+                                .scale(when(LocalLayoutDirection.current) {
+                                    LayoutDirection.Rtl -> -1f
+                                    else -> 1f
+                                }, 1f)
+                        )
+                        Image(
+                            painter = painterResource("welcome/intro/short.svg"),
+                            contentDescription = locale["welcome.intro.short"],
+                            modifier = Modifier
+                                .align(Alignment.Bottom)
+                                .offset(x = 16.dp, y = -16.dp)
+                                .scale(when(LocalLayoutDirection.current) {
+                                    LayoutDirection.Rtl -> -1f
+                                    else -> 1f
+                                }, 1f)
+                        )
+                    }
+                }
+            }
+        }
+
+        @Composable
+        fun actions(){
+            val locale = LocalLocale.current
+            val base = LocalBase.current
+            PDEChip(onClick = {
+                base?.defaultMode?.showExamplesFrame()
+            }) {
+                Text(
+                    text = locale["welcome.action.examples"],
+                )
+                Image(
+                    imageVector = Icons.AutoMirrored.Default.ArrowForward,
+                    contentDescription = locale["welcome.action.tutorials"],
+                    colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+                    modifier = Modifier
+                        .padding(start = 8.dp)
+                        .size(typography.body1.fontSize.value.dp)
+                )
+            }
+            PDEChip(onClick = {
+                if (!Desktop.isDesktopSupported()) return@PDEChip
+                val desktop = Desktop.getDesktop()
+                if(!desktop.isSupported(Desktop.Action.BROWSE)) return@PDEChip
+                try {
+                    desktop.browse(URI(System.getProperty("processing.tutorials")))
+                } catch (e: Exception) {
+                    e.printStackTrace()
+                }
+            }) {
+                Text(
+                    text = locale["welcome.action.tutorials"],
+                )
+                Image(
+                    imageVector = Icons.AutoMirrored.Default.ArrowForward,
+                    contentDescription = locale["welcome.action.tutorials"],
+                    colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+                    modifier = Modifier
+                        .padding(start = 8.dp)
+                        .size(typography.body1.fontSize.value.dp)
+                )
+            }
+            Row(
+                horizontalArrangement = Arrangement.SpaceBetween,
+                verticalAlignment = Alignment.CenterVertically,
+                modifier = Modifier
+                    .fillMaxWidth()
+            ) {
+                Row(
+                    horizontalArrangement = Arrangement.spacedBy(8.dp),
+                    verticalAlignment = Alignment.CenterVertically,
+                    modifier = Modifier
+                        .offset(-32.dp)
+                ) {
+                    val preferences = LocalPreferences.current
+                    Checkbox(
+                        checked = preferences["welcome.four.show"]?.equals("true") ?: false,
+                        onCheckedChange = {
+                            preferences.setProperty("welcome.four.show", it.toString())
+                        },
+                        modifier = Modifier
+                            .size(24.dp)
+                    )
+                    Text(
+                        text = locale["welcome.action.startup"],
+                    )
+                }
+                val window = LocalWindow.current
+                PDEButton(onClick = {
+                    window.dispose()
+                }) {
+                    Text(
+                        text = locale["welcome.action.go"],
+                        modifier = Modifier
+                    )
+                }
+            }
+        }
+
+
+
+        @JvmStatic
+        fun main(args: Array<String>) {
+            pdeapplication("menu.help.welcome", fullWindowContent = true) {
+                welcome()
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/WelcomeToBeta.kt b/app/src/processing/app/ui/WelcomeToBeta.kt
index d7492fa6aa..6112820268 100644
--- a/app/src/processing/app/ui/WelcomeToBeta.kt
+++ b/app/src/processing/app/ui/WelcomeToBeta.kt
@@ -30,6 +30,7 @@ import androidx.compose.ui.window.WindowPosition
 import androidx.compose.ui.window.application
 import androidx.compose.ui.window.rememberWindowState
 import com.formdev.flatlaf.util.SystemInfo
+import processing.app.ui.theme.*
 import com.mikepenz.markdown.compose.Markdown
 import com.mikepenz.markdown.m2.markdownColor
 import com.mikepenz.markdown.m2.markdownTypography
@@ -54,44 +55,18 @@ import javax.swing.SwingUtilities
 class WelcomeToBeta {
     companion object{
         val windowSize = Dimension(400, 200)
-        val windowTitle = Locale()["beta.window.title"]
 
         @JvmStatic
         fun showWelcomeToBeta() {
-            val mac = SystemInfo.isMacFullWindowContentSupported
             SwingUtilities.invokeLater {
-                JFrame(windowTitle).apply {
-                    val close = { dispose() }
-                    rootPane.putClientProperty("apple.awt.transparentTitleBar", mac)
-                    rootPane.putClientProperty("apple.awt.fullWindowContent", mac)
-                    defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE
-                    contentPane.add(ComposePanel().apply {
-                        size = windowSize
-                        setContent {
-                            ProcessingTheme {
-                                Box(modifier = Modifier.padding(top = if (mac) 22.dp else 0.dp)) {
-                                    welcomeToBeta(close)
-                                }
-                            }
-                        }
-                    })
-                    pack()
-                    background = java.awt.Color.white
-                    setLocationRelativeTo(null)
-                    addKeyListener(object : KeyAdapter() {
-                        override fun keyPressed(e: KeyEvent) {
-                            if (e.keyCode == KeyEvent.VK_ESCAPE) close()
-                        }
-                    })
-                    isResizable = false
-                    isVisible = true
-                    requestFocus()
+                PDEWindow("beta.window.title") {
+                    welcomeToBeta()
                 }
             }
         }
 
         @Composable
-        fun welcomeToBeta(close: () -> Unit = {}) {
+        fun welcomeToBeta() {
             Row(
                 modifier = Modifier
                     .padding(20.dp, 10.dp)
@@ -131,9 +106,10 @@ class WelcomeToBeta {
                         modifier = Modifier.background(Color.Transparent).padding(bottom = 10.dp)
                     )
                     Row {
+                        val window = LocalWindow.current
                         Spacer(modifier = Modifier.weight(1f))
                         PDEButton(onClick = {
-                            close()
+                            window.dispose()
                         }) {
                             Text(
                                 text = locale["beta.button"],
@@ -144,66 +120,11 @@ class WelcomeToBeta {
                 }
             }
         }
-        @OptIn(ExperimentalComposeUiApi::class)
-        @Composable
-        fun PDEButton(onClick: () -> Unit, content: @Composable BoxScope.() -> Unit) {
-            val theme = LocalTheme.current
-
-            var hover by remember { mutableStateOf(false) }
-            var clicked by remember { mutableStateOf(false) }
-            val offset by animateFloatAsState(if (hover) -5f else 5f)
-            val color by animateColorAsState(if(clicked) colors.primaryVariant else colors.primary)
-
-            Box(modifier = Modifier.padding(end = 5.dp, top = 5.dp)) {
-                Box(
-                    modifier = Modifier
-                        .offset((-offset).dp, (offset).dp)
-                        .background(theme.getColor("toolbar.button.pressed.field"))
-                        .matchParentSize()
-                )
-                Box(
-                    modifier = Modifier
-                        .onPointerEvent(PointerEventType.Press) {
-                            clicked = true
-                        }
-                        .onPointerEvent(PointerEventType.Release) {
-                            clicked = false
-                            onClick()
-                        }
-                        .onPointerEvent(PointerEventType.Enter) {
-                            hover = true
-                        }
-                        .onPointerEvent(PointerEventType.Exit) {
-                            hover = false
-                        }
-                        .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR)))
-                        .background(color)
-                        .padding(10.dp)
-                        .sizeIn(minWidth = 100.dp),
-                    contentAlignment = Alignment.Center,
-                    content = content
-                )
-            }
-        }
-
 
         @JvmStatic
         fun main(args: Array<String>) {
-            application {
-                val windowState = rememberWindowState(
-                    size = DpSize.Unspecified,
-                    position = WindowPosition(Alignment.Center)
-                )
-
-                Window(onCloseRequest = ::exitApplication, state = windowState, title = windowTitle) {
-                    ProcessingTheme {
-                        Surface(color = colors.background) {
-                            welcomeToBeta {
-                                exitApplication()
-                            }
-                        }
-                    }
-                }
+            pdeapplication("beta.window.title") {
+                welcomeToBeta()
             }
         }
     }
diff --git a/app/src/processing/app/ui/components/LanuageSelector.kt b/app/src/processing/app/ui/components/LanuageSelector.kt
new file mode 100644
index 0000000000..5c42443fe4
--- /dev/null
+++ b/app/src/processing/app/ui/components/LanuageSelector.kt
@@ -0,0 +1,126 @@
+package processing.app.ui.components
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.DropdownMenu
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.LocalContentColor
+import androidx.compose.material.MaterialTheme.typography
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowForward
+import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.outlined.Language
+import androidx.compose.runtime.*
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.unit.dp
+import processing.app.Platform
+import processing.app.ui.theme.LocalLocale
+import processing.app.ui.theme.PDEChip
+import processing.app.watchFile
+import java.io.File
+import java.nio.file.FileSystem
+import java.nio.file.FileSystems
+import java.nio.file.Files
+import java.nio.file.Paths
+import java.util.*
+import kotlin.io.path.inputStream
+
+data class Language(
+    val name: String,
+    val code: String,
+    val locale: Locale,
+    val properties: Properties
+)
+
+var jarFs: FileSystem? = null
+
+@Composable
+fun LanguageChip(){
+    var expanded by remember { mutableStateOf(false) }
+
+    val settingsFolder = Platform.getSettingsFolder()
+    val languageFile = File(settingsFolder, "language.txt")
+    watchFile(languageFile)
+
+    val main = ClassLoader.getSystemResource("PDE.properties")?: return
+
+    val languages = remember {
+        val list = when(main.protocol){
+            "file" -> {
+                val path = Paths.get(main.toURI())
+                Files.list(path.parent)
+            }
+            "jar" -> {
+                val uri = main.toURI()
+                jarFs = jarFs ?: FileSystems.newFileSystem(uri, emptyMap<String, Any>()) ?: return@remember null
+                Files.list(jarFs!!.getPath("/"))
+            }
+            else -> null
+        } ?: return@remember null
+
+        list.toList()
+            .map { Pair(it, it.fileName.toString()) }
+            .filter { (_, fileName) -> fileName.startsWith("PDE_") && fileName.endsWith(".properties") }
+            .map { (path, _) ->
+                path.inputStream().reader(Charsets.UTF_8).use {
+                    val properties = Properties()
+                    properties.load(it)
+
+                    val code = path.fileName.toString().removeSuffix(".properties").replace("PDE_", "")
+                    val locale = Locale.forLanguageTag(code)
+                    val name = locale.getDisplayName(locale)
+
+                    return@map Language(
+                        name,
+                        code,
+                        locale,
+                        properties
+                    )
+                }
+            }
+            .sortedBy { it.name.lowercase() }
+    } ?: return
+
+    val current = languageFile.readText(Charsets.UTF_8).substring(0, 2)
+    val currentLanguage = remember(current) { languages.find { it.code.startsWith(current) } ?: languages.first()}
+
+    val locale = LocalLocale.current
+
+    PDEChip(onClick = { expanded = !expanded }, leadingIcon = {
+        Image(
+            imageVector = Icons.Outlined.Language,
+            contentDescription = "Language",
+            colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+            modifier = Modifier
+                .padding(start = 8.dp)
+                .size(typography.body1.fontSize.value.dp)
+        )
+    }) {
+        Text(currentLanguage.name)
+        Image(
+            imageVector = Icons.Default.ArrowDropDown,
+            contentDescription = locale["welcome.action.tutorials"],
+            colorFilter = ColorFilter.tint(color = LocalContentColor.current),
+            modifier = Modifier
+                .size(typography.body1.fontSize.value.dp)
+        )
+        DropdownMenu(
+            expanded = expanded,
+            onDismissRequest = {
+                expanded = false
+            },
+        ){
+            for (language in languages){
+                DropdownMenuItem(onClick = {
+                    locale.set(language.locale)
+                    expanded = false
+                }) {
+                    Text(language.name)
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/components/examples/Examples.kt b/app/src/processing/app/ui/components/examples/Examples.kt
new file mode 100644
index 0000000000..4c0a9045cb
--- /dev/null
+++ b/app/src/processing/app/ui/components/examples/Examples.kt
@@ -0,0 +1,194 @@
+package processing.app.ui.components.examples
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.material.Button
+import androidx.compose.material.ButtonDefaults
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.material.MaterialTheme.typography
+import androidx.compose.material.Text
+import androidx.compose.runtime.*
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.painter.BitmapPainter
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerIcon
+import androidx.compose.ui.input.pointer.onPointerEvent
+import androidx.compose.ui.input.pointer.pointerHoverIcon
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.jetbrains.compose.resources.ExperimentalResourceApi
+import org.jetbrains.compose.resources.decodeToImageBitmap
+import processing.app.LocalPreferences
+import processing.app.Messages
+import processing.app.Platform
+import processing.app.ui.Welcome.Companion.LocalBase
+import java.awt.Cursor
+import java.io.File
+import java.nio.file.*
+import java.nio.file.attribute.BasicFileAttributes
+import kotlin.io.path.exists
+import kotlin.io.path.inputStream
+import kotlin.io.path.isDirectory
+
+data class Example(
+    val folder: Path,
+    val library: Path,
+    val path: String = library.resolve("examples").relativize(folder).toString(),
+    val title: String = folder.fileName.toString(),
+    val image: Path = folder.resolve("$title.png")
+)
+
+@Composable
+fun loadExamples(): List<Example> {
+    val sketchbook = rememberSketchbookPath()
+    val resources = File(System.getProperty("compose.application.resources.dir") ?: "")
+    var examples by remember { mutableStateOf(emptyList<Example>()) }
+
+    val settingsFolder = Platform.getSettingsFolder()
+    val examplesCache = settingsFolder.resolve("examples.cache")
+    LaunchedEffect(sketchbook, resources){
+        if (!examplesCache.exists()) return@LaunchedEffect
+        withContext(Dispatchers.IO) {
+            examples = examplesCache.readText().lines().map {
+                val (library, folder) = it.split(",")
+                Example(
+                    folder = File(folder).toPath(),
+                    library = File(library).toPath()
+                )
+            }
+        }
+    }
+
+    LaunchedEffect(sketchbook, resources){
+        withContext(Dispatchers.IO) {
+            // TODO: Optimize
+            Messages.log("Start scanning for examples in $sketchbook and $resources")
+            //                  Folders that can contain contributions with examples
+            val scanned = listOf("libraries", "examples", "modes")
+                .flatMap { listOf(sketchbook.resolve(it), resources.resolve(it)) }
+                .filter { it.exists() && it.isDirectory() }
+                // Find contributions within those folders
+                .flatMap { Files.list(it.toPath()).toList() }
+                .filter { Files.isDirectory(it) }
+                // Find examples within those contributions
+                .flatMap { library ->
+                    val fs = FileSystems.getDefault()
+                    val matcher = fs.getPathMatcher("glob:**/*.pde")
+                    val exampleFolders = mutableListOf<Path>()
+                    val examples = library.resolve("examples")
+                    if (!Files.exists(examples) || !examples.isDirectory()) return@flatMap emptyList()
+
+                    Files.walkFileTree(library, object : SimpleFileVisitor<Path>() {
+                        override fun visitFile(file: Path, attrs: BasicFileAttributes): FileVisitResult {
+                            if (matcher.matches(file)) {
+                                exampleFolders.add(file.parent)
+                            }
+                            return FileVisitResult.CONTINUE
+                        }
+                    })
+                    return@flatMap exampleFolders.map { folder ->
+                        Example(
+                            folder,
+                            library,
+                        )
+                    }
+                }
+                .filter { it.image.exists() }
+            Messages.log("Done scanning for examples in $sketchbook and $resources")
+            if(scanned.isEmpty()) return@withContext
+            examples = scanned
+            examplesCache.writeText(examples.joinToString("\n") { "${it.library},${it.folder}" })
+        }
+    }
+
+    return examples
+
+}
+
+@Composable
+fun rememberSketchbookPath(): File {
+    val preferences = LocalPreferences.current
+    val sketchbookPath = remember(preferences["sketchbook.path.four"]) {
+        preferences["sketchbook.path.four"] ?: Platform.getDefaultSketchbookFolder().toString()
+    }
+    return File(sketchbookPath)
+}
+
+
+
+@Composable
+fun examples(){
+    val examples = loadExamples()
+
+
+    var randoms = examples.shuffled().take(4)
+    if(randoms.size < 4){
+        randoms = randoms + List(4 - randoms.size) { Example(
+            folder = Paths.get(""),
+            library = Paths.get(""),
+            title = "Example",
+            image = ClassLoader.getSystemResource("default.png")?.toURI()?.let { Paths.get(it) } ?: Paths.get(""),
+        ) }
+    }
+
+    Column(
+        verticalArrangement = Arrangement.spacedBy(16.dp),
+    ) {
+        randoms.chunked(2).forEach { row ->
+            Row (
+                horizontalArrangement = Arrangement.spacedBy(16.dp),
+            ){
+                row.forEach { example ->
+                    Example(example)
+                }
+            }
+        }
+    }
+}
+@OptIn(ExperimentalResourceApi::class)
+@Composable
+fun Example(example: Example){
+    val base = LocalBase.current
+    Button(
+        onClick = {
+            base?.handleOpenExample("${example.folder}/${example.title}.pde", base.defaultMode)
+        },
+        contentPadding = PaddingValues(0.dp),
+        elevation = null,
+        shape = RectangleShape,
+        colors = ButtonDefaults.buttonColors(
+            backgroundColor = Color.Transparent,
+            contentColor = colors.onBackground
+        ),
+    ) {
+        Column(
+            modifier = Modifier
+                .width(185.dp)
+        ) {
+            val imageBitmap: ImageBitmap = remember(example.image) {
+                example.image.inputStream().readAllBytes().decodeToImageBitmap()
+            }
+            Image(
+                painter = BitmapPainter(imageBitmap),
+                contentDescription = example.title,
+                modifier = Modifier
+                    .background(colors.primary)
+                    .aspectRatio(16f / 9f)
+            )
+            Text(
+                example.title,
+                style = typography.body1,
+                maxLines = 1
+            )
+        }
+    }
+}
diff --git a/app/src/processing/app/ui/theme/Button.kt b/app/src/processing/app/ui/theme/Button.kt
new file mode 100644
index 0000000000..bec6dd3bcd
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Button.kt
@@ -0,0 +1,52 @@
+package processing.app.ui.theme
+
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.Button
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.input.pointer.PointerEventType
+import androidx.compose.ui.input.pointer.PointerIcon
+import androidx.compose.ui.input.pointer.onPointerEvent
+import androidx.compose.ui.input.pointer.pointerHoverIcon
+import androidx.compose.ui.unit.dp
+import java.awt.Cursor
+
+@OptIn(ExperimentalComposeUiApi::class)
+@Composable
+fun PDEButton(onClick: () -> Unit, content: @Composable RowScope.() -> Unit) {
+    var hover by remember { mutableStateOf(false) }
+    val offset by animateFloatAsState(if (hover) -3f else 3f)
+
+    Box {
+        Box(
+            modifier = Modifier
+                .offset((-offset).dp, (offset).dp)
+                .matchParentSize()
+                .padding(vertical = 6.dp)
+                .background(colors.secondary)
+
+        )
+        Button(
+            onClick = onClick,
+            shape = RectangleShape,
+            contentPadding = PaddingValues(vertical = 8.dp, horizontal = 32.dp),
+            modifier = Modifier
+                .onPointerEvent(PointerEventType.Enter) {
+                    hover = true
+                }
+                .onPointerEvent(PointerEventType.Exit) {
+                    hover = false
+                }
+                .pointerHoverIcon(PointerIcon(Cursor(Cursor.HAND_CURSOR))),
+            content = content
+        )
+    }
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Chip.kt b/app/src/processing/app/ui/theme/Chip.kt
new file mode 100644
index 0000000000..baab6e8ef9
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Chip.kt
@@ -0,0 +1,31 @@
+package processing.app.ui.theme
+
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.material.Chip
+import androidx.compose.material.ChipDefaults
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun PDEChip(
+    onClick: () -> Unit = {},
+    leadingIcon: @Composable (() -> Unit)? = null,
+    content: @Composable RowScope.() -> Unit
+){
+    Chip(
+        onClick = onClick,
+        border = BorderStroke(1.dp, colors.secondary),
+        colors = ChipDefaults.chipColors(
+            backgroundColor = colors.background,
+            contentColor = colors.primaryVariant
+        ),
+        leadingIcon = leadingIcon,
+        modifier = Modifier,
+        content = content
+    )
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Colors.kt b/app/src/processing/app/ui/theme/Colors.kt
new file mode 100644
index 0000000000..efa97d37cc
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Colors.kt
@@ -0,0 +1,33 @@
+package processing.app.ui.theme
+
+import androidx.compose.material.Colors
+import androidx.compose.ui.graphics.Color
+
+val PDELightColors = Colors(
+    primary = Color("#0F195A".toColorInt()),
+    primaryVariant = Color("#1F34AB".toColorInt()),
+    secondary = Color("#82AFFF".toColorInt()),
+    secondaryVariant = Color("#0468FF".toColorInt()),
+    background = Color("#FFFFFF".toColorInt()),
+    surface = Color("#C0D7FF".toColorInt()),
+    error = Color("#0F195A".toColorInt()),
+    onPrimary = Color("#FFFFFF".toColorInt()),
+    onSecondary = Color("#FFFFFF".toColorInt()),
+    onBackground = Color("#0F195A".toColorInt()),
+    onSurface = Color("#FFFFFF".toColorInt()),
+    onError = Color("#0F195A".toColorInt()),
+    isLight = true,
+)
+
+fun String.toColorInt(): Int {
+    if (this[0] == '#') {
+        var color = substring(1).toLong(16)
+        if (length == 7) {
+            color = color or 0x00000000ff000000L
+        } else if (length != 9) {
+            throw IllegalArgumentException("Unknown color")
+        }
+        return color.toInt()
+    }
+    throw IllegalArgumentException("Unknown color")
+}
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Locale.kt b/app/src/processing/app/ui/theme/Locale.kt
index 254c0946c1..a4fd9eecfc 100644
--- a/app/src/processing/app/ui/theme/Locale.kt
+++ b/app/src/processing/app/ui/theme/Locale.kt
@@ -1,24 +1,27 @@
 package processing.app.ui.theme
 
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.compositionLocalOf
-import processing.app.LocalPreferences
-import processing.app.Messages
-import processing.app.Platform
-import processing.app.PlatformStart
-import processing.app.watchFile
+import androidx.compose.runtime.*
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.LayoutDirection
+import processing.app.*
 import java.io.File
 import java.io.InputStream
 import java.util.*
 
-class Locale(language: String = "") : Properties() {
+class Locale(language: String = "", val setLocale: (java.util.Locale) -> Unit) : Properties() {
+    var locale: java.util.Locale = java.util.Locale.getDefault()
+
     init {
-        val locale = java.util.Locale.getDefault()
-        load(ClassLoader.getSystemResourceAsStream("PDE.properties"))
-        load(ClassLoader.getSystemResourceAsStream("PDE_${locale.language}.properties") ?: InputStream.nullInputStream())
-        load(ClassLoader.getSystemResourceAsStream("PDE_${locale.toLanguageTag()}.properties") ?: InputStream.nullInputStream())
-        load(ClassLoader.getSystemResourceAsStream("PDE_${language}.properties") ?: InputStream.nullInputStream())
+        fun loadResourceUTF8(path: String) {
+            val stream = ClassLoader.getSystemResourceAsStream(path)
+            stream?.reader(charset = Charsets.UTF_8)?.use { reader ->
+                load(reader)
+            }
+        }
+        loadResourceUTF8("PDE.properties")
+        loadResourceUTF8("PDE_${locale.language}.properties")
+        loadResourceUTF8("PDE_${locale.toLanguageTag()}.properties")
+        loadResourceUTF8("PDE_${language}.properties")
     }
 
     @Deprecated("Use get instead", ReplaceWith("get(key)"))
@@ -28,18 +31,40 @@ class Locale(language: String = "") : Properties() {
         return value
     }
     operator fun get(key: String): String = getProperty(key, key)
+    fun set(locale: java.util.Locale) {
+        setLocale(locale)
+    }
 }
-val LocalLocale = compositionLocalOf { Locale() }
+val LocalLocale = compositionLocalOf<Locale> { error("No Locale Set") }
 @Composable
 fun LocaleProvider(content: @Composable () -> Unit) {
-    PlatformStart()
+    remember {
+        Platform.init()
+    }
 
     val settingsFolder = Platform.getSettingsFolder()
     val languageFile = File(settingsFolder, "language.txt")
     watchFile(languageFile)
+    var code by remember{ mutableStateOf(languageFile.readText().substring(0, 2)) }
+
+    fun setLocale(locale: java.util.Locale) {
+        java.util.Locale.setDefault(locale)
+        languageFile.writeText(locale.language)
+        code = locale.language
+        Language.reload()
+    }
+
+
+    val locale = Locale(code, ::setLocale)
+    Messages.log("Locale: $code")
+    val dir = when(locale["locale.direction"]) {
+        "rtl" -> LayoutDirection.Rtl
+        else -> LayoutDirection.Ltr
+    }
 
-    val locale = Locale(languageFile.readText().substring(0, 2))
-    CompositionLocalProvider(LocalLocale provides locale) {
-        content()
+    CompositionLocalProvider(LocalLayoutDirection provides dir) {
+        CompositionLocalProvider(LocalLocale provides locale) {
+            content()
+        }
     }
 }
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Theme.kt b/app/src/processing/app/ui/theme/Theme.kt
index 735d8e5b2a..aee7abe00f 100644
--- a/app/src/processing/app/ui/theme/Theme.kt
+++ b/app/src/processing/app/ui/theme/Theme.kt
@@ -1,7 +1,6 @@
 package processing.app.ui.theme
 
 import androidx.compose.foundation.isSystemInDarkTheme
-import androidx.compose.material.Colors
 import androidx.compose.material.MaterialTheme
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.CompositionLocalProvider
@@ -16,7 +15,7 @@ import java.util.Properties
 class Theme(themeFile: String? = "") : Properties() {
     init {
         load(ClassLoader.getSystemResourceAsStream("theme.txt"))
-        load(ClassLoader.getSystemResourceAsStream(themeFile) ?: InputStream.nullInputStream())
+        load(ClassLoader.getSystemResourceAsStream(themeFile ?: "") ?: InputStream.nullInputStream())
     }
     fun getColor(key: String): Color {
         return Color(getProperty(key).toColorInt())
@@ -33,43 +32,31 @@ fun ProcessingTheme(
     PreferencesProvider {
         val preferences = LocalPreferences.current
         val theme = Theme(preferences.getProperty("theme"))
-        val colors = Colors(
-            primary = theme.getColor("editor.gradient.top"),
-            primaryVariant = theme.getColor("toolbar.button.pressed.field"),
-            secondary = theme.getColor("editor.gradient.bottom"),
-            secondaryVariant = theme.getColor("editor.scrollbar.thumb.pressed.color"),
-            background = theme.getColor("editor.bgcolor"),
-            surface = theme.getColor("editor.bgcolor"),
-            error = theme.getColor("status.error.bgcolor"),
-            onPrimary = theme.getColor("toolbar.button.enabled.field"),
-            onSecondary = theme.getColor("toolbar.button.enabled.field"),
-            onBackground = theme.getColor("editor.fgcolor"),
-            onSurface = theme.getColor("editor.fgcolor"),
-            onError = theme.getColor("status.error.fgcolor"),
-            isLight = theme.getProperty("laf.mode").equals("light")
-        )
+//        val colors = Colors(
+//            primary = theme.getColor("editor.gradient.top"),
+//            primaryVariant = theme.getColor("toolbar.button.pressed.field"),
+//            secondary = theme.getColor("editor.gradient.bottom"),
+//            secondaryVariant = theme.getColor("editor.scrollbar.thumb.pressed.color"),
+//            background = theme.getColor("editor.bgcolor"),
+//            surface = theme.getColor("editor.bgcolor"),
+//            error = theme.getColor("status.error.bgcolor"),
+//            onPrimary = theme.getColor("toolbar.button.enabled.field"),
+//            onSecondary = theme.getColor("toolbar.button.enabled.field"),
+//            onBackground = theme.getColor("editor.fgcolor"),
+//            onSurface = theme.getColor("editor.fgcolor"),
+//            onError = theme.getColor("status.error.fgcolor"),
+//            isLight = theme.getProperty("laf.mode").equals("light")
+//        )
+
 
         CompositionLocalProvider(LocalTheme provides theme) {
             LocaleProvider {
                 MaterialTheme(
-                    colors = colors,
+                    colors = if(darkTheme) PDELightColors else PDELightColors,
                     typography = Typography,
                     content = content
                 )
             }
         }
     }
-}
-
-fun String.toColorInt(): Int {
-    if (this[0] == '#') {
-        var color = substring(1).toLong(16)
-        if (length == 7) {
-            color = color or 0x00000000ff000000L
-        } else if (length != 9) {
-            throw IllegalArgumentException("Unknown color")
-        }
-        return color.toInt()
-    }
-    throw IllegalArgumentException("Unknown color")
 }
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Typography.kt b/app/src/processing/app/ui/theme/Typography.kt
index 5d87c490e6..c21d554f7e 100644
--- a/app/src/processing/app/ui/theme/Typography.kt
+++ b/app/src/processing/app/ui/theme/Typography.kt
@@ -2,6 +2,8 @@ package processing.app.ui.theme
 
 import androidx.compose.material.MaterialTheme.typography
 import androidx.compose.material.Typography
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.text.ExperimentalTextApi
 import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontFamily
 import androidx.compose.ui.text.font.FontStyle
@@ -21,18 +23,39 @@ val processingFont = FontFamily(
         style = FontStyle.Normal
     )
 )
+val spaceGroteskFont = FontFamily(
+    Font(
+        resource = "SpaceGrotesk-Bold.ttf",
+        weight = FontWeight.Bold,
+    ),
+    Font(
+        resource = "SpaceGrotesk-Regular.ttf",
+        weight = FontWeight.Normal,
+    ),
+    Font(
+        resource = "SpaceGrotesk-Medium.ttf",
+        weight = FontWeight.Medium,
+    ),
+    Font(
+        resource = "SpaceGrotesk-SemiBold.ttf",
+        weight = FontWeight.SemiBold,
+    ),
+    Font(
+        resource = "SpaceGrotesk-Light.ttf",
+        weight = FontWeight.Light,
+    )
+)
 
 val Typography = Typography(
+    defaultFontFamily = spaceGroteskFont,
+    h4 = TextStyle(
+        fontWeight = FontWeight.Bold,
+        fontSize = 19.sp,
+        lineHeight = 24.sp
+    ),
     body1 = TextStyle(
-        fontFamily = processingFont,
         fontWeight = FontWeight.Normal,
-        fontSize = 13.sp,
-        lineHeight = 16.sp
+        fontSize = 15.sp,
+        lineHeight = 19.sp
     ),
-    subtitle1 = TextStyle(
-        fontFamily = processingFont,
-        fontWeight = FontWeight.Bold,
-        fontSize = 16.sp,
-        lineHeight = 20.sp
-    )
 )
\ No newline at end of file
diff --git a/app/src/processing/app/ui/theme/Window.kt b/app/src/processing/app/ui/theme/Window.kt
new file mode 100644
index 0000000000..0cb419332c
--- /dev/null
+++ b/app/src/processing/app/ui/theme/Window.kt
@@ -0,0 +1,106 @@
+package processing.app.ui.theme
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.MaterialTheme.colors
+import androidx.compose.material.Surface
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.compositionLocalOf
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.awt.ComposePanel
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.Window
+import androidx.compose.ui.window.WindowPosition
+import androidx.compose.ui.window.application
+import androidx.compose.ui.window.rememberWindowState
+import com.formdev.flatlaf.util.SystemInfo
+
+import java.awt.event.KeyAdapter
+import java.awt.event.KeyEvent
+import javax.swing.JFrame
+
+val LocalWindow = compositionLocalOf<JFrame> { error("No Window Set") }
+
+class PDEWindow(titleKey: String = "", fullWindowContent: Boolean = false, content: @Composable () -> Unit): JFrame(){
+    init{
+        val mac = SystemInfo.isMacFullWindowContentSupported
+
+        rootPane.apply{
+            putClientProperty("apple.awt.transparentTitleBar", mac)
+            putClientProperty("apple.awt.fullWindowContent", mac)
+        }
+
+        defaultCloseOperation = DISPOSE_ON_CLOSE
+        ComposePanel().apply {
+            setContent {
+                CompositionLocalProvider(LocalWindow provides this@PDEWindow) {
+                    ProcessingTheme {
+                        val locale = LocalLocale.current
+                        this@PDEWindow.title = locale[titleKey]
+                        LaunchedEffect(locale) {
+                            this@PDEWindow.pack()
+                            this@PDEWindow.setLocationRelativeTo(null)
+                        }
+
+                        Box(
+                            modifier = Modifier
+                                .padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp)
+                        ) {
+                            content()
+
+                        }
+                    }
+                }
+            }
+
+            this@PDEWindow.add(this)
+        }
+        pack()
+        background = java.awt.Color.white
+        setLocationRelativeTo(null)
+        addKeyListener(object : KeyAdapter() {
+            override fun keyPressed(e: KeyEvent) {
+                if (e.keyCode == KeyEvent.VK_ESCAPE) this@PDEWindow.dispose()
+            }
+        })
+        isResizable = false
+        isVisible = true
+        requestFocus()
+    }
+}
+
+fun pdeapplication(titleKey: String = "", fullWindowContent: Boolean = false,content: @Composable () -> Unit){
+    application {
+        val windowState = rememberWindowState(
+            size = DpSize.Unspecified,
+            position = WindowPosition(Alignment.Center)
+        )
+        ProcessingTheme {
+            val locale = LocalLocale.current
+            val mac = SystemInfo.isMacFullWindowContentSupported
+            Window(onCloseRequest = ::exitApplication, state = windowState, title = locale[titleKey]) {
+                window.rootPane.apply {
+                    putClientProperty("apple.awt.fullWindowContent", mac)
+                    putClientProperty("apple.awt.transparentTitleBar", mac)
+                }
+                LaunchedEffect(locale){
+                    window.pack()
+                    window.setLocationRelativeTo(null)
+                }
+                CompositionLocalProvider(LocalWindow provides window) {
+                    Surface(color = colors.background) {
+                        Box(
+                            modifier = Modifier
+                                .padding(top = if (mac && !fullWindowContent) 22.dp else 0.dp)
+                        ) {
+                            content()
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/app/test/processing/app/PreferencesKtTest.kt b/app/test/processing/app/PreferencesKtTest.kt
new file mode 100644
index 0000000000..f38796668e
--- /dev/null
+++ b/app/test/processing/app/PreferencesKtTest.kt
@@ -0,0 +1,34 @@
+package processing.app
+
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.testTag
+import androidx.compose.ui.test.*
+import kotlin.test.Test
+
+class PreferencesKtTest{
+    @OptIn(ExperimentalTestApi::class)
+    @Test
+    fun testKeyReactivity() = runComposeUiTest {
+        val newValue = (0..Int.MAX_VALUE).random().toString()
+        val testKey = "test.preferences.reactivity.$newValue"
+        setContent {
+            PreferencesProvider {
+                val preferences = LocalPreferences.current
+                Text(preferences[testKey] ?: "default", modifier = Modifier.testTag("text"))
+
+                Button(onClick = {
+                    preferences[testKey] = newValue
+                }, modifier = Modifier.testTag("button")) {
+                    Text("Change")
+                }
+            }
+        }
+
+        onNodeWithTag("text").assertTextEquals("default")
+        onNodeWithTag("button").performClick()
+        onNodeWithTag("text").assertTextEquals(newValue)
+    }
+
+}
\ No newline at end of file
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf b/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf
new file mode 100644
index 0000000000..0408641c61
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Bold.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt b/build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt
new file mode 100644
index 0000000000..6a314848b3
--- /dev/null
+++ b/build/shared/lib/fonts/SpaceGrotesk-LICENSE.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+https://openfontlicense.org
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded, 
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Light.ttf b/build/shared/lib/fonts/SpaceGrotesk-Light.ttf
new file mode 100644
index 0000000000..d41bcccd86
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Light.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Medium.ttf b/build/shared/lib/fonts/SpaceGrotesk-Medium.ttf
new file mode 100644
index 0000000000..7d44b663b9
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Medium.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf b/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf
new file mode 100644
index 0000000000..981bcf5b2c
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-Regular.ttf differ
diff --git a/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf b/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf
new file mode 100644
index 0000000000..e7e02e51e4
Binary files /dev/null and b/build/shared/lib/fonts/SpaceGrotesk-SemiBold.ttf differ
diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties
index 9d03f33c08..3ea6d7652b 100644
--- a/build/shared/lib/languages/PDE.properties
+++ b/build/shared/lib/languages/PDE.properties
@@ -621,6 +621,24 @@ update_check = Update
 update_check.updates_available.core = A new version of Processing is available,\nwould you like to visit the Processing download page?
 update_check.updates_available.contributions = There are updates available for some of the installed contributions,\nwould you like to open the the Contribution Manager now?
 
+
+# ---------------------------------------
+# Welcome
+welcome.intro.title = Welcome to Processing
+welcome.intro.message = A flexible software sketchbook and a language for learning how to code.
+welcome.intro.suggestion = Is it your first time using Processing? Try one of the examples on the right.
+welcome.action.examples = More examples
+welcome.action.tutorials = Tutorials
+welcome.action.startup = Show this window at startup
+welcome.action.go = Let's go!
+
+# ---------------------------------------
+# Beta
+beta.window.title = Welcome to Beta
+beta.title = Welcome to the Processing Beta
+beta.message = Thank you for trying out the new version of Processing. We're very grateful!\n\nPlease report any bugs on the forums.
+beta.button = Got it!
+
 # ---------------------------------------
 # Beta
 beta.window.title = Welcome to Beta