diff --git a/examples/glow/.gitignore b/examples/glow/.gitignore new file mode 100644 index 000000000..91861fd3c --- /dev/null +++ b/examples/glow/.gitignore @@ -0,0 +1,47 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release +.agent/ +.aic/ diff --git a/examples/glow/.metadata b/examples/glow/.metadata new file mode 100644 index 000000000..e89a49f88 --- /dev/null +++ b/examples/glow/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "f5b0b092640949e853a5053edbf724ad08210800" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + - platform: android + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + - platform: ios + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + - platform: linux + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + - platform: macos + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + - platform: web + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + - platform: windows + create_revision: f5b0b092640949e853a5053edbf724ad08210800 + base_revision: f5b0b092640949e853a5053edbf724ad08210800 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/examples/glow/README.md b/examples/glow/README.md new file mode 100644 index 000000000..d18c7f32a --- /dev/null +++ b/examples/glow/README.md @@ -0,0 +1,41 @@ +# Glow + +A creative AI companion that generates personalized wallpapers and immersive UI experiences using Gemini and GenUI. + +## Overview + +Glow is a demonstration of how Generative AI can be integrated into Flutter applications to create dynamic, personalized user experiences. By answering a few questions about your mood and style, Glow uses Gemini to generate unique visual assets and adapts the UI to match. + +## Features + +- **Intelligent Onboarding**: A personality-driven quiz that gathers user preferences. +- **AI Generation**: Integrated with Gemini (Google Generative AI) for real-time image and style generation. +- **Immersive Aesthetics**: High-performance fragment shaders create ambient, glowing background effects. +- **Modern Design**: Built with Material 3, custom typography, and a premium "glassmorphism" aesthetic. +- **GenUI Driven**: Showcases the power of the GenUI framework for building intelligent, adaptive interfaces. + +## Getting Started + +### Prerequisites + +- Flutter SDK (^3.11.0-144.0.dev or later recommended) +- A Google Gemini API Key + +### Configuration + +1. Obtain an API key from [Google AI Studio](https://aistudio.google.com/). +2. The first time you run the app, you will be prompted to enter your API key in the settings. + +### Running the App + +```bash +flutter run +``` + +## Project Structure + +- `lib/screens/`: Main application screens (Welcome, Quiz, Generation, Editor). +- `lib/widgets/`: Reusable UI components including branded elements and shader-based backgrounds. +- `lib/services/`: Backend logic for Gemini integration. +- `lib/theme.dart`: Centralized design system tokens (colors, typography, shadows). +- `shaders/`: Custom GLSL fragment shaders for advanced visual effects. diff --git a/examples/glow/analysis_options.yaml b/examples/glow/analysis_options.yaml new file mode 100644 index 000000000..7ce00ecc7 --- /dev/null +++ b/examples/glow/analysis_options.yaml @@ -0,0 +1,15 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +include: package:flutter_lints/flutter.yaml diff --git a/examples/glow/android/.gitignore b/examples/glow/android/.gitignore new file mode 100644 index 000000000..be3943c96 --- /dev/null +++ b/examples/glow/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/examples/glow/android/app/build.gradle.kts b/examples/glow/android/app/build.gradle.kts new file mode 100644 index 000000000..55571a201 --- /dev/null +++ b/examples/glow/android/app/build.gradle.kts @@ -0,0 +1,60 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.glow" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.glow" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/examples/glow/android/app/src/debug/AndroidManifest.xml b/examples/glow/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000..4b4563013 --- /dev/null +++ b/examples/glow/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/examples/glow/android/app/src/main/AndroidManifest.xml b/examples/glow/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..b59b640e9 --- /dev/null +++ b/examples/glow/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/glow/android/app/src/main/kotlin/com/example/glow/MainActivity.kt b/examples/glow/android/app/src/main/kotlin/com/example/glow/MainActivity.kt new file mode 100644 index 000000000..7565e2a9d --- /dev/null +++ b/examples/glow/android/app/src/main/kotlin/com/example/glow/MainActivity.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +package com.example.glow + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/examples/glow/android/app/src/main/res/drawable-v21/launch_background.xml b/examples/glow/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000..19b8858a2 --- /dev/null +++ b/examples/glow/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/examples/glow/android/app/src/main/res/drawable/launch_background.xml b/examples/glow/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000..17eb42b48 --- /dev/null +++ b/examples/glow/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/examples/glow/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/examples/glow/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..db77bb4b7 Binary files /dev/null and b/examples/glow/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/examples/glow/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/examples/glow/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..17987b79b Binary files /dev/null and b/examples/glow/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/examples/glow/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/examples/glow/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..09d439148 Binary files /dev/null and b/examples/glow/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/examples/glow/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/examples/glow/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d5f1c8d34 Binary files /dev/null and b/examples/glow/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/examples/glow/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/examples/glow/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/examples/glow/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/examples/glow/android/app/src/main/res/values-night/styles.xml b/examples/glow/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000..78444de94 --- /dev/null +++ b/examples/glow/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/examples/glow/android/app/src/main/res/values/styles.xml b/examples/glow/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000..f45a4ff83 --- /dev/null +++ b/examples/glow/android/app/src/main/res/values/styles.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/examples/glow/android/app/src/profile/AndroidManifest.xml b/examples/glow/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000..4b4563013 --- /dev/null +++ b/examples/glow/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/examples/glow/android/build.gradle.kts b/examples/glow/android/build.gradle.kts new file mode 100644 index 000000000..a5d2a7860 --- /dev/null +++ b/examples/glow/android/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/examples/glow/android/gradle.properties b/examples/glow/android/gradle.properties new file mode 100644 index 000000000..fbee1d8cd --- /dev/null +++ b/examples/glow/android/gradle.properties @@ -0,0 +1,2 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true diff --git a/examples/glow/android/gradle/wrapper/gradle-wrapper.properties b/examples/glow/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e4ef43fb9 --- /dev/null +++ b/examples/glow/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip diff --git a/examples/glow/android/settings.gradle.kts b/examples/glow/android/settings.gradle.kts new file mode 100644 index 000000000..eec89e552 --- /dev/null +++ b/examples/glow/android/settings.gradle.kts @@ -0,0 +1,42 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.11.1" apply false + id("org.jetbrains.kotlin.android") version "2.2.20" apply false +} + +include(":app") diff --git a/examples/glow/ios/.gitignore b/examples/glow/ios/.gitignore new file mode 100644 index 000000000..7a7f9873a --- /dev/null +++ b/examples/glow/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/examples/glow/ios/Flutter/AppFrameworkInfo.plist b/examples/glow/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000..1dc6cf765 --- /dev/null +++ b/examples/glow/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/examples/glow/ios/Flutter/Debug.xcconfig b/examples/glow/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000..ec97fc6f3 --- /dev/null +++ b/examples/glow/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/examples/glow/ios/Flutter/Release.xcconfig b/examples/glow/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000..c4855bfe2 --- /dev/null +++ b/examples/glow/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/examples/glow/ios/Podfile b/examples/glow/ios/Podfile new file mode 100644 index 000000000..620e46eba --- /dev/null +++ b/examples/glow/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/examples/glow/ios/Runner.xcodeproj/project.pbxproj b/examples/glow/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..7145b3e89 --- /dev/null +++ b/examples/glow/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,616 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/examples/glow/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/glow/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/glow/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..e3773d42e --- /dev/null +++ b/examples/glow/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/glow/ios/Runner.xcworkspace/contents.xcworkspacedata b/examples/glow/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..1d526a16e --- /dev/null +++ b/examples/glow/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/examples/glow/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/glow/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/glow/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/glow/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/examples/glow/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000..f9b0d7c5e --- /dev/null +++ b/examples/glow/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/examples/glow/ios/Runner/AppDelegate.swift b/examples/glow/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000..62fa1e597 --- /dev/null +++ b/examples/glow/ios/Runner/AppDelegate.swift @@ -0,0 +1,27 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..d36b1fab2 --- /dev/null +++ b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000..dc9ada472 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 000000000..7353c41ec Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 000000000..6ed2d933e Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 000000000..4cd7b0099 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 000000000..fe730945a Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 000000000..321773cd8 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 000000000..797d452e4 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 000000000..502f463a9 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 000000000..0ec303439 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 000000000..e9f5fea27 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 000000000..84ac32ae7 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 000000000..8953cba09 Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 000000000..0467bf12a Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 000000000..0bedcf2fd --- /dev/null +++ b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 000000000..9da19eaca Binary files /dev/null and b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 000000000..89c2725b7 --- /dev/null +++ b/examples/glow/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/examples/glow/ios/Runner/Base.lproj/LaunchScreen.storyboard b/examples/glow/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 000000000..f2e259c7c --- /dev/null +++ b/examples/glow/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/glow/ios/Runner/Base.lproj/Main.storyboard b/examples/glow/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 000000000..f3c28516f --- /dev/null +++ b/examples/glow/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/glow/ios/Runner/Info.plist b/examples/glow/ios/Runner/Info.plist new file mode 100644 index 000000000..dc04442b0 --- /dev/null +++ b/examples/glow/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Glow + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + glow + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/examples/glow/ios/Runner/Runner-Bridging-Header.h b/examples/glow/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000..d272c4e25 --- /dev/null +++ b/examples/glow/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1,17 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +#import "GeneratedPluginRegistrant.h" diff --git a/examples/glow/ios/RunnerTests/RunnerTests.swift b/examples/glow/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..8e82568cc --- /dev/null +++ b/examples/glow/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,26 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/examples/glow/l10n.yaml b/examples/glow/l10n.yaml new file mode 100644 index 000000000..91ee8e8ab --- /dev/null +++ b/examples/glow/l10n.yaml @@ -0,0 +1,17 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +arb-dir: lib/l10n +template-arb-file: app_en.arb +output-localization-file: app_localizations.dart diff --git a/examples/glow/lib/constants.dart b/examples/glow/lib/constants.dart new file mode 100644 index 000000000..44d67a9ee --- /dev/null +++ b/examples/glow/lib/constants.dart @@ -0,0 +1,18 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +class GlowConstants { + static const String defaultModel = 'models/gemini-3-flash-preview'; + static const String defaultImageModel = 'models/gemini-3-pro-image-preview'; +} diff --git a/examples/glow/lib/genui/editor_catalog.dart b/examples/glow/lib/genui/editor_catalog.dart new file mode 100644 index 000000000..01b58bfb6 --- /dev/null +++ b/examples/glow/lib/genui/editor_catalog.dart @@ -0,0 +1,179 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; +import 'package:glow/theme.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:glow/widgets/inputs/glow_inputs.dart'; // Reusing Glow inputs + +/// Catalog definition for dynamic Editor Controls +final editorCatalogItems = [editorControlItem]; +final editorCatalog = Catalog(editorCatalogItems); + +enum ControlType { slider, toggle, dropdown } + +// Schema for an Editor Control +final editorControlSchema = S.object( + properties: { + 'id': S.string(), + 'label': S.string(), + 'type': S.string( + enumValues: ControlType.values.map((e) => e.name).toList(), + ), + 'min': S.number(), + 'max': S.number(), + 'defaultValue': S.any(), + 'options': S.list( + // For dropdown + items: S.string(), + ), + }, + required: ['id', 'label', 'type'], +); + +final editorControlItem = CatalogItem( + name: 'EditorControl', + dataSchema: editorControlSchema, + widgetBuilder: (context) { + final data = context.data as Map; + final id = data['id'] as String; + final label = data['label'] as String; + final typeStr = data['type'] as String; + final type = ControlType.values.firstWhere( + (e) => e.name == typeStr, + orElse: () => ControlType.slider, + ); + + // We need to access the EditorViewModel to read/write values. + // We will use the QuizAnswerHandler pattern (renamed effectively to ControlHandler) + // or just look up the handler we injected in the screen. + return _DynamicEditorControl( + id: id, + label: label, + type: type, + min: (data['min'] as num?)?.toDouble() ?? 0.0, + max: (data['max'] as num?)?.toDouble() ?? 100.0, + defaultValue: data['defaultValue'], + options: (data['options'] as List?)?.cast(), + ); + }, +); + +class _DynamicEditorControl extends StatelessWidget { + final String id; + final String label; + final ControlType type; + final double min; + final double max; + final dynamic defaultValue; + final List? options; + + const _DynamicEditorControl({ + required this.id, + required this.label, + required this.type, + this.min = 0, + this.max = 100, + this.defaultValue, + this.options, + }); + + @override + Widget build(BuildContext context) { + // We expect an ancestor that implements a handler interface + // reusing the QuizAnswerHandler for simplicity as it matches the signature (id, value) + // We can rename/alias it if needed, but for now we essentially need "Get" and "Set". + // Let's assume the scoped handler uses the same interface class but semantically it's for editor. + final handler = _EditorControlHandler.of(context); + + final currentValue = handler?.getValue(id) ?? defaultValue; + + switch (type) { + case ControlType.slider: + final double val = (currentValue is num) + ? currentValue.toDouble() + : min; + return GlowSlider( + label: label, + value: val, + min: min, + max: max, + onChanged: (v) => handler?.setValue(id, v), + ); + case ControlType.toggle: + final bool val = (currentValue is bool) ? currentValue : false; + return GlowToggle( + title: label, + value: val, + onChanged: (v) => handler?.setValue(id, v), + ); + case ControlType.dropdown: + final String? val = (currentValue is String) + ? currentValue + : options?.firstOrNull; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: GlowTheme.textStyles.bodyMedium), + const SizedBox(height: 8), + GlowDropdown( + value: val, + items: (options ?? []) + .map((e) => DropdownMenuItem(value: e, child: Text(e))) + .toList(), + onChanged: (v) => handler?.setValue(id, v), + ), + ], + ); + } + } +} + +// Specialized Handler Interface for Editor +abstract class EditorControlHandler { + void setValue(String id, dynamic value); + dynamic getValue(String id); +} + +class _EditorControlHandler extends InheritedWidget { + final EditorControlHandler handler; + + const _EditorControlHandler({required this.handler, required super.child}); + + static EditorControlHandler? of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType<_EditorControlHandler>() + ?.handler; + } + + @override + bool updateShouldNotify(_EditorControlHandler oldWidget) => false; +} + +// Public wrapper for the screen to use +class EditorControlScope extends StatelessWidget { + final EditorControlHandler handler; + final Widget child; + const EditorControlScope({ + super.key, + required this.handler, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return _EditorControlHandler(handler: handler, child: child); + } +} diff --git a/examples/glow/lib/genui/quiz_catalog.dart b/examples/glow/lib/genui/quiz_catalog.dart new file mode 100644 index 000000000..6c0e7d11f --- /dev/null +++ b/examples/glow/lib/genui/quiz_catalog.dart @@ -0,0 +1,283 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; +import 'package:glow/theme.dart'; +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:glow/models/quiz_question.dart'; +import 'package:glow/widgets/quiz/quiz_choice_inputs.dart'; +import 'package:glow/widgets/inputs/glow_inputs.dart'; + +final quizCatalogItems = [quizQuestionItem]; +final quizCatalog = Catalog(quizCatalogItems); + +final quizQuestionSchema = S.object( + properties: { + 'id': S.string(), + 'text': S.string(), + 'type': S.string( + enumValues: QuestionType.values.map((e) => e.name).toList(), + ), + 'options': S.list( + items: S.object( + properties: { + 'id': S.string(), + 'label': S.string(), + 'imageSeed': S.string(), + }, + required: ['id', 'label'], + ), + ), + 'min': S.number(), + 'max': S.number(), + 'divisions': S.integer(), + }, + required: ['id', 'text', 'type'], +); + +final quizQuestionItem = CatalogItem( + name: 'QuizQuestion', + dataSchema: quizQuestionSchema, + widgetBuilder: (context) { + final data = context.data as Map; + final id = data['id'] as String; + final text = data['text'] as String; + final typeStr = data['type'] as String; + final type = QuestionType.values.firstWhere( + (e) => e.name == typeStr, + orElse: () => QuestionType.singleChoiceText, + ); + + final optionsList = (data['options'] as List?)?.map((e) { + final map = e as Map; + return QuizOption( + id: map['id'], + label: map['label'], + imageSeed: map['imageSeed'], + ); + }).toList(); + + // We need a way to access the view model or data model to store answers. + // In GenUI, we usually bind to data. + // But here we are building a specific quiz flow where we want to capture the answer. + // The simple way is to use a callback or find the ancestor. + // Since we can't easily pass the VM here without context, we'll use `context.dataContext` if we were doing pure GenUI data binding. + // BUT, our plan handles answers in `QuizViewModel`. + // We can use a `GenUiSurface`'s interaction event to send data back. + // Or we can simple emit a custom event. + // For now, let's assume we can find the `QuizViewModel` via `Provider` or `context`? + // No, `QuizViewModel` is passed to `QuizScreen`. + // Let's rely on standard Flutter widget tree access if we put `QuizViewModel` in a `Provider` or `ListenableBuilder`. + // Or better: `GenUiSurface` allows `actions`. + + // Simplification: We will just build the UI. User interaction updates the LOCAL state of the widget (if needed) or trigger events. + // The `QuizScreen` wraps this surface. + // Wait, the `widgetBuilder` runs inside `GenUiSurface`. + // We need to capture the answer. + + // Let's use a `GlobalKey` or similar? No. + // Let's use `top-level` event handler? + // Actually, `GenUi` encourages data binding. `context.data` is arguments. + // We need to OUTPUT data. + // We can use `context.dataContext.update(...)` if we bind. + + // Alternative: The `QuizQuestion` widget we build here should be capable of communicating answer back. + // Let's create a `DynamicQuizQuestion` widget that finds the `QuizViewModel` via `InheritedWidget` or similar. + // Or just pass a callback? We can't pass callbacks from `CatalogItem` easily. + + return _DynamicQuizQuestion( + id: id, + text: text, + type: type, + options: optionsList, + min: data['min'] as double?, + max: data['max'] as double?, + divisions: data['divisions'] as int?, + ); + }, +); + +class _DynamicQuizQuestion extends StatelessWidget { + final String id; + final String text; + final QuestionType type; + final List? options; + final double? min; + final double? max; + final int? divisions; + + const _DynamicQuizQuestion({ + required this.id, + required this.text, + required this.type, + this.options, + this.min, + this.max, + this.divisions, + }); + + @override + Widget build(BuildContext context) { + // We need to perform "answer handling". + // We can bubble up an event or use a locally scoped provider. + // Let's use a specialized InheritedWidget in QuizScreen to expose the callback. + final handler = QuizAnswerHandler.of(context); + + // Helper for checking selection + bool isSelected(String optId) => handler?.isSelected(id, optId) ?? false; + // Helper for handling selection + void onSelect(dynamic val) => handler?.apiSetAnswer(id, val); + + Widget inputBody; + switch (type) { + case QuestionType.singleChoiceImage: + case QuestionType.multipleChoiceImage: + inputBody = GlowImageGridSelector( + options: options ?? [], + isSelected: isSelected, + onToggle: (optId) => _handleMultiSelect(handler, id, optId, type), + ); + break; + case QuestionType.singleChoiceText: + case QuestionType.multipleChoiceText: + inputBody = GlowTextChoiceSelector( + options: options ?? [], + isSelected: isSelected, + onToggle: (optId) => _handleMultiSelect(handler, id, optId, type), + ); + break; + case QuestionType.slider: + final currentVal = + (handler?.getAnswer(id) as num?)?.toDouble() ?? min ?? 0; + inputBody = GlowSlider( + value: currentVal, + min: min ?? 0, + max: max ?? 100, + divisions: divisions, + onChanged: (val) => onSelect(val), + label: "${currentVal.round()}%", + ); + break; + case QuestionType.toggle: + final val = (handler?.getAnswer(id) as bool?) ?? false; + inputBody = GlowToggle( + value: val, + title: "Select", + onChanged: (v) => onSelect(v), + ); + break; + case QuestionType.dropdown: + final val = handler?.getAnswer(id) as String?; + inputBody = GlowDropdown( + value: val, + items: (options ?? []) + .map((e) => DropdownMenuItem(value: e.id, child: Text(e.label))) + .toList(), + onChanged: (v) => onSelect(v), + ); + break; + case QuestionType.textInputShort: + case QuestionType.textInputLong: + // Text fields need state management or controller. + // For simplicity, we just use onChanged. + // In a real app, restore text from handler. + inputBody = GlowTextField( + isLong: type == QuestionType.textInputLong, + onChanged: (v) => onSelect(v), + // controller can be set if we tracked it + ); + break; + } + + return Column( + children: [ + const SizedBox(height: 10), + Text( + text, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: GlowTheme.colors.white, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + inputBody, + ], + ); + } + + void _handleMultiSelect( + QuizAnswerHandler? handler, + String qId, + String optId, + QuestionType type, + ) { + if (handler == null) return; + final isMulti = + type == QuestionType.multipleChoiceImage || + type == QuestionType.multipleChoiceText; + + if (isMulti) { + final current = List.from(handler.getAnswer(qId) as List? ?? []); + if (current.contains(optId)) { + current.remove(optId); + } else { + current.add(optId); + } + handler.apiSetAnswer(qId, current); + } else { + handler.apiSetAnswer(qId, optId); + } + } +} + +// InheritedWidget to pass down the handler +abstract class QuizAnswerHandler { + void apiSetAnswer(String qId, dynamic value); + dynamic getAnswer(String qId); + bool isSelected(String qId, String optId); + + static QuizAnswerHandler? of(BuildContext context) { + return context + .findAncestorWidgetOfExactType<_QuizAnswerHandlerScope>() + ?.handler; + } +} + +class QuizAnswerHandlerScope extends StatelessWidget { + final QuizAnswerHandler handler; + final Widget child; + + const QuizAnswerHandlerScope({ + super.key, + required this.handler, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return _QuizAnswerHandlerScope(handler: handler, child: child); + } +} + +class _QuizAnswerHandlerScope extends InheritedWidget { + final QuizAnswerHandler handler; + + const _QuizAnswerHandlerScope({required this.handler, required super.child}); + + @override + bool updateShouldNotify(_QuizAnswerHandlerScope oldWidget) => false; +} diff --git a/examples/glow/lib/l10n/app_en.arb b/examples/glow/lib/l10n/app_en.arb new file mode 100644 index 000000000..c55fedc77 --- /dev/null +++ b/examples/glow/lib/l10n/app_en.arb @@ -0,0 +1,142 @@ +{ + "appTitle": "Glow", + "@appTitle": { + "description": "The title of the application" + }, + "welcomeSubtitle": "Personalized Wallpaper & GenUI Showcase", + "@welcomeSubtitle": { + "description": "Subtitle on the welcome screen" + }, + "welcomeDescription": "Welcome to Glow! Take a fun, interactive personality quiz driven by Gemini AI to discover your unique visual style. We'll generate a one-of-a-kind phone wallpaper, and then you can refine it through an iterative, agent-assisted creative process.", + "@welcomeDescription": { + "description": "Description on the welcome screen" + }, + "personalityQuiz": "Personality Quiz", + "@personalityQuiz": { + "description": "Label for personality quiz feature" + }, + "aiGeneration": "AI Generation", + "@aiGeneration": { + "description": "Label for AI generation feature" + }, + "iterativeRefinement": "Iterative Refinement", + "@iterativeRefinement": { + "description": "Label for iterative refinement feature" + }, + "getStarted": "Get Started", + "@getStarted": { + "description": "Button text to get started" + }, + "changeApiKey": "Change API Key", + "@changeApiKey": { + "description": "Button text to change API key" + }, + "poweredBy": "Powered by GenUI SDK for Flutter & Gemini Models", + "@poweredBy": { + "description": "Footer text showing technology stack" + }, + "apiKeyDialogTitle": "Enter Gemini API Key", + "@apiKeyDialogTitle": { + "description": "Title of the API key dialog" + }, + "apiKeyDialogDescription": "To use Glow, you need a free Gemini API Key.", + "@apiKeyDialogDescription": { + "description": "Description in the API key dialog" + }, + "getApiKeyHere": "Get an API Key here", + "@getApiKeyHere": { + "description": "Link text to get an API key" + }, + "apiKeyLabel": "API Key", + "@apiKeyLabel": { + "description": "Label for API key input field" + }, + "cancel": "Cancel", + "@cancel": { + "description": "Cancel button text" + }, + "save": "Save", + "@save": { + "description": "Save button text" + }, + "quizTitle": "Glow Personality Quiz", + "@quizTitle": { + "description": "Title of the quiz screen" + }, + "questionCount": "Question {currentIndex} of {totalQuestions}", + "@questionCount": { + "description": "Counter for the current question", + "placeholders": { + "currentIndex": { + "type": "int" + }, + "totalQuestions": { + "type": "int" + } + } + }, + "next": "Next", + "@next": { + "description": "Next button text" + }, + "skipToGeneration": "Skip to generation", + "@skipToGeneration": { + "description": "Skip to generation button text" + }, + "generatingGlow": "Generating your personal glow...", + "@generatingGlow": { + "description": "Text shown during wallpaper generation" + }, + "generationDescription": "Using Gemini to create your wallpaper.\nThis may take a moment.", + "@generationDescription": { + "description": "Description shown during wallpaper generation" + }, + "adjustWallpaper": "Adjust Wallpaper", + "@adjustWallpaper": { + "description": "Header for adjusting wallpaper settings" + }, + "wallpaperStyle": "Wallpaper Style", + "@wallpaperStyle": { + "description": "Header for wallpaper style selection" + }, + "regenerate": "Regenerate", + "@regenerate": { + "description": "Label for the regenerate button" + }, + "styleCosmic": "Cosmic", + "@styleCosmic": { + "description": "Label for the Cosmic style" + }, + "styleAbstract": "Abstract", + "@styleAbstract": { + "description": "Label for the Abstract style" + }, + "styleNature": "Nature", + "@styleNature": { + "description": "Label for the Nature style" + }, + "styleCrystal": "Crystal", + "@styleCrystal": { + "description": "Label for the Crystal style" + }, + "atmosphereWarmCalm": "Warm & Calm", + "@atmosphereWarmCalm": { + "description": "Label for the warm and calm atmosphere setting" + }, + "atmosphereCoolEnergetic": "Cool & Energetic", + "@atmosphereCoolEnergetic": { + "description": "Label for the cool and energetic atmosphere setting" + }, + "atmosphereLabel": "Atmosphere", + "@atmosphereLabel": { + "description": "Label for the atmosphere slider" + }, + "keyElementLabel": "Key Element", + "@keyElementLabel": { + "description": "Label for the key element selection" + }, + "myGlowWallpaper": "My Glow Wallpaper", + "@myGlowWallpaper": { + "description": "Default title for the wallpaper editor" + } +} diff --git a/examples/glow/lib/l10n/app_localizations.dart b/examples/glow/lib/l10n/app_localizations.dart new file mode 100644 index 000000000..f0d58b760 --- /dev/null +++ b/examples/glow/lib/l10n/app_localizations.dart @@ -0,0 +1,326 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [Locale('en')]; + + /// The title of the application + /// + /// In en, this message translates to: + /// **'Glow'** + String get appTitle; + + /// Subtitle on the welcome screen + /// + /// In en, this message translates to: + /// **'Personalized Wallpaper & GenUI Showcase'** + String get welcomeSubtitle; + + /// Description on the welcome screen + /// + /// In en, this message translates to: + /// **'Welcome to Glow! Take a fun, interactive personality quiz driven by Gemini AI to discover your unique visual style. We\'ll generate a one-of-a-kind phone wallpaper, and then you can refine it through an iterative, agent-assisted creative process.'** + String get welcomeDescription; + + /// Label for personality quiz feature + /// + /// In en, this message translates to: + /// **'Personality Quiz'** + String get personalityQuiz; + + /// Label for AI generation feature + /// + /// In en, this message translates to: + /// **'AI Generation'** + String get aiGeneration; + + /// Label for iterative refinement feature + /// + /// In en, this message translates to: + /// **'Iterative Refinement'** + String get iterativeRefinement; + + /// Button text to get started + /// + /// In en, this message translates to: + /// **'Get Started'** + String get getStarted; + + /// Button text to change API key + /// + /// In en, this message translates to: + /// **'Change API Key'** + String get changeApiKey; + + /// Footer text showing technology stack + /// + /// In en, this message translates to: + /// **'Powered by GenUI SDK for Flutter & Gemini Models'** + String get poweredBy; + + /// Title of the API key dialog + /// + /// In en, this message translates to: + /// **'Enter Gemini API Key'** + String get apiKeyDialogTitle; + + /// Description in the API key dialog + /// + /// In en, this message translates to: + /// **'To use Glow, you need a free Gemini API Key.'** + String get apiKeyDialogDescription; + + /// Link text to get an API key + /// + /// In en, this message translates to: + /// **'Get an API Key here'** + String get getApiKeyHere; + + /// Label for API key input field + /// + /// In en, this message translates to: + /// **'API Key'** + String get apiKeyLabel; + + /// Cancel button text + /// + /// In en, this message translates to: + /// **'Cancel'** + String get cancel; + + /// Save button text + /// + /// In en, this message translates to: + /// **'Save'** + String get save; + + /// Title of the quiz screen + /// + /// In en, this message translates to: + /// **'Glow Personality Quiz'** + String get quizTitle; + + /// Counter for the current question + /// + /// In en, this message translates to: + /// **'Question {currentIndex} of {totalQuestions}'** + String questionCount(int currentIndex, int totalQuestions); + + /// Next button text + /// + /// In en, this message translates to: + /// **'Next'** + String get next; + + /// Skip to generation button text + /// + /// In en, this message translates to: + /// **'Skip to generation'** + String get skipToGeneration; + + /// Text shown during wallpaper generation + /// + /// In en, this message translates to: + /// **'Generating your personal glow...'** + String get generatingGlow; + + /// Description shown during wallpaper generation + /// + /// In en, this message translates to: + /// **'Using Gemini to create your wallpaper.\nThis may take a moment.'** + String get generationDescription; + + /// Header for adjusting wallpaper settings + /// + /// In en, this message translates to: + /// **'Adjust Wallpaper'** + String get adjustWallpaper; + + /// Header for wallpaper style selection + /// + /// In en, this message translates to: + /// **'Wallpaper Style'** + String get wallpaperStyle; + + /// Label for the regenerate button + /// + /// In en, this message translates to: + /// **'Regenerate'** + String get regenerate; + + /// Label for the Cosmic style + /// + /// In en, this message translates to: + /// **'Cosmic'** + String get styleCosmic; + + /// Label for the Abstract style + /// + /// In en, this message translates to: + /// **'Abstract'** + String get styleAbstract; + + /// Label for the Nature style + /// + /// In en, this message translates to: + /// **'Nature'** + String get styleNature; + + /// Label for the Crystal style + /// + /// In en, this message translates to: + /// **'Crystal'** + String get styleCrystal; + + /// Label for the warm and calm atmosphere setting + /// + /// In en, this message translates to: + /// **'Warm & Calm'** + String get atmosphereWarmCalm; + + /// Label for the cool and energetic atmosphere setting + /// + /// In en, this message translates to: + /// **'Cool & Energetic'** + String get atmosphereCoolEnergetic; + + /// Label for the atmosphere slider + /// + /// In en, this message translates to: + /// **'Atmosphere'** + String get atmosphereLabel; + + /// Label for the key element selection + /// + /// In en, this message translates to: + /// **'Key Element'** + String get keyElementLabel; + + /// Default title for the wallpaper editor + /// + /// In en, this message translates to: + /// **'My Glow Wallpaper'** + String get myGlowWallpaper; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/examples/glow/lib/l10n/app_localizations_en.dart b/examples/glow/lib/l10n/app_localizations_en.dart new file mode 100644 index 000000000..62d1ec7cd --- /dev/null +++ b/examples/glow/lib/l10n/app_localizations_en.dart @@ -0,0 +1,114 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get appTitle => 'Glow'; + + @override + String get welcomeSubtitle => 'Personalized Wallpaper & GenUI Showcase'; + + @override + String get welcomeDescription => + 'Welcome to Glow! Take a fun, interactive personality quiz driven by Gemini AI to discover your unique visual style. We\'ll generate a one-of-a-kind phone wallpaper, and then you can refine it through an iterative, agent-assisted creative process.'; + + @override + String get personalityQuiz => 'Personality Quiz'; + + @override + String get aiGeneration => 'AI Generation'; + + @override + String get iterativeRefinement => 'Iterative Refinement'; + + @override + String get getStarted => 'Get Started'; + + @override + String get changeApiKey => 'Change API Key'; + + @override + String get poweredBy => 'Powered by GenUI SDK for Flutter & Gemini Models'; + + @override + String get apiKeyDialogTitle => 'Enter Gemini API Key'; + + @override + String get apiKeyDialogDescription => + 'To use Glow, you need a free Gemini API Key.'; + + @override + String get getApiKeyHere => 'Get an API Key here'; + + @override + String get apiKeyLabel => 'API Key'; + + @override + String get cancel => 'Cancel'; + + @override + String get save => 'Save'; + + @override + String get quizTitle => 'Glow Personality Quiz'; + + @override + String questionCount(int currentIndex, int totalQuestions) { + return 'Question $currentIndex of $totalQuestions'; + } + + @override + String get next => 'Next'; + + @override + String get skipToGeneration => 'Skip to generation'; + + @override + String get generatingGlow => 'Generating your personal glow...'; + + @override + String get generationDescription => + 'Using Gemini to create your wallpaper.\nThis may take a moment.'; + + @override + String get adjustWallpaper => 'Adjust Wallpaper'; + + @override + String get wallpaperStyle => 'Wallpaper Style'; + + @override + String get regenerate => 'Regenerate'; + + @override + String get styleCosmic => 'Cosmic'; + + @override + String get styleAbstract => 'Abstract'; + + @override + String get styleNature => 'Nature'; + + @override + String get styleCrystal => 'Crystal'; + + @override + String get atmosphereWarmCalm => 'Warm & Calm'; + + @override + String get atmosphereCoolEnergetic => 'Cool & Energetic'; + + @override + String get atmosphereLabel => 'Atmosphere'; + + @override + String get keyElementLabel => 'Key Element'; + + @override + String get myGlowWallpaper => 'My Glow Wallpaper'; +} diff --git a/examples/glow/lib/main.dart b/examples/glow/lib/main.dart new file mode 100644 index 000000000..dcdafba56 --- /dev/null +++ b/examples/glow/lib/main.dart @@ -0,0 +1,55 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/router.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; + +import 'package:glow/view_models/settings_view_model.dart'; + +import 'package:logging/logging.dart'; + +import 'l10n/app_localizations.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Logger.root.level = Level.ALL; + Logger.root.onRecord.listen((record) { + debugPrint('${record.level.name}: ${record.time}: ${record.message}'); + }); + + await SettingsViewModel.instance.loadSettings(); + runApp(const GlowApp()); +} + +class GlowApp extends StatelessWidget { + const GlowApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp.router( + debugShowCheckedModeBanner: false, + theme: ThemeData(useMaterial3: true, fontFamily: 'San Francisco'), + routerConfig: router, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + ); + } +} diff --git a/examples/glow/lib/models/editor_state.dart b/examples/glow/lib/models/editor_state.dart new file mode 100644 index 000000000..6455c0eba --- /dev/null +++ b/examples/glow/lib/models/editor_state.dart @@ -0,0 +1,47 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'dart:typed_data'; + +class EditorState { + final int selectedStyleIndex; + final double atmosphereValue; + final bool isSheetOpen; + final bool isRegenerating; + final Uint8List? currentImage; + + const EditorState({ + this.selectedStyleIndex = 2, + this.atmosphereValue = 0.5, + this.isSheetOpen = false, + this.isRegenerating = false, + this.currentImage, + }); + + EditorState copyWith({ + int? selectedStyleIndex, + double? atmosphereValue, + bool? isSheetOpen, + bool? isRegenerating, + Uint8List? currentImage, + }) { + return EditorState( + selectedStyleIndex: selectedStyleIndex ?? this.selectedStyleIndex, + atmosphereValue: atmosphereValue ?? this.atmosphereValue, + isSheetOpen: isSheetOpen ?? this.isSheetOpen, + isRegenerating: isRegenerating ?? this.isRegenerating, + currentImage: currentImage ?? this.currentImage, + ); + } +} diff --git a/examples/glow/lib/models/quiz_question.dart b/examples/glow/lib/models/quiz_question.dart new file mode 100644 index 000000000..1bfab7f43 --- /dev/null +++ b/examples/glow/lib/models/quiz_question.dart @@ -0,0 +1,108 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +enum QuestionType { + singleChoiceImage, + multipleChoiceImage, + singleChoiceText, + multipleChoiceText, + slider, + toggle, + dropdown, + textInputShort, + textInputLong, +} + +class QuizOption { + final String id; + final String label; + final String? imageSeed; + + QuizOption({required this.id, required this.label, this.imageSeed}); +} + +class QuizQuestion { + final String id; + final String text; + final QuestionType type; + final List? options; + final double? min; + final double? max; + final int? divisions; + + QuizQuestion({ + required this.id, + required this.text, + required this.type, + this.options, + this.min, + this.max, + this.divisions, + }); +} + +final List demoQuestions = [ + QuizQuestion( + id: 'q1', + text: "What atmosphere resonates with you?", + type: QuestionType.singleChoiceImage, + options: [ + QuizOption(id: '1', label: 'Ethereal Dream', imageSeed: 'galaxy'), + QuizOption(id: '2', label: 'Urban Nightlife', imageSeed: 'city'), + QuizOption(id: '3', label: 'Cozy Cabin', imageSeed: 'cabin'), + QuizOption(id: '4', label: 'Digital Zen', imageSeed: 'geometric'), + QuizOption(id: '5', label: 'Retro Neon', imageSeed: 'neon'), + QuizOption(id: '6', label: 'Sunlit Garden', imageSeed: 'flowers'), + ], + ), + QuizQuestion( + id: 'q2', + text: "How much energy do you want in your design?", + type: QuestionType.slider, + min: 0, + max: 100, + divisions: 10, + ), + QuizQuestion( + id: 'q3', + text: "Pick a primary color palette.", + type: QuestionType.singleChoiceText, + options: [ + QuizOption(id: 'warm', label: 'Warm (Red, Orange, Yellow)'), + QuizOption(id: 'cool', label: 'Cool (Blue, Green, Purple)'), + QuizOption(id: 'mono', label: 'Monochrome (Black, White, Grey)'), + ], + ), + QuizQuestion( + id: 'q4', + text: "Enable dark mode optimization?", + type: QuestionType.toggle, + ), + QuizQuestion( + id: 'q5', + text: "Select your art style.", + type: QuestionType.dropdown, + options: [ + QuizOption(id: 'real', label: 'Photorealistic'), + QuizOption(id: 'abstr', label: 'Abstract'), + QuizOption(id: 'illus', label: 'Illustration'), + QuizOption(id: '3d', label: '3D Render'), + ], + ), + QuizQuestion( + id: 'q6', + text: "Describe your dream wallpaper in a few words.", + type: QuestionType.textInputLong, + ), +]; diff --git a/examples/glow/lib/router.dart b/examples/glow/lib/router.dart new file mode 100644 index 000000000..103804f0b --- /dev/null +++ b/examples/glow/lib/router.dart @@ -0,0 +1,37 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:go_router/go_router.dart'; + +import 'screens/generation_screen.dart'; +import 'screens/welcome_screen.dart'; +import 'screens/editor_screen.dart'; + +import 'screens/quiz_screen.dart'; + +final router = GoRouter( + routes: [ + GoRoute(path: '/', builder: (context, state) => const GlowWelcomeScreen()), + GoRoute(path: '/quiz', builder: (context, state) => const GlowQuizScreen()), + GoRoute( + path: '/generation', + builder: (context, state) => + GlowGenerationScreen(answers: state.extra as Map?), + ), + GoRoute( + path: '/editor', + builder: (context, state) => const GlowEditorScreen(), + ), + ], +); diff --git a/examples/glow/lib/screens/editor_screen.dart b/examples/glow/lib/screens/editor_screen.dart new file mode 100644 index 000000000..a34942cf6 --- /dev/null +++ b/examples/glow/lib/screens/editor_screen.dart @@ -0,0 +1,384 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; +import 'package:glow/view_models/editor_view_model.dart'; +import 'package:glow/widgets/images/generating_shader_image.dart'; +import 'package:glow/widgets/backgrounds/glow_orb.dart'; +import 'package:glow/widgets/navigation/glass_top_bar.dart'; +import 'package:glow/widgets/panels/glass_properties_panel.dart'; +import 'package:genui/genui.dart'; +import 'package:glow/genui/editor_catalog.dart'; + +class GlowEditorScreen extends StatefulWidget { + const GlowEditorScreen({super.key}); + + @override + State createState() => _GlowEditorScreenState(); +} + +class _GlowEditorScreenState extends State + with TickerProviderStateMixin { + final DraggableScrollableController _sheetController = + DraggableScrollableController(); + // ignore: unused_field + late final EditorViewModel _viewModel; + + @override + void initState() { + super.initState(); + // Use the singleton instance + _viewModel = EditorViewModel.instance; + _sheetController.addListener(_onSheetChanged); + } + + @override + void dispose() { + _sheetController.removeListener(_onSheetChanged); + _sheetController.dispose(); + super.dispose(); + } + + void _onSheetChanged() { + _viewModel.setSheetOpen(_sheetController.size > 0.15); + } + + void _toggleSheet() { + if (_sheetController.size < 0.2) { + _sheetController.animateTo( + 0.45, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutBack, + ); + } else { + _sheetController.animateTo( + 0.1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOutBack, + ); + } + } + + void _closeSheet() { + _sheetController.animateTo( + 0.0, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn, + ); + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: _viewModel, + builder: (context, _) { + final state = _viewModel.state; + + // 1. BACKGROUND Layer + Widget backgroundLayer = const _BackgroundLayer(); + + // 2. THE GLASS PANEL CONTENT Helper + Widget propertiesContent( + ScrollController? scrollController, + VoidCallback? onClose, { + bool showDragHandle = true, + }) { + // If we have no controls surface yet, show loading + if (_viewModel.controlsSurfaceId == null) { + return GlassPropertiesPanel( + scrollController: scrollController, + onClose: onClose, + showDragHandle: showDragHandle, + // Fallback / Loading Mode + selectedIndex: 0, + atmosphere: 0, + onStyleSelected: (_) {}, + onAtmosphereChanged: (_) {}, + onRegenerate: _viewModel.regenerateImage, + isRegenerating: true, // Only show spinner + ); + } + + // We wrap the surface in EditorControlScope to use the mechanisms we built for the editor + // This allows us to use `EditorControl` widgets (sliders, toggles) + // that call `handler.setValue` -> `EditorViewModel.setControlValue`. + return EditorControlScope( + handler: _EditorBinding(_viewModel), + child: GlassPropertiesPanel( + scrollController: scrollController, + onClose: onClose, + showDragHandle: showDragHandle, + // We override the child of GlassPropertiesPanel to show GenUiSurface instead of static controls + selectedIndex: 0, + atmosphere: 0, + onStyleSelected: (_) {}, + onAtmosphereChanged: (_) {}, + onRegenerate: _viewModel.regenerateImage, + isRegenerating: state.isRegenerating, + onSave: () { + final timestamp = DateTime.now().millisecondsSinceEpoch; + _viewModel.saveImage('glow_wallpaper_$timestamp'); + }, + customContent: GenUiSurface( + surfaceId: _viewModel.controlsSurfaceId!, + host: _viewModel.genUiService!.conversation.host, + ), + ), + ); + } + // ... + // (Middle of file unchanged, we use replace_file_content for chunks) + + return Scaffold( + backgroundColor: GlowTheme.colors.background, + body: Stack( + fit: StackFit.expand, + children: [ + backgroundLayer, + LayoutBuilder( + builder: (context, constraints) { + final isTablet = constraints.maxWidth > 800; + + if (isTablet) { + return Row( + children: [ + Expanded( + child: Stack( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500, + maxHeight: 650, + ), + child: AspectRatio( + aspectRatio: 600 / 800, + child: Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: GlowTheme + .colors + .surfacePrimaryLow, + blurRadius: 40, + spreadRadius: 5, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: GeneratingShaderImage( + imageUrl: + "https://picsum.photos/seed/89/600/800", + imageData: state.currentImage, + isGenerating: state.isRegenerating, + fit: BoxFit.cover, + ), + ), + ), + ), + ), + ), + const Positioned( + top: 0, + left: 0, + right: 0, + child: GlassTopBar(isTablet: true), + ), + ], + ), + ), + ClipRect( + child: Container( + width: 400, + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: GlowTheme.colors.surfaceContainer, + ), + ), + ), + child: Stack( + children: [ + BackdropFilter( + filter: ui.ImageFilter.blur( + sigmaX: 20, + sigmaY: 20, + ), + child: Container( + color: GlowTheme.colors.scrim, + ), + ), + propertiesContent( + null, + null, + showDragHandle: false, + ), + ], + ), + ), + ), + ], + ); + } else { + return Stack( + children: [ + Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 500, + maxHeight: 650, + ), + child: AspectRatio( + aspectRatio: 600 / 800, + child: Container( + margin: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + boxShadow: [ + BoxShadow( + color: GlowTheme.colors.surfacePrimaryLow, + blurRadius: 40, + spreadRadius: 5, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(24), + child: GeneratingShaderImage( + imageUrl: + "https://picsum.photos/seed/89/600/800", + imageData: state.currentImage, + isGenerating: state.isRegenerating, + fit: BoxFit.cover, + ), + ), + ), + ), + ), + ), + Positioned( + top: 0, + left: 0, + right: 0, + child: GlassTopBar( + isTablet: false, + onMenuTap: _toggleSheet, + isMenuOpen: state.isSheetOpen, + ), + ), + DraggableScrollableSheet( + controller: _sheetController, + initialChildSize: 0.15, + minChildSize: 0.0, + maxChildSize: 0.9, + snap: true, + builder: (context, scrollController) { + return ClipRRect( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(30), + ), + child: BackdropFilter( + filter: ui.ImageFilter.blur( + sigmaX: 25, + sigmaY: 25, + ), + child: Container( + decoration: BoxDecoration( + color: GlowTheme.colors.surfaceVariant, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(30), + ), + border: Border( + top: BorderSide( + color: GlowTheme.colors.outlineMedium, + width: 1, + ), + ), + ), + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context) + .copyWith( + dragDevices: { + ui.PointerDeviceKind.touch, + ui.PointerDeviceKind.mouse, + }, + ), + child: propertiesContent( + scrollController, + _closeSheet, + ), + ), + ), + ), + ); + }, + ), + ], + ); + } + }, + ), + ], + ), + ); + }, + ); + } +} + +class _BackgroundLayer extends StatelessWidget { + const _BackgroundLayer(); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(gradient: GlowTheme.gradients.backgroundDark), + child: Stack( + fit: StackFit.expand, + children: [ + Positioned( + top: -100, + left: -100, + child: GlowOrb(color: GlowTheme.colors.secondary, size: 400), + ), + Positioned( + bottom: 100, + right: -50, + child: GlowOrb(color: GlowTheme.colors.primary, size: 300), + ), + ], + ), + ); + } +} + +class _EditorBinding implements EditorControlHandler { + final EditorViewModel vm; + _EditorBinding(this.vm); + + @override + void setValue(String id, dynamic value) { + vm.setControlValue(id, value); + } + + @override + dynamic getValue(String id) { + return vm.getControlValue(id); + } +} diff --git a/examples/glow/lib/screens/generation_screen.dart b/examples/glow/lib/screens/generation_screen.dart new file mode 100644 index 000000000..663cb148b --- /dev/null +++ b/examples/glow/lib/screens/generation_screen.dart @@ -0,0 +1,276 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'dart:ui' as ui; +import 'dart:typed_data'; +import 'package:flutter/scheduler.dart'; +import 'package:glow/theme.dart'; +import 'package:glow/widgets/branding/glow_logo.dart'; +import 'package:glow/widgets/feedback/glow_progress_bar.dart'; +import 'package:glow/widgets/backgrounds/glow_shaders.dart'; +import 'package:glow/services/gemini_service.dart'; +import 'package:glow/utils/prompt_helper.dart'; +import 'package:glow/view_models/settings_view_model.dart'; +import 'package:glow/view_models/editor_view_model.dart'; +import 'package:go_router/go_router.dart'; +import '../l10n/app_localizations.dart'; + +class GlowGenerationScreen extends StatefulWidget { + final void Function(BuildContext context, double progress)? onProgressUpdate; + final Map? answers; + + const GlowGenerationScreen({super.key, this.onProgressUpdate, this.answers}); + + @override + State createState() => _GlowGenerationScreenState(); +} + +class _GlowGenerationScreenState extends State + with TickerProviderStateMixin { + // Animation controller for the progress bar + late final AnimationController _progressController = AnimationController( + vsync: this, + duration: const Duration(seconds: 60), // Slow breathing animation + ); + + // Ticker to drive the shader time uniform + late final Ticker _ticker; + double _time = 0.0; + + // Store the compiled shader programs + ui.FragmentProgram? _orbShaderProgram; + ui.FragmentProgram? _meshShaderProgram; + + Uint8List? _generatedImage; + // ignore: unused_field + bool _isGenerating = false; + bool _isNavigating = false; + + @override + void initState() { + super.initState(); + _loadShader(); + + // Start a repeating "breathing" animation for loading + _progressController.repeat(reverse: true); + + // Start a ticker that updates time for the shader + _ticker = createTicker((elapsed) { + if (mounted) { + setState(() { + _time = elapsed.inMilliseconds / 1000.0; + }); + } + }); + _ticker.start(); + + _startGeneration(); + } + + Future _startGeneration() async { + if (widget.answers == null) return; + + setState(() => _isGenerating = true); + + try { + final apiKey = SettingsViewModel.instance.apiKey; + final service = GeminiService(apiKey: apiKey); + final prompt = PromptHelper.fromQuizAnswers(widget.answers!); + + final image = await service.generateImage(prompt); + + if (mounted) { + setState(() { + _generatedImage = image; + _isGenerating = false; + }); + + // Stop repeating and animate to full + _progressController.stop(); + _progressController + .animateTo(1.0, duration: const Duration(milliseconds: 500)) + .then((_) { + _checkCompletion(); + }); + } + } catch (e) { + debugPrint("Generation Error: $e"); + if (mounted) { + setState(() => _isGenerating = false); + // Fallback or error handling could go here + context.go('/editor'); + } + } + } + + void _checkCompletion() { + if (_generatedImage != null && !_isNavigating) { + _isNavigating = true; + Future.delayed(const Duration(milliseconds: 500), () async { + if (mounted) { + // Set the image in the global view model + EditorViewModel.instance.setImage(_generatedImage); + // Initialize dynamic controls + if (widget.answers != null) { + final prompt = PromptHelper.fromQuizAnswers(widget.answers!); + await EditorViewModel.instance.initialize(prompt); + } + if (mounted) { + context.go('/editor'); + } + } + }); + } + } + + Future _loadShader() async { + try { + final orbProgram = await ui.FragmentProgram.fromAsset( + 'shaders/orb_shader.frag', + ); + final meshProgram = await ui.FragmentProgram.fromAsset( + 'shaders/mesh_gradient.frag', + ); + setState(() { + _orbShaderProgram = orbProgram; + _meshShaderProgram = meshProgram; + }); + } catch (e) { + debugPrint('Error loading shader: $e'); + } + } + + @override + void dispose() { + _ticker.dispose(); + _progressController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + // 1. Background + if (_meshShaderProgram != null) + Positioned.fill( + child: CustomPaint( + painter: MeshGradientPainter( + shaderProgram: _meshShaderProgram!, + time: _time, + colors: [ + GlowTheme.colors.brandBlue, + GlowTheme.colors.brandPurple, + GlowTheme.colors.brandOrange, + GlowTheme.colors.brandCyan, + ], + ), + ), + ), + + SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 20), + const GlowLogoText(), + + const Spacer(flex: 2), + + // 2. SHADER ORB REPLACES IMAGE + Padding( + padding: const EdgeInsets.all(20), + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: 100, + minHeight: 100, + maxWidth: 250, + maxHeight: 250, + ), + child: SizedBox.expand( + child: _orbShaderProgram == null + ? const Center(child: CircularProgressIndicator()) + : CustomPaint( + painter: OrbShaderPainter( + shaderProgram: _orbShaderProgram!, + time: _time, + ), + ), + ), + ), + ), + + const Spacer(flex: 2), + + // 3. Text + Padding( + padding: const EdgeInsets.symmetric(horizontal: 30.0), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Column( + children: [ + Text( + AppLocalizations.of(context)!.generatingGlow, + textAlign: TextAlign.center, + style: GlowTheme.textStyles.titleLarge.copyWith( + shadows: GlowTheme.shadows.textGlow, + ), + ), + const SizedBox(height: 16), + Text( + AppLocalizations.of( + context, + )!.generationDescription, + textAlign: TextAlign.center, + style: GlowTheme.textStyles.bodyMedium, + ), + ], + ), + ), + ), + ), + + const Spacer(flex: 3), + + // 4. Progress Bar + Padding( + padding: const EdgeInsets.fromLTRB(24, 0, 24, 50), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: GlowProgressBar( + controller: _progressController, + colors: [ + GlowTheme.colors.tertiary, + GlowTheme.colors.secondary, + ], + ), + ), + ), + ), + ], + ); + }, + ), + ), + ], + ), + ); + } +} diff --git a/examples/glow/lib/screens/quiz_screen.dart b/examples/glow/lib/screens/quiz_screen.dart new file mode 100644 index 000000000..ec1833060 --- /dev/null +++ b/examples/glow/lib/screens/quiz_screen.dart @@ -0,0 +1,260 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:glow/theme.dart'; +import 'package:glow/view_models/quiz_view_model.dart'; +import 'package:glow/widgets/feedback/glow_progress_bar.dart'; +import 'package:glow/genui/quiz_catalog.dart'; +import 'package:genui/genui.dart'; + +import '../l10n/app_localizations.dart'; + +// --- Main Screen --- + +class GlowQuizScreen extends StatefulWidget { + const GlowQuizScreen({super.key}); + + @override + State createState() => _GlowQuizScreenState(); +} + +class _GlowQuizScreenState extends State + with SingleTickerProviderStateMixin { + late final QuizViewModel _viewModel; + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + // We pass an empty list now as the VM handles everything dynamically + _viewModel = QuizViewModel(questions: []); + } + + @override + void dispose() { + _viewModel.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + void _scrollToBottom() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_scrollController.hasClients) { + // Scroll to the absolute maximum extent to show new content at the bottom + _scrollController.animateTo( + _scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: _viewModel, + builder: (context, _) { + final double progress = _viewModel.progress; + + // Auto-scroll trigger: If we just finished generating a new question + if (!_viewModel.isGenerating && _viewModel.currentSurfaceId != null) { + _scrollToBottom(); + } + + // Auto-navigation when finished + if (_viewModel.isFinished) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (GoRouter.of( + context, + ).routerDelegate.currentConfiguration.fullPath == + '/quiz') { + context.go('/generation', extra: _viewModel.answers); + } + }); + } + + return Scaffold( + backgroundColor: GlowTheme.colors.background, + appBar: AppBar( + backgroundColor: GlowTheme.colors.transparent, + elevation: 0, + centerTitle: true, + title: Text( + AppLocalizations.of(context)!.quizTitle, + style: GlowTheme.textStyles.bodyMedium.copyWith( + color: GlowTheme.colors.onBackground, + ), + ), + leading: IconButton( + icon: Icon( + Icons.arrow_back_ios_new, + size: 20, + color: GlowTheme.colors.onBackground, + ), + onPressed: () => context.go('/'), + ), + ), + body: Column( + children: [ + // Custom Gradient Progress Bar + GlowProgressBar(progress: progress), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric( + horizontal: 24.0, + vertical: 20, + ), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Column( + children: [ + const SizedBox(height: 10), + Text( + AppLocalizations.of( + context, + )!.questionCount(_viewModel.currentIndex + 1, 10), + style: TextStyle( + color: GlowTheme.colors.onBackgroundTertiary, + fontSize: 14, + ), + ), + const SizedBox(height: 20), + + // DYNAMIC BODY GENERATOR + // If generating or no surface yet, show spinner. + if (_viewModel.isGenerating || + _viewModel.currentSurfaceId == null) + const SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ) + else + // We need to wrap this in the HandlerScope so the deep widget can find it. + QuizAnswerHandlerScope( + handler: _QuizBinding(_viewModel), + child: GenUiSurface( + surfaceId: _viewModel.currentSurfaceId!, + host: _viewModel.genUiService.conversation.host, + ), + ), + + const SizedBox(height: 40), + ], + ), + ), + ), + ), + ), + + // Footer Buttons + Padding( + padding: const EdgeInsets.all(24.0), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // NEXT Button + Container( + width: double.infinity, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: GlowTheme.gradients.glassButton, + border: Border.all(color: GlowTheme.colors.outline), + ), + child: ElevatedButton( + onPressed: _viewModel.isGenerating + ? null + : () => _viewModel.nextQuestion(), + style: ElevatedButton.styleFrom( + backgroundColor: GlowTheme.colors.transparent, + shadowColor: GlowTheme.colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: _viewModel.isGenerating + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : Text( + AppLocalizations.of(context)!.next, + style: TextStyle( + fontSize: 18, + color: GlowTheme.colors.onBackground, + ), + ), + ), + ), + + const SizedBox(height: 16), + + // SKIP TO GENERATION Button (Secondary) + TextButton( + onPressed: _viewModel.isGenerating + ? null + : () => _viewModel.skipToGeneration(), + child: Text( + AppLocalizations.of(context)!.skipToGeneration, + style: TextStyle( + color: GlowTheme.colors.onBackgroundTertiary, + fontSize: 16, + ), + ), + ), + ], + ), + ), + ), + ), + const SizedBox(height: 10), + ], + ), + ); + }, + ); + } +} + +class _QuizBinding implements QuizAnswerHandler { + final QuizViewModel vm; + _QuizBinding(this.vm); + + @override + void apiSetAnswer(String qId, dynamic value) { + vm.apiSetAnswer(qId, value); + } + + @override + dynamic getAnswer(String qId) { + return vm.getAnswer(qId); + } + + @override + bool isSelected(String qId, String optId) { + return vm.isSelected(qId, optId); + } +} diff --git a/examples/glow/lib/screens/welcome_screen.dart b/examples/glow/lib/screens/welcome_screen.dart new file mode 100644 index 000000000..0bb58dd06 --- /dev/null +++ b/examples/glow/lib/screens/welcome_screen.dart @@ -0,0 +1,356 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:glow/theme.dart'; +import 'package:glow/widgets/branding/glow_logo.dart'; +import 'package:glow/widgets/backgrounds/glow_background.dart'; +import 'package:glow/widgets/buttons/glow_button.dart'; +import 'package:glow/widgets/cards/glow_cards.dart'; +import 'package:glow/view_models/settings_view_model.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../l10n/app_localizations.dart'; + +class GlowWelcomeScreen extends StatelessWidget { + const GlowWelcomeScreen({super.key}); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: SettingsViewModel.instance, + builder: (context, _) { + return Scaffold( + body: Stack( + children: [ + // 1. Background with Wavy Lines + const Positioned.fill(child: GlowBackground()), + + // 2. Main Content + SafeArea( + child: LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth > 800) { + return const _TabletLayout(); + } else { + return const _MobileLayout(); + } + }, + ), + ), + ], + ), + ); + }, + ); + } +} + +class _MobileLayout extends StatelessWidget { + const _MobileLayout(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 60), + + // Logo Section + const GlowLogo(), + const SizedBox(height: 20), + + // Subtitle + Text( + l10n.welcomeSubtitle, + textAlign: TextAlign.center, + style: GlowTheme.textStyles.lightTitleLarge.copyWith( + fontSize: 18, + ), + ), + + const SizedBox(height: 40), + + // Description Text + Text( + l10n.welcomeDescription, + textAlign: TextAlign.center, + style: GlowTheme.textStyles.lightBodyMedium, + ), + + const SizedBox(height: 40), + + // Features Row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GlowFeatureItem( + icon: Icons.psychology_outlined, + label: l10n.personalityQuiz, + gradientColors: [ + GlowTheme.colors.brandPurple, + GlowTheme.colors.brandOrange, + ], + ), + GlowFeatureItem( + icon: Icons.brush_outlined, + label: l10n.aiGeneration, + gradientColors: [ + GlowTheme.colors.brandBlue, + GlowTheme.colors.brandPurple, + ], + ), + GlowFeatureItem( + icon: Icons.sync, + label: l10n.iterativeRefinement, + gradientColors: [ + GlowTheme.colors.brandBlueMaterial, + GlowTheme.colors.brandCyan, + ], + ), + ], + ), + + const SizedBox(height: 60), + + // Gradient Button + GlowButton( + text: l10n.getStarted, + onTap: () => _handleGetStarted(context), + ), + if (SettingsViewModel.instance.hasApiKey) ...[ + const SizedBox(height: 16), + TextButton( + onPressed: () => _showApiKeyDialog(context), + child: Text( + l10n.changeApiKey, + style: TextStyle( + color: GlowTheme.colors.brandPurple, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + const SizedBox(height: 40), + + // Footer + Text( + l10n.poweredBy, + style: TextStyle( + fontSize: 12, + color: GlowTheme.colors.outlineVariant, + ), + ), + const SizedBox(height: 20), + ], + ), + ), + ), + ), + ); + } +} + +class _TabletLayout extends StatelessWidget { + const _TabletLayout(); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Center( + child: Container( + constraints: const BoxConstraints(maxWidth: 1000), + padding: const EdgeInsets.symmetric(horizontal: 48.0), + child: Row( + children: [ + // Left Side: Branding + Expanded( + flex: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const GlowLogo(isTablet: true), + const SizedBox(height: 24), + Text( + l10n.welcomeSubtitle, + style: GlowTheme.textStyles.lightTitleLarge, + ), + const SizedBox(height: 24), + Text( + l10n.poweredBy, + style: TextStyle( + fontSize: 14, + color: GlowTheme.colors.onSurfaceSecondary, + ), + ), + ], + ), + ), + const SizedBox(width: 60), + // Right Side: Content & Actions + Expanded( + flex: 3, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + l10n.welcomeDescription, + style: GlowTheme.textStyles.lightBodyLarge, + ), + const SizedBox(height: 48), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GlowFeatureItem( + icon: Icons.psychology_outlined, + label: l10n.personalityQuiz, + gradientColors: [ + GlowTheme.colors.brandPurple, + GlowTheme.colors.brandOrange, + ], + ), + GlowFeatureItem( + icon: Icons.brush_outlined, + label: l10n.aiGeneration, + gradientColors: [ + GlowTheme.colors.brandBlue, + GlowTheme.colors.brandPurple, + ], + ), + GlowFeatureItem( + icon: Icons.sync, + label: l10n.iterativeRefinement, + gradientColors: [ + GlowTheme.colors.brandBlueMaterial, + GlowTheme.colors.brandCyan, + ], + ), + ], + ), + const SizedBox(height: 60), + SizedBox( + width: 300, + child: GlowButton( + text: l10n.getStarted, + onTap: () => _handleGetStarted(context), + ), + ), + if (SettingsViewModel.instance.hasApiKey) ...[ + const SizedBox(height: 16), + TextButton( + onPressed: () => _showApiKeyDialog(context), + child: Text( + l10n.changeApiKey, + style: TextStyle( + color: GlowTheme.colors.brandPurple, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ], + ), + ), + ], + ), + ), + ); + } +} + +void _handleGetStarted(BuildContext context) { + if (SettingsViewModel.instance.hasApiKey) { + context.go('/quiz'); + } else { + _showApiKeyDialog(context); + } +} + +void _showApiKeyDialog(BuildContext context) { + final controller = TextEditingController( + text: SettingsViewModel.instance.apiKey, + ); + final l10n = AppLocalizations.of(context)!; + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text(l10n.apiKeyDialogTitle), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.apiKeyDialogDescription, + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 8), + InkWell( + onTap: () async { + const url = "https://aistudio.google.com/app/apikey"; + if (await canLaunchUrl(Uri.parse(url))) { + await launchUrl(Uri.parse(url)); + } + }, + child: Text( + l10n.getApiKeyHere, + style: TextStyle( + color: GlowTheme.colors.brandBlue, + decoration: TextDecoration.underline, + ), + ), + ), + const SizedBox(height: 16), + TextField( + controller: controller, + obscureText: true, + decoration: InputDecoration( + labelText: l10n.apiKeyLabel, + border: const OutlineInputBorder(), + ), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(l10n.cancel), + ), + FilledButton( + onPressed: () async { + if (controller.text.isNotEmpty) { + await SettingsViewModel.instance.setApiKey(controller.text); + if (context.mounted) { + Navigator.pop(context); + } + } + }, + child: Text(l10n.save), + ), + ], + ); + }, + ); +} diff --git a/examples/glow/lib/services/gemini_service.dart b/examples/glow/lib/services/gemini_service.dart new file mode 100644 index 000000000..196f54bff --- /dev/null +++ b/examples/glow/lib/services/gemini_service.dart @@ -0,0 +1,84 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'dart:typed_data'; +import 'package:glow/constants.dart'; +import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart'; + +class GeminiService { + late GenerativeService model; + + /// The default model to use for generating content. + String defaultModel = GlowConstants.defaultModel; + String defaultImageModel = GlowConstants.defaultImageModel; + + GeminiService({String? apiKey}) + : model = GenerativeService.fromApiKey(apiKey); + + Future generateText(String prompt, {String? model}) async { + model ??= defaultModel; + final response = await this.model.generateContent( + GenerateContentRequest( + model: model, + contents: [ + Content(parts: [Part(text: prompt)]), + ], + ), + ); + return response.text ?? ''; + } + + Future generateImage(String prompt, {String? model}) async { + model ??= defaultImageModel; + final response = await this.model.generateContent( + GenerateContentRequest( + model: model, + contents: [ + Content(parts: [Part(text: prompt)]), + ], + ), + ); + return response.image ?? Uint8List.fromList([]); + } +} + +extension on GenerateContentResponse { + String? get text { + for (final canidate in candidates) { + if (canidate.content != null) { + final parts = canidate.content!.parts; + for (final part in parts) { + if (part.text != null) { + return part.text!; + } + } + } + } + return null; + } + + Uint8List? get image { + for (final canidate in candidates) { + if (canidate.content != null) { + final parts = canidate.content!.parts; + for (final part in parts) { + if (part.inlineData != null) { + return part.inlineData!.data; + } + } + } + } + return null; + } +} diff --git a/examples/glow/lib/services/genui_service.dart b/examples/glow/lib/services/genui_service.dart new file mode 100644 index 000000000..000b2aa29 --- /dev/null +++ b/examples/glow/lib/services/genui_service.dart @@ -0,0 +1,70 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:genui/genui.dart'; +import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; +import 'package:glow/constants.dart'; + +class GenUiService { + late final GenUiConversation conversation; + late final A2uiMessageProcessor messageProcessor; + + GenUiService({ + String? apiKey, + required String systemInstruction, + List? additionalItems, + void Function(SurfaceAdded)? onSurfaceAdded, + void Function(SurfaceUpdated)? onSurfaceUpdated, + void Function(SurfaceRemoved)? onSurfaceDeleted, + void Function(String)? onTextResponse, + }) { + final mergedCatalog = additionalItems != null + ? CoreCatalogItems.asCatalog().copyWith(additionalItems) + : CoreCatalogItems.asCatalog(); + + messageProcessor = A2uiMessageProcessor( + catalogs: [mergedCatalog], + // We also need to add our custom tools/actions if any. + // For now, implicit. + ); + conversation = GenUiConversation( + contentGenerator: GoogleGenerativeAiContentGenerator( + catalog: mergedCatalog, + systemInstruction: systemInstruction, + modelName: GlowConstants.defaultModel, + apiKey: apiKey, + ), + a2uiMessageProcessor: messageProcessor, + onSurfaceAdded: onSurfaceAdded, + onSurfaceUpdated: onSurfaceUpdated, + onSurfaceDeleted: onSurfaceDeleted, + onTextResponse: onTextResponse, + ); + } + + bool _isDisposed = false; + + void sendRequest(UserMessage message) { + if (_isDisposed) { + // Ignore requests after disposal to prevent "Stream closed" errors + return; + } + conversation.sendRequest(message); + } + + void dispose() { + _isDisposed = true; + conversation.dispose(); + } +} diff --git a/examples/glow/lib/theme.dart b/examples/glow/lib/theme.dart new file mode 100644 index 000000000..b3f07e4c5 --- /dev/null +++ b/examples/glow/lib/theme.dart @@ -0,0 +1,252 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; + +class GlowTheme { + static const colors = GlowColors(); + static const textStyles = GlowTextStyles(); + static const gradients = GlowGradients(); + static const shadows = GlowShadows(); + + static const double opacityLow = 0.2; + static const double opacityMedium = 0.3; + static const double opacityHigh = 0.4; +} + +class GlowColors { + const GlowColors(); + + // Brand Colors + final Color deepPurple = const Color(0xFF2A1A3F); + final Color orangeGlow = const Color(0xFFD96C3A); + final Color purpleAccent = const Color(0xFF9A75E6); + final Color peach = const Color(0xFFF4B097); + final Color blue = const Color(0xFF448AFF); + final Color lightPurple = const Color(0xFFA682FF); + final Color lightOrange = const Color(0xFFFF8A65); + + // Backgrounds + final Color darkBackground = const Color(0xFF121225); + final Color dropdownBackground = const Color(0xFF1E1E2E); + final Color lightBackgroundStart = const Color(0xFFF5F7FA); + final Color lightBackgroundEnd = const Color(0xFFFFF3E0); + final Color logoLightStart = const Color(0xFFF0F4FF); + final Color logoLightEnd = const Color(0xFFFFF0EE); + + // Accents + final Color cyanAccent = Colors.cyanAccent; + final Color blueAccent = Colors.blueAccent; + final Color orangeAccent = Colors.orangeAccent; + + // Basic Colors + final Color white = Colors.white; + final Color white10 = Colors.white10; + final Color white24 = Colors.white24; + final Color white30 = Colors.white30; + final Color white54 = Colors.white54; + final Color white70 = Colors.white70; + + final Color black = Colors.black; + final Color black87 = Colors.black87; + final Color black54 = Colors.black54; + + final Color transparent = Colors.transparent; + final Color grey = Colors.grey; + final Color grey200 = const Color(0xFFEEEEEE); + + // Extended Accents + final Color purple = Colors.purple; + final Color teal = Colors.teal; + final Color pinkAccent = Colors.pinkAccent; + final Color cyan = Colors.cyan; + + // Text + final Color textDark = Colors.black87; + final Color textLight = Colors.white; + final Color textLight70 = Colors.white70; + final Color textLight54 = Colors.white54; + final Color textLight30 = Colors.white30; + + // Semantic Tokens + Color get background => darkBackground; + Color get surface => dropdownBackground; + + Color get onBackground => white; + Color get onBackgroundSecondary => white70; + Color get onBackgroundTertiary => white54; + Color get onBackgroundDisabled => white30; + + Color get onSurface => black87; + Color get onSurfaceSecondary => black54; + + Color get primary => cyanAccent; + Color get onPrimary => black87; + + Color get secondary => purpleAccent; + Color get tertiary => orangeAccent; + + Color get outline => white10; + Color get outlineMedium => onBackground.withValues(alpha: 0.2); + Color get outlineStrong => white24; + Color get outlineVariant => grey200; + + Color get scrim => black.withValues(alpha: 0.3); + Color get surfaceVariant => const Color(0xFF1E293B).withValues(alpha: 0.6); + + Color get logoStart => logoLightStart; + Color get logoEnd => logoLightEnd; + + Color get dimmer => black.withValues(alpha: 0.2); + Color get surfaceScrim => black.withValues(alpha: 0.9); + + // Brand Semantic Tokens + Color get brandBlue => blueAccent; + Color get brandPurple => purpleAccent; + Color get brandOrange => orangeAccent; + Color get brandCyan => cyanAccent; + Color get brandBlueMaterial => Colors.blue; + + // Surface Opacities + Color get surfacePrimary => primary.withValues(alpha: 0.15); + Color get surfacePrimaryLow => primary.withValues(alpha: 0.2); + Color get surfaceContainerLow => onBackground.withValues(alpha: 0.05); + Color get surfaceContainer => onBackground.withValues(alpha: 0.1); + Color get surfaceContainerHigh => onBackground.withValues(alpha: 0.3); + + // Glows & Shadows + Color get glowPrimary => primary.withValues(alpha: 0.2); + Color get glowPrimaryStrong => primary.withValues(alpha: 0.4); + Color get glowSecondary => secondary.withValues(alpha: 0.4); + Color get glowTertiary => tertiary.withValues(alpha: 0.5); + Color get glowTertiaryStrong => tertiary.withValues(alpha: 0.6); + + // Text & Icons + Color get textMedium => onBackground.withValues(alpha: 0.6); + Color get iconStrong => onBackground.withValues(alpha: 0.8); + + // Waves + Color get waveTertiary => tertiary.withValues(alpha: 0.2); + Color get wavePrimary => primary.withValues(alpha: 0.15); + Color get waveTertiaryWeak => tertiary.withValues(alpha: 0.1); +} + +class GlowTextStyles { + const GlowTextStyles(); + + TextStyle get logo => + const TextStyle(fontWeight: FontWeight.bold, color: Colors.white); + + TextStyle get titleLarge => const TextStyle( + fontSize: 32, + fontWeight: FontWeight.bold, + color: Colors.white, + ); + + TextStyle get titleMedium => const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: Colors.white, + ); + + TextStyle get bodyLarge => const TextStyle(fontSize: 18, color: Colors.white); + + TextStyle get bodyMedium => + const TextStyle(fontSize: 16, color: Colors.white70, height: 1.5); + + TextStyle get bodySmall => + const TextStyle(fontSize: 14, color: Colors.white54); + + // Light Theme Text Styles + TextStyle get lightTitleLarge => const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ); + + TextStyle get lightBodyLarge => + const TextStyle(fontSize: 18, height: 1.6, color: Colors.black87); + + TextStyle get lightBodyMedium => + const TextStyle(fontSize: 16, height: 1.5, color: Colors.black87); +} + +class GlowGradients { + const GlowGradients(); + + LinearGradient get logoMask => const LinearGradient( + colors: [Colors.white, Color(0xFFF4B097)], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + LinearGradient get brand => const LinearGradient( + colors: [ + Color(0xFF448AFF), // Blue + Color(0xFFA682FF), // Purple + Color(0xFFFF8A65), // Orange/Peach + ], + ); + + LinearGradient get quizProgressBar => + const LinearGradient(colors: [Colors.cyanAccent, Colors.blueAccent]); + + LinearGradient get glassButton => LinearGradient( + colors: [ + Colors.white.withValues(alpha: 0.15), + Colors.white.withValues(alpha: 0.05), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ); + + LinearGradient get backgroundLight => const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFFF5F7FA), + Colors.white, + Color(0xFFFFF3E0), + Color(0xFFF5F7FA), + ], + ); + + LinearGradient get backgroundDark => const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Color(0xFF0F172A), // Deep Blue/Black + Color(0xFF312E81), // Indigo + Color(0xFF059669), // Emerald tint + Color(0xFF0F172A), + ], + ); +} + +class GlowShadows { + const GlowShadows(); + + List get logoGlow => [ + const Shadow(color: Color(0xAAFFA726), blurRadius: 15.0), + ]; + + List get textGlow => [ + const Shadow(color: Color(0xFFD96C3A), blurRadius: 20.0), + ]; + + List get neonBlue => [ + const Shadow(color: Colors.cyan, blurRadius: 25), + const Shadow(color: Colors.blue, blurRadius: 45), + ]; +} diff --git a/examples/glow/lib/utils/prompt_helper.dart b/examples/glow/lib/utils/prompt_helper.dart new file mode 100644 index 000000000..03ebebe2a --- /dev/null +++ b/examples/glow/lib/utils/prompt_helper.dart @@ -0,0 +1,67 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:glow/models/editor_state.dart'; + +class PromptHelper { + static String fromQuizAnswers(Map answers) { + final buffer = StringBuffer(); + buffer.writeln( + 'Generate a high quality abstract phone wallpaper based on these preferences:', + ); + + answers.forEach((key, value) { + if (value is List) { + buffer.writeln('- $key: ${value.join(", ")}'); + } else { + buffer.writeln('- $key: $value'); + } + }); + + buffer.writeln( + '\nThe image should be colorful, vibrant, and suitable for a mobile background.', + ); + return buffer.toString(); + } + + static String fromEditorState(EditorState state, {String? baseDescription}) { + final buffer = StringBuffer(); + buffer.writeln('Generate a phone wallpaper.'); + if (baseDescription != null) { + buffer.writeln('Base concept: $baseDescription'); + } + + // Styles based on index (Mapping valid as of current EditorScreen UI) + final styles = ['Minimalist', 'Abstract', 'Cyberpunk', 'Nature', 'Retro']; + final style = state.selectedStyleIndex < styles.length + ? styles[state.selectedStyleIndex] + : 'Abstract'; + + buffer.writeln('Style: $style'); + buffer.writeln( + 'Atmosphere Intensity: ${state.atmosphereValue.toStringAsFixed(2)}', + ); + + // Interpret atmosphere + if (state.atmosphereValue < 0.3) { + buffer.writeln('Mood: Calm, soft, muted colors.'); + } else if (state.atmosphereValue > 0.7) { + buffer.writeln('Mood: Intense, high contrast, dynamic.'); + } else { + buffer.writeln('Mood: Balanced, pleasing aesthetics.'); + } + + return buffer.toString(); + } +} diff --git a/examples/glow/lib/view_models/editor_view_model.dart b/examples/glow/lib/view_models/editor_view_model.dart new file mode 100644 index 000000000..0f4748fe4 --- /dev/null +++ b/examples/glow/lib/view_models/editor_view_model.dart @@ -0,0 +1,193 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'dart:async'; +import 'dart:typed_data'; +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/material.dart'; +import 'package:glow/models/editor_state.dart'; +import 'package:glow/services/gemini_service.dart'; +import 'package:glow/services/genui_service.dart'; +import 'package:glow/view_models/settings_view_model.dart'; +import 'package:glow/genui/editor_catalog.dart'; // Use Editor Catalog +import 'package:logging/logging.dart'; +import 'package:genui/genui.dart'; + +class EditorViewModel extends ChangeNotifier { + static final EditorViewModel instance = EditorViewModel._(); + final Logger _log = Logger('EditorViewModel'); + + EditorViewModel._(); + + EditorState _state = const EditorState(); + EditorState get state => _state; + + // GenUI for Dynamic Controls + GenUiService? _genUiService; + GenUiService? get genUiService => _genUiService; + String? _controlsSurfaceId; + String? get controlsSurfaceId => _controlsSurfaceId; + + // Track prompt context + String? _basePrompt; + + final Map _controlValues = {}; + + Future initialize(String prompt) async { + _basePrompt = prompt; + _disposeGenUi(); + + _log.info('Initializing Dynamic Editor with prompt: $prompt'); + + // Do NOT auto-regenerate image here. The image is passed from GenerationScreen. + + final completer = Completer(); + + // Initialize GenUI for Controls + final apiKey = SettingsViewModel.instance.apiKey; + const systemInstruction = ''' + You are an expert UI designer for a Wallpaper Editor. + Based on the user's wallpaper description, generate a set of PARAMETER CONTROLS using the 'EditorControl' tool. + + Example Controls: + - If prompt is "Cyberpunk City", generate: + - EditorControl(id="neon", label="Neon Intensity", type="slider", min=0, max=100, defaultValue=80) + - EditorControl(id="rain", label="Rain Amount", type="slider", min=0, max=100) + + Action: Generate a surface with 3-5 relevant controls. + IMPORTANT: vary the control types! + - Use 'slider' for intensity/amount values. + - Use 'toggle' for binary on/off features (e.g., "Neon Flicker", "HDR Mode"). + - Use 'dropdown' for selecting styles/variants (e.g., "Color Palette" -> ["Cyber", "Vapor", "Noir"]). + '''; + + _genUiService = GenUiService( + apiKey: apiKey, + systemInstruction: systemInstruction, + additionalItems: editorCatalogItems, // Use Editor Catalog + onSurfaceAdded: (surface) { + _log.info('Controls Surface Added: ${surface.surfaceId}'); + _controlsSurfaceId = surface.surfaceId; + if (!completer.isCompleted) completer.complete(); + notifyListeners(); + }, + onSurfaceUpdated: (update) { + _log.info('Controls Surface Updated: ${update.surfaceId}'); + }, + ); + + // Send initial request for controls + _genUiService?.sendRequest( + UserMessage.text("Generate controls for this wallpaper: $prompt"), + ); + + return completer.future; + } + + void _disposeGenUi() { + _genUiService?.dispose(); + _genUiService = null; + _controlsSurfaceId = null; + _controlValues.clear(); + } + + // Handle Updates from Dynamic Controls + // This mimics QuizViewModel's apiSetAnswer but for controls + void setControlValue(String id, dynamic value) { + _controlValues[id] = value; + notifyListeners(); + } + + dynamic getControlValue(String id) => _controlValues[id]; + + void setImage(Uint8List? image) { + if (image != null) { + _state = _state.copyWith(currentImage: image, isRegenerating: false); + notifyListeners(); + } + } + + // Standard UI actions + void setSheetOpen(bool isOpen) { + if (_state.isSheetOpen != isOpen) { + _state = _state.copyWith(isSheetOpen: isOpen); + notifyListeners(); + } + } + + Future regenerateImage() async { + if (_state.isRegenerating || _basePrompt == null) return; + + _state = _state.copyWith(isRegenerating: true); + notifyListeners(); + + try { + final apiKey = SettingsViewModel.instance.apiKey; + final service = GeminiService(apiKey: apiKey); + + // Construct prompt with dynamic control values + final StringBuffer modifiedPrompt = StringBuffer(_basePrompt!); + if (_controlValues.isNotEmpty) { + modifiedPrompt.write(" Adjusted with: "); + _controlValues.forEach((key, value) { + modifiedPrompt.write("$key: $value, "); + }); + } + + _log.info('Regenerating with prompt: ${modifiedPrompt.toString()}'); + final image = await service.generateImage(modifiedPrompt.toString()); + + _state = _state.copyWith(isRegenerating: false, currentImage: image); + + // Trigger GenUI Update for Controls + if (_genUiService != null) { + final updateMessage = + "I have updated the wallpaper based on the following adjustments: ${_controlValues.toString()}. Please update the available controls to be relevant to this new state. For example, if 'Rain' was increased, maybe offer 'Lightning' or 'Puddle Reflections' next. Maintain a diverse set of controls."; + + final parts = [ + TextPart(updateMessage), + if (_state.currentImage != null) + DataPart({'mime_type': 'image/png', 'data': _state.currentImage!}), + ]; + _genUiService!.sendRequest(UserMessage(parts)); + } + } catch (e) { + _log.severe("Regeneration Error: $e"); + _state = _state.copyWith(isRegenerating: false); + } + notifyListeners(); + } + + Future saveImage(String filename) async { + if (_state.currentImage == null) return; + + try { + await FileSaver.instance.saveFile( + name: filename, + bytes: _state.currentImage!, + fileExtension: 'png', + mimeType: MimeType.png, + ); + _log.info('Image saved successfully: $filename'); + } catch (e) { + _log.severe('Error saving image: $e'); + } + } + + @override + void dispose() { + _disposeGenUi(); + super.dispose(); + } +} diff --git a/examples/glow/lib/view_models/quiz_view_model.dart b/examples/glow/lib/view_models/quiz_view_model.dart new file mode 100644 index 000000000..b54917c95 --- /dev/null +++ b/examples/glow/lib/view_models/quiz_view_model.dart @@ -0,0 +1,242 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; +import 'package:logging/logging.dart'; +import 'package:glow/genui/quiz_catalog.dart'; +import 'package:glow/models/quiz_question.dart'; +import 'package:glow/services/genui_service.dart'; +import 'package:glow/view_models/settings_view_model.dart'; + +class QuizViewModel extends ChangeNotifier { + final List questions; + final Logger _log = Logger('QuizViewModel'); + + int _currentIndex = 0; + final Map _answers = {}; + + // GenUI State + late final GenUiService _genUiService; + GenUiService get genUiService => _genUiService; + + // Track the most recent surface for the current step + String? _latestSurfaceId; + String? _currentRootId; + bool _isGenerating = false; + + // System Instruction + static const _systemInstruction = ''' + You are an expert AI Digital Artist & Wallpaper Curator for 'Glow'. + Your goal is to create a stunning, personalized DIGITAL WALLPAPER (for a phone or desktop screen) based on the user's taste. + + CONTEXT: + - This is NOT about home decor or physical wall paper. + - This IS about digital art, abstract backgrounds, landscapes, or artistic scenes for a SCREEN. + + STRATEGY: + - Start with MOOD and VIBE (e.g., "Dreamy & Ethereal", "Cyberpunk", "Minimalist Nature"). + - Ask about ARTISTIC STYLE relative to digital art (e.g., "3D Render", "Oil Painting", "Vector Art", "Neon"). + - Ask about specific ELEMENTS (e.g., "Clouds", "Geometric shapes", "Gradient"). + - You can ask up to 10 questions. + + RULES: + RULES: + 1. Ask EXACTLY ONE question at a time using the 'QuizQuestion' tool. + 2. STOP and WAIT for the user to answer. DO NOT simulate the user's answer. + 3. DO NOT generate multiple questions in a sequence. + 4. NEVER ask questions in plain text. ALWAYS use the 'QuizQuestion' tool. + 5. If the user asks to "Skip" or says they are done, output "SUMMARY: " followed by the final prompt. + 6. After the 10th question is answered, output "SUMMARY: " followed by the final prompt. + + CRITICAL: + - You MUST wait for the user's response after every question. + - Do NOT advance the conversation yourself. + + Action: Ask Question 1 using the QuizQuestion tool. + '''; + + QuizViewModel({required List questions}) : questions = [] { + _log.info('Initialized Step-by-Step Dynamic Quiz'); + _initGenUi(); + } + + // Completion State + bool _isFinished = false; + bool get isFinished => _isFinished; + String? _summaryPrompt; + String? get summaryPrompt => _summaryPrompt; + + void _initGenUi() { + final apiKey = SettingsViewModel.instance.apiKey; + _genUiService = GenUiService( + apiKey: apiKey, + systemInstruction: _systemInstruction, + additionalItems: quizCatalogItems, + onSurfaceAdded: (surface) { + final rootId = surface.definition.rootComponentId; + _log.info('Surface Added: ${surface.surfaceId} Root: $rootId'); + _latestSurfaceId = surface.surfaceId; + _currentRootId = rootId; + _isGenerating = false; + notifyListeners(); + }, + onSurfaceUpdated: (update) { + _log.info('Surface Updated: ${update.surfaceId}'); + if (_isGenerating) { + // Check if root has changed to prevent flashing old content + final definition = update.definition; + final rootId = definition.rootComponentId; + + if (rootId != null && rootId != _currentRootId) { + _log.info('Root Changed: $_currentRootId -> $rootId'); + _currentRootId = rootId; + _latestSurfaceId = update.surfaceId; + _isGenerating = false; + notifyListeners(); + } else { + _log.info( + 'Surface updated but root is same ($_currentRootId). Keeping spinner...', + ); + } + } + }, + onTextResponse: (text) { + _log.info('Text Response: $text'); + + if (text.contains("SUMMARY:")) { + _summaryPrompt = text.split("SUMMARY:").last.trim(); + _answers['prompt'] = _summaryPrompt; + _isFinished = true; + _isGenerating = false; + _log.info('Quiz Finished. Summary: $_summaryPrompt'); + notifyListeners(); + } else if (_isGenerating) { + _log.warning('Model returned text but we expected a tool call (UI).'); + // Auto-recover by asking for the tool + _genUiService.sendRequest( + UserMessage.text( + "Please display the question using the 'QuizQuestion' tool. Do not just say it.", + ), + ); + } + }, + ); + + // Start the conversation + _isGenerating = true; + _genUiService.sendRequest( + UserMessage.text( + "Hi! Let's start the wallpaper personalization quiz. Please ask the first question.", + ), + ); + } + + // Current State + int get currentIndex => _currentIndex; + + double get progress { + if (_isFinished) return 1.0; + // 0..9 -> 10 questions. + return (_currentIndex + 1) / 10.0; + } + + Map get answers => _answers; + + // Surface Logic + QuizQuestion? get currentStaticQuestion => null; // No static questions + bool get isDynamic => true; + + String? get currentSurfaceId => _latestSurfaceId; + bool get isGenerating => _isGenerating; + + @override + void dispose() { + _genUiService.dispose(); + super.dispose(); + } + + Future nextQuestion() async { + _log.info('nextQuestion called. Stack: ${StackTrace.current}'); + // 1. Mark as generating (UI shows spinner) + _isGenerating = true; + notifyListeners(); + + // 2. Prepare context + final currentAnswers = _answers.toString(); + _log.info('Submitting answer for Q${_currentIndex + 1}...'); + + // 3. Send request + _genUiService.sendRequest( + UserMessage.text( + "I have answered. Current State: $currentAnswers. Please save this and ASK THE NEXT QUESTION (Question ${_currentIndex + 2}).", + ), + ); + + // 4. Advance index logic + // We increment index to show progress, but we wait for `isGenerating` to flip back to false before showing surface. + _currentIndex++; + notifyListeners(); + } + + Future skipToGeneration() async { + _isGenerating = true; + notifyListeners(); + + _log.info('User requested Skip/Finish.'); + final currentAnswers = _answers.toString(); + + _genUiService.sendRequest( + UserMessage.text( + "I am satisfied with my answers so far: $currentAnswers. Please STOP asking questions and generate the SUMMARY now.", + ), + ); + } + + // Helpers + void setAnswer(String questionId, dynamic value) { + _answers[questionId] = value; + notifyListeners(); + } + + // Support for dynamic widgets calling back + void apiSetAnswer(String qId, dynamic value) { + setAnswer(qId, value); + } + + dynamic getAnswer(String qId) => _answers[qId]; + + bool isSelected(String qId, String optId) { + final ans = _answers[qId]; + if (ans is List) { + return ans.contains(optId); + } + return ans == optId; + } + + void handleSelection(QuizQuestion q, String optId) { + if (q.type == QuestionType.multipleChoiceImage || + q.type == QuestionType.multipleChoiceText) { + List current = List.from(_answers[q.id] ?? []); + if (current.contains(optId)) { + current.remove(optId); + } else { + current.add(optId); + } + setAnswer(q.id, current); + } else { + setAnswer(q.id, optId); + } + } +} diff --git a/examples/glow/lib/view_models/settings_view_model.dart b/examples/glow/lib/view_models/settings_view_model.dart new file mode 100644 index 000000000..6793d503f --- /dev/null +++ b/examples/glow/lib/view_models/settings_view_model.dart @@ -0,0 +1,75 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsViewModel extends ChangeNotifier { + static const String _apiKeyKey = 'gemini_api_key'; + + static final SettingsViewModel instance = SettingsViewModel._(); + SettingsViewModel._() { + loadSettings(); + } + + static const String _generationTimeKey = 'generation_time_ms'; + + String? _apiKey; + int _predictedGenerationTimeMs = 10000; // Default to 10 seconds + bool _isLoaded = false; + + String? get apiKey => _apiKey; + bool get isLoaded => _isLoaded; + bool get hasApiKey => _apiKey != null && _apiKey!.isNotEmpty; + Duration get predictedGenerationDuration => + Duration(milliseconds: _predictedGenerationTimeMs); + + Future loadSettings() async { + final prefs = await SharedPreferences.getInstance(); + _apiKey = prefs.getString(_apiKeyKey); + _predictedGenerationTimeMs = prefs.getInt(_generationTimeKey) ?? 10000; + _isLoaded = true; + notifyListeners(); + } + + Future setApiKey(String key) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString(_apiKeyKey, key); + _apiKey = key; + notifyListeners(); + } + + Future updateGenerationTime(Duration duration) async { + final prefs = await SharedPreferences.getInstance(); + // Simple moving average or direct update? User said "use that for the next". + // Let's weighted average it to smooth out outliers: 70% old, 30% new + final newTime = + (0.7 * _predictedGenerationTimeMs + 0.3 * duration.inMilliseconds) + .round(); + + // Clamp to reasonable bounds (e.g. 8s to 30s) + final clampedTime = newTime.clamp(8000, 30000); + + await prefs.setInt(_generationTimeKey, clampedTime); + _predictedGenerationTimeMs = clampedTime; + notifyListeners(); + } + + Future clearApiKey() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_apiKeyKey); + _apiKey = null; + notifyListeners(); + } +} diff --git a/examples/glow/lib/widgets/backgrounds/glow_background.dart b/examples/glow/lib/widgets/backgrounds/glow_background.dart new file mode 100644 index 000000000..07f03edc9 --- /dev/null +++ b/examples/glow/lib/widgets/backgrounds/glow_background.dart @@ -0,0 +1,80 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// A subtle wavy background with a gradient and animated-looking lines. +class GlowBackground extends StatelessWidget { + const GlowBackground({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration(gradient: GlowTheme.gradients.backgroundLight), + child: CustomPaint(painter: WavyLinePainter()), + ); + } +} + +/// Custom painter for drawing subtle wavy lines on the background. +class WavyLinePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + // Helper to draw a wave + void drawWave(double offset, Color color) { + paint.color = color; + final path = Path(); + path.moveTo(0, size.height * 0.1 + offset); + + path.quadraticBezierTo( + size.width * 0.25, + size.height * 0.15 + offset + 50, + size.width * 0.5, + size.height * 0.1 + offset, + ); + path.quadraticBezierTo( + size.width * 0.75, + size.height * 0.05 + offset - 50, + size.width, + size.height * 0.1 + offset, + ); + + canvas.drawPath(path, paint); + } + + // Top left subtle waves + drawWave(0, GlowTheme.colors.waveTertiary); + drawWave(20, GlowTheme.colors.wavePrimary); + + // Bottom left subtle waves + final pathBottom = Path(); + paint.color = GlowTheme.colors.waveTertiaryWeak; + pathBottom.moveTo(0, size.height * 0.85); + pathBottom.quadraticBezierTo( + size.width * 0.5, + size.height * 0.95, + size.width, + size.height * 0.8, + ); + canvas.drawPath(pathBottom, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/examples/glow/lib/widgets/backgrounds/glow_orb.dart b/examples/glow/lib/widgets/backgrounds/glow_orb.dart new file mode 100644 index 000000000..ecfa54e48 --- /dev/null +++ b/examples/glow/lib/widgets/backgrounds/glow_orb.dart @@ -0,0 +1,54 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// A glowing orb widget used for creating ambient background effects. +class GlowOrb extends StatelessWidget { + /// The base color of the orb. + final Color color; + + /// The diameter of the orb. + final double size; + + /// Optional opacity for the orb's core. + final double? opacity; + + const GlowOrb({ + super.key, + required this.color, + required this.size, + this.opacity, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: color.withValues(alpha: opacity ?? GlowTheme.opacityMedium), + boxShadow: [ + BoxShadow( + color: color.withValues(alpha: GlowTheme.opacityHigh), + blurRadius: size / 4, + spreadRadius: size / 20, + ), + ], + ), + ); + } +} diff --git a/examples/glow/lib/widgets/backgrounds/glow_shaders.dart b/examples/glow/lib/widgets/backgrounds/glow_shaders.dart new file mode 100644 index 000000000..d05829a2d --- /dev/null +++ b/examples/glow/lib/widgets/backgrounds/glow_shaders.dart @@ -0,0 +1,86 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; + +/// Painter for an animated glowing orb using a fragment shader. +class OrbShaderPainter extends CustomPainter { + final ui.FragmentProgram shaderProgram; + final double time; + + OrbShaderPainter({required this.shaderProgram, required this.time}); + + @override + void paint(Canvas canvas, Size size) { + final shader = shaderProgram.fragmentShader(); + + // Pass Uniforms matches the order in .frag file + // 1. uSize (vec2) -> floats 0, 1 + shader.setFloat(0, size.width); + shader.setFloat(1, size.height); + // 2. uTime (float) -> float 2 + shader.setFloat(2, time); + + final paint = Paint()..shader = shader; + canvas.drawRect(Offset.zero & size, paint); + } + + @override + bool shouldRepaint(covariant OrbShaderPainter oldDelegate) { + return oldDelegate.time != time; + } +} + +/// Painter for a mesh gradient background using a fragment shader. +class MeshGradientPainter extends CustomPainter { + final ui.FragmentProgram shaderProgram; + final double time; + final List colors; + + MeshGradientPainter({ + required this.shaderProgram, + required this.time, + required this.colors, + }); + + @override + void paint(Canvas canvas, Size size) { + final shader = shaderProgram.fragmentShader(); + + // 1. uSize (vec2) + shader.setFloat(0, size.width); + shader.setFloat(1, size.height); + + // 2. uTime (float) + shader.setFloat(2, time); + + // 3. uColors (vec3 * 4) + // Flatten colors to r, g, b floats + int floatIndex = 3; + for (final color in colors) { + shader.setFloat(floatIndex++, color.r); + shader.setFloat(floatIndex++, color.g); + shader.setFloat(floatIndex++, color.b); + } + + final paint = Paint()..shader = shader; + canvas.drawRect(Offset.zero & size, paint); + } + + @override + bool shouldRepaint(covariant MeshGradientPainter oldDelegate) { + return oldDelegate.time != time; + } +} diff --git a/examples/glow/lib/widgets/branding/glow_logo.dart b/examples/glow/lib/widgets/branding/glow_logo.dart new file mode 100644 index 000000000..f55e6039f --- /dev/null +++ b/examples/glow/lib/widgets/branding/glow_logo.dart @@ -0,0 +1,113 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// The main brand logo for Glow. +/// +/// Shows a glowing circle with a smaller inner circle and the "Glow" text. +/// Supports an [isTablet] mode for larger sizing and alignment adjustments. +class GlowLogo extends StatelessWidget { + /// Whether to use tablet-specific sizing and alignment. + final bool isTablet; + + const GlowLogo({super.key, this.isTablet = false}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: isTablet + ? MainAxisAlignment.start + : MainAxisAlignment.center, + children: [ + // Icon + Container( + width: isTablet ? 80 : 60, + height: isTablet ? 80 : 60, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + GlowTheme.colors.brandBlue, + GlowTheme.colors.brandPurple, + GlowTheme.colors.brandOrange, + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(12.0), // Thickness of the ring + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: GlowTheme.colors.white, // Inner white cutout + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + GlowTheme.colors.logoStart, + GlowTheme.colors.logoEnd, + ], + ), + ), + child: Center( + child: Container( + width: isTablet ? 20 : 15, + height: isTablet ? 20 : 15, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: GlowTheme.colors.glowTertiary, + ), + ), + ), + ), + ), + ), + const SizedBox(width: 12), + // Text + const GlowLogoText(), + ], + ); + } +} + +/// A text-only version of the Glow logo with a gradient mask and glow effects. +class GlowLogoText extends StatelessWidget { + /// Optional font size. If null, uses theme defaults. + final double? fontSize; + + /// Whether to show the logo glow effect. + final bool showGlow; + + const GlowLogoText({super.key, this.fontSize, this.showGlow = true}); + + @override + Widget build(BuildContext context) { + return ShaderMask( + shaderCallback: (bounds) => + GlowTheme.gradients.brand.createShader(bounds), + child: Text( + "Glow", + style: TextStyle( + fontSize: fontSize ?? 64, + fontWeight: FontWeight.bold, + color: GlowTheme.colors.white, + shadows: showGlow ? GlowTheme.shadows.logoGlow : null, + ), + ), + ); + } +} diff --git a/examples/glow/lib/widgets/buttons/glow_button.dart b/examples/glow/lib/widgets/buttons/glow_button.dart new file mode 100644 index 000000000..76e2bee31 --- /dev/null +++ b/examples/glow/lib/widgets/buttons/glow_button.dart @@ -0,0 +1,129 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// A consistent button for the Glow app supporting multiple styles and loading states. +class GlowButton extends StatelessWidget { + /// The text to display on the button. + final String text; + + /// Optional icon to display before the text. + final IconData? icon; + + /// Callback when the button is pressed. + final VoidCallback? onTap; + + /// If true, shows a loading indicator instead of the icon/text. + final bool isLoading; + + /// Whether this is a primary action button (gradient background). + final bool isPrimary; + + /// Whether this is a glass-style button (translucent). + final bool isGlass; + + /// Full width button. + final bool isFullWidth; + + const GlowButton({ + super.key, + required this.text, + this.icon, + this.onTap, + this.isLoading = false, + this.isPrimary = true, + this.isGlass = false, + this.isFullWidth = true, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: (isLoading || onTap == null) ? null : onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: isFullWidth ? double.infinity : null, + height: 56, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(isGlass ? 16 : 30), + gradient: isPrimary + ? GlowTheme.gradients.brand + : (isGlass ? GlowTheme.gradients.glassButton : null), + color: (!isPrimary && !isGlass) + ? GlowTheme.colors.surfaceContainer + : null, + border: (!isPrimary && !isGlass) + ? Border.all(color: GlowTheme.colors.outlineStrong) + : (isGlass ? Border.all(color: GlowTheme.colors.outline) : null), + boxShadow: isPrimary + ? [ + BoxShadow( + color: GlowTheme.colors.glowSecondary, + blurRadius: 12, + offset: const Offset(0, 6), + ), + ] + : null, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: isFullWidth ? MainAxisSize.max : MainAxisSize.min, + children: [ + if (isLoading) + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + color: isPrimary + ? GlowTheme.colors.onPrimary + : GlowTheme.colors.onBackground, + ), + ) + else ...[ + if (icon != null) ...[ + Icon( + icon, + color: isPrimary + ? GlowTheme.colors.onPrimary + : GlowTheme.colors.onBackground, + size: 20, + ), + const SizedBox(width: 8), + ], + Expanded( + child: Text( + text, + style: TextStyle( + color: isPrimary + ? GlowTheme.colors.onPrimary + : GlowTheme.colors.onBackground, + fontSize: 14, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + ), + ], + ], + ), + ), + ), + ); + } +} diff --git a/examples/glow/lib/widgets/cards/glow_cards.dart b/examples/glow/lib/widgets/cards/glow_cards.dart new file mode 100644 index 000000000..aecaabc51 --- /dev/null +++ b/examples/glow/lib/widgets/cards/glow_cards.dart @@ -0,0 +1,313 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// A circular feature item with an icon and label, used on the welcome screen. +class GlowFeatureItem extends StatelessWidget { + final IconData icon; + final String label; + final List gradientColors; + + const GlowFeatureItem({ + super.key, + required this.icon, + required this.label, + required this.gradientColors, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: GlowTheme.colors.white, + border: Border.all( + color: GlowTheme.colors.outlineVariant, + width: 1, + ), + boxShadow: [ + BoxShadow( + color: GlowTheme.colors.outline, + blurRadius: 10, + spreadRadius: 2, + ), + ], + ), + child: ShaderMask( + shaderCallback: (bounds) => LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: gradientColors, + ).createShader(bounds), + child: Icon(icon, size: 36, color: GlowTheme.colors.white), + ), + ), + const SizedBox(height: 12), + Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + height: 1.2, + color: GlowTheme.colors.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ], + ); + } +} + +/// A text-based option card for quizzes. +class GlowQuizOptionCard extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + + const GlowQuizOptionCard({ + super.key, + required this.label, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), + decoration: BoxDecoration( + color: isSelected + ? GlowTheme.colors.surfacePrimary + : GlowTheme.colors.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? GlowTheme.colors.primary + : GlowTheme.colors.outline, + width: 1.5, + ), + ), + child: Row( + children: [ + Text( + label, + style: TextStyle( + color: GlowTheme.colors.onBackground, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const Spacer(), + if (isSelected) + Icon(Icons.check_circle, color: GlowTheme.colors.primary) + else + Icon( + Icons.circle_outlined, + color: GlowTheme.colors.outlineStrong, + ), + ], + ), + ), + ); + } +} + +/// An image-based option card for quizzes. +class GlowQuizImageOption extends StatelessWidget { + final String label; + final String? imageSeed; + final bool isSelected; + final VoidCallback onTap; + + const GlowQuizImageOption({ + super.key, + required this.label, + this.imageSeed, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: isSelected + ? GlowTheme.colors.primary + : GlowTheme.colors.transparent, + width: 3, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: GlowTheme.colors.primary, + blurRadius: 12, + spreadRadius: -2, + ), + ] + : [], + image: DecorationImage( + image: NetworkImage( + "https://picsum.photos/seed/$imageSeed/400/300", + ), + fit: BoxFit.cover, + colorFilter: isSelected + ? null + : ColorFilter.mode(GlowTheme.colors.dimmer, BlendMode.darken), + ), + ), + child: Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4), + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical( + bottom: Radius.circular(12), + ), + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + colors: [ + GlowTheme.colors.surfaceScrim, + GlowTheme.colors.transparent, + ], + ), + ), + child: Text( + label, + textAlign: TextAlign.center, + style: TextStyle( + color: GlowTheme.colors.onBackground, + fontWeight: FontWeight.bold, + fontSize: 14, + ), + ), + ), + ), + ), + ); + } +} + +/// A card representing a wallpaper style in the editor. +class GlowStyleCard extends StatelessWidget { + final String label; + final bool isSelected; + final VoidCallback onTap; + final Color color; + + const GlowStyleCard({ + super.key, + required this.label, + required this.isSelected, + required this.onTap, + required this.color, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + margin: const EdgeInsets.only(right: 16), + width: 80, + child: Column( + children: [ + Container( + height: 70, + width: 70, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + color: color.withValues(alpha: GlowTheme.opacityLow), + border: isSelected + ? Border.all(color: GlowTheme.colors.primary, width: 2) + : Border.all(color: GlowTheme.colors.transparent, width: 2), + boxShadow: isSelected + ? [ + BoxShadow( + color: GlowTheme.colors.glowPrimaryStrong, + blurRadius: 12, + ), + ] + : [], + ), + child: Icon(Icons.wallpaper, color: GlowTheme.colors.iconStrong), + ), + const SizedBox(height: 8), + Text( + label, + style: TextStyle( + color: isSelected + ? GlowTheme.colors.primary + : GlowTheme.colors.onBackgroundSecondary, + fontSize: 12, + ), + ), + ], + ), + ), + ); + } +} + +/// A toggle button for enabling/disabling elements in the editor. +class GlowElementToggle extends StatelessWidget { + final IconData icon; + final bool isActive; + final VoidCallback? onTap; + + const GlowElementToggle({ + super.key, + required this.icon, + required this.isActive, + this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 60, + height: 40, + decoration: BoxDecoration( + color: isActive + ? GlowTheme.colors.primary + : GlowTheme.colors.surfaceContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Icon( + icon, + color: isActive + ? GlowTheme.colors.onSurface + : GlowTheme.colors.onBackgroundTertiary, + ), + ), + ); + } +} diff --git a/examples/glow/lib/widgets/feedback/glow_progress_bar.dart b/examples/glow/lib/widgets/feedback/glow_progress_bar.dart new file mode 100644 index 000000000..4089328f4 --- /dev/null +++ b/examples/glow/lib/widgets/feedback/glow_progress_bar.dart @@ -0,0 +1,89 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// A gradient progress bar with a glowing effect, used across the app to show progress. +class GlowProgressBar extends StatelessWidget { + /// The current progress value (0.0 to 1.0). + final double progress; + + /// Optional animation controller to drive the progress bar. + /// If provided, [progress] is ignored. + final AnimationController? controller; + + /// Colors for the gradient. Defaults to brand colors. + final List? colors; + + /// Height of the progress bar. + final double height; + + const GlowProgressBar({ + super.key, + this.progress = 0.0, + this.controller, + this.colors, + this.height = 6.0, + }); + + @override + Widget build(BuildContext context) { + if (controller != null) { + return AnimatedBuilder( + animation: controller!, + builder: (context, child) => _buildBar(context, controller!.value), + ); + } + return _buildBar(context, progress); + } + + Widget _buildBar(BuildContext context, double value) { + return Stack( + children: [ + // Background track + Container( + height: height, + width: double.infinity, + decoration: BoxDecoration( + color: GlowTheme.colors.outline, + borderRadius: BorderRadius.circular(height / 2), + ), + ), + // Progress track + Container( + height: height, + width: MediaQuery.of(context).size.width * value, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(height / 2), + gradient: LinearGradient( + colors: + colors ?? + [GlowTheme.colors.brandPurple, GlowTheme.colors.brandCyan], + begin: Alignment.centerLeft, + end: Alignment.centerRight, + ), + boxShadow: [ + BoxShadow( + color: GlowTheme.colors.primary.withValues(alpha: 0.5), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + ), + ], + ); + } +} diff --git a/examples/glow/lib/widgets/images/generating_shader_image.dart b/examples/glow/lib/widgets/images/generating_shader_image.dart new file mode 100644 index 000000000..b24205523 --- /dev/null +++ b/examples/glow/lib/widgets/images/generating_shader_image.dart @@ -0,0 +1,238 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; + +/// A widget that displays an image with a custom shader effect overlay. +/// +/// The shader effect (e.g., ripple and frost) is active when [isGenerating] is true. +/// It uses a smooth ramp-up/down animation for the effect intensity. +class GeneratingShaderImage extends StatefulWidget { + /// The URL of the image to display. + final String? imageUrl; + + /// The image data to display (takes precedence over imageUrl). + final Uint8List? imageData; + + /// Whether the AI generation is in progress (enables the shader effect). + final bool isGenerating; + + /// How the image should be inscribed into the box. + final BoxFit fit; + + const GeneratingShaderImage({ + super.key, + this.imageUrl, + this.imageData, + required this.isGenerating, + this.fit = BoxFit.cover, + }); + + @override + State createState() => _GeneratingShaderImageState(); +} + +class _GeneratingShaderImageState extends State + with TickerProviderStateMixin { + ui.FragmentProgram? _program; + ui.Image? _image; + + // Controls the continuous ripple movement (phase) + late final Ticker _ticker; + double _time = 0.0; + + // Controls the fade in/out of the effect (intensity) + late final AnimationController _intensityController; + + // Image Loading Helpers + ImageStream? _imageStream; + ImageStreamListener? _imageStreamListener; + + @override + void initState() { + super.initState(); + _loadShader(); + _loadImage(widget.imageUrl); + + _intensityController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 800), // Smooth ramp up/down + ); + + _ticker = createTicker((elapsed) { + if (mounted) { + setState(() { + _time = elapsed.inMilliseconds / 1000.0; + }); + } + }); + + if (widget.isGenerating) { + _startEffect(); + } + } + + @override + void didUpdateWidget(covariant GeneratingShaderImage oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.isGenerating != oldWidget.isGenerating) { + if (widget.isGenerating) { + _startEffect(); + } else { + _stopEffect(); + } + } + + if (widget.imageUrl != oldWidget.imageUrl || + widget.imageData != oldWidget.imageData) { + _loadImage(widget.imageUrl); + } + } + + void _startEffect() { + if (!_ticker.isActive) { + _ticker.start(); + } + _intensityController.forward(); + } + + void _stopEffect() { + _intensityController.reverse().then((_) { + if (mounted && !widget.isGenerating) { + _ticker.stop(); + } + }); + } + + @override + void dispose() { + _ticker.dispose(); + _intensityController.dispose(); + _stopImageStream(); + super.dispose(); + } + + Future _loadShader() async { + try { + final program = await ui.FragmentProgram.fromAsset( + 'shaders/ripple_frost.frag', + ); + setState(() => _program = program); + } catch (e) { + debugPrint("Shader Error: $e"); + } + } + + void _loadImage(String? url) { + ImageProvider provider; + + if (widget.imageData != null) { + provider = MemoryImage(widget.imageData!); + } else if (url != null) { + provider = NetworkImage(url); + } else { + return; + } + + _stopImageStream(); + + _imageStream = provider.resolve(ImageConfiguration.empty); + _imageStreamListener = ImageStreamListener((ImageInfo info, _) { + if (mounted) { + setState(() { + _image = info.image; + }); + } + }); + _imageStream!.addListener(_imageStreamListener!); + } + + void _stopImageStream() { + if (_imageStream != null && _imageStreamListener != null) { + _imageStream!.removeListener(_imageStreamListener!); + } + } + + @override + Widget build(BuildContext context) { + if (_image == null) { + return const Center(child: CircularProgressIndicator()); + } + + return Stack( + fit: StackFit.expand, + children: [ + RawImage(image: _image!, fit: widget.fit), + if (_program != null) + AnimatedBuilder( + animation: _intensityController, + builder: (context, child) { + if (_intensityController.value == 0 && !widget.isGenerating) { + return const SizedBox.shrink(); + } + return Positioned.fill( + child: CustomPaint( + painter: _ShaderPainter( + shader: _program!.fragmentShader(), + image: _image!, + time: _time, + intensity: _intensityController.value, + ), + ), + ); + }, + ), + ], + ); + } +} + +class _ShaderPainter extends CustomPainter { + final ui.FragmentShader shader; + final ui.Image image; + final double time; + final double intensity; + + _ShaderPainter({ + required this.shader, + required this.image, + required this.time, + required this.intensity, + }); + + @override + void paint(Canvas canvas, Size size) { + shader.setFloat(0, size.width); + shader.setFloat(1, size.height); + shader.setFloat(2, time); + shader.setFloat(3, intensity); + + shader.setImageSampler(0, image); + + canvas.drawRect( + Rect.fromLTWH(0, 0, size.width, size.height), + Paint()..shader = shader, + ); + } + + @override + bool shouldRepaint(covariant _ShaderPainter oldDelegate) { + return oldDelegate.time != time || oldDelegate.intensity != intensity; + } +} diff --git a/examples/glow/lib/widgets/inputs/glow_inputs.dart b/examples/glow/lib/widgets/inputs/glow_inputs.dart new file mode 100644 index 000000000..b86b5ab9d --- /dev/null +++ b/examples/glow/lib/widgets/inputs/glow_inputs.dart @@ -0,0 +1,233 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; + +/// A themed text field for the Glow design system. +class GlowTextField extends StatelessWidget { + final TextEditingController? controller; + final String? hintText; + final bool isLong; + final ValueChanged? onChanged; + + const GlowTextField({ + super.key, + this.controller, + this.hintText, + this.isLong = false, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + maxLines: isLong ? 5 : 1, + style: TextStyle(color: GlowTheme.colors.onBackground), + onChanged: onChanged, + decoration: InputDecoration( + filled: true, + fillColor: GlowTheme.colors.surfaceContainerLow, + hintText: hintText ?? "Type your answer here...", + hintStyle: TextStyle(color: GlowTheme.colors.onBackgroundDisabled), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: GlowTheme.colors.primary), + ), + ), + ); + } +} + +/// A themed dropdown for the Glow design system. +class GlowDropdown extends StatelessWidget { + final String? value; + final String? hintText; + final List> items; + final ValueChanged? onChanged; + + const GlowDropdown({ + super.key, + this.value, + this.hintText, + required this.items, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + color: GlowTheme.colors.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: GlowTheme.colors.outline), + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + value: value, + hint: Text( + hintText ?? "Choose an option", + style: TextStyle(color: GlowTheme.colors.onBackgroundTertiary), + ), + isExpanded: true, + dropdownColor: GlowTheme.colors.surface, + style: TextStyle(color: GlowTheme.colors.onBackground, fontSize: 16), + items: items, + onChanged: onChanged, + ), + ), + ); + } +} + +/// A themed slider for the Glow design system. +class GlowSlider extends StatelessWidget { + final double value; + final double min; + final double max; + final int? divisions; + final ValueChanged? onChanged; + final String? label; + final String? leftLabel; + final String? rightLabel; + final IconData? leftIcon; + final IconData? rightIcon; + + const GlowSlider({ + super.key, + required this.value, + this.min = 0, + this.max = 100, + this.divisions, + this.onChanged, + this.label, + this.leftLabel, + this.rightLabel, + this.leftIcon, + this.rightIcon, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (label != null) + Text( + label!, + style: GlowTheme.textStyles.titleLarge.copyWith( + color: GlowTheme.colors.primary, + ), + ), + const SizedBox(height: 20), + Row( + children: [ + if (leftIcon != null) + Icon( + leftIcon, + color: GlowTheme.colors.onBackgroundSecondary, + size: 20, + ), + Expanded( + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + activeTrackColor: GlowTheme.colors.primary, + inactiveTrackColor: GlowTheme.colors.outlineStrong, + thumbColor: GlowTheme.colors.onBackground, + overlayColor: GlowTheme.colors.surfacePrimaryLow, + trackHeight: 4, + ), + child: Slider( + value: value, + min: min, + max: max, + divisions: divisions, + onChanged: onChanged, + ), + ), + ), + if (rightIcon != null) + Icon( + rightIcon, + color: GlowTheme.colors.onBackgroundSecondary, + size: 20, + ), + ], + ), + if (leftLabel != null || rightLabel != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (leftLabel != null) + Text( + leftLabel!, + style: TextStyle( + color: GlowTheme.colors.onBackgroundTertiary, + ), + ), + if (rightLabel != null) + Text( + rightLabel!, + style: TextStyle( + color: GlowTheme.colors.onBackgroundTertiary, + ), + ), + ], + ), + ), + ], + ); + } +} + +/// A themed toggle/switch for the Glow design system. +class GlowToggle extends StatelessWidget { + final bool value; + final String title; + final ValueChanged? onChanged; + + const GlowToggle({ + super.key, + required this.value, + required this.title, + this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: GlowTheme.colors.surfaceContainerLow, + borderRadius: BorderRadius.circular(12), + ), + child: SwitchListTile( + title: Text( + title, + style: TextStyle(color: GlowTheme.colors.onBackground), + ), + value: value, + activeThumbColor: GlowTheme.colors.primary, + contentPadding: const EdgeInsets.all(16), + onChanged: onChanged, + ), + ); + } +} diff --git a/examples/glow/lib/widgets/navigation/glass_top_bar.dart b/examples/glow/lib/widgets/navigation/glass_top_bar.dart new file mode 100644 index 000000000..c2686c6ce --- /dev/null +++ b/examples/glow/lib/widgets/navigation/glass_top_bar.dart @@ -0,0 +1,109 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:glow/theme.dart'; +import '../../l10n/app_localizations.dart'; + +/// A glass-morphic top navigation bar used across different screens. +class GlassTopBar extends StatelessWidget { + /// Whether to use tablet-optimized layout. + final bool isTablet; + + /// Callback for the menu/tune button (typically on mobile). + final VoidCallback? onMenuTap; + + /// Whether the associated settings menu is currently open. + final bool isMenuOpen; + + /// Title text to display in the center. + final String? title; + + const GlassTopBar({ + super.key, + required this.isTablet, + this.onMenuTap, + this.isMenuOpen = false, + this.title, + }); + + @override + Widget build(BuildContext context) { + return Container( + padding: EdgeInsets.only( + top: MediaQuery.of(context).padding.top + 10, + bottom: 10, + left: 20, + right: 20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + onTap: () { + if (context.canPop()) { + context.pop(); + } else { + context.go('/'); + } + }, + child: CircleAvatar( + backgroundColor: GlowTheme.colors.scrim, + child: Icon( + Icons.arrow_back, + color: GlowTheme.colors.onBackground, + ), + ), + ), + Text( + title ?? AppLocalizations.of(context)!.myGlowWallpaper, + style: GlowTheme.textStyles.bodyLarge.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + backgroundColor: GlowTheme.colors.scrim, + child: Icon( + Icons.ios_share, + color: GlowTheme.colors.onBackground, + ), + ), + if (!isTablet) ...[ + const SizedBox(width: 12), + GestureDetector( + onTap: onMenuTap, + child: CircleAvatar( + backgroundColor: isMenuOpen + ? GlowTheme.colors.primary + : GlowTheme.colors.surfacePrimaryLow, + child: Icon( + Icons.tune, + color: isMenuOpen + ? GlowTheme.colors.onSurface + : GlowTheme.colors.primary, + ), + ), + ), + ], + ], + ), + ], + ), + ); + } +} diff --git a/examples/glow/lib/widgets/panels/glass_properties_panel.dart b/examples/glow/lib/widgets/panels/glass_properties_panel.dart new file mode 100644 index 000000000..af63af6b3 --- /dev/null +++ b/examples/glow/lib/widgets/panels/glass_properties_panel.dart @@ -0,0 +1,218 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/theme.dart'; +import 'package:glow/widgets/buttons/glow_button.dart'; +import 'package:glow/widgets/cards/glow_cards.dart'; +import 'package:glow/widgets/inputs/glow_inputs.dart'; +import '../../l10n/app_localizations.dart'; + +/// A glass-morphic properties panel for editing wallpaper settings. +/// +/// Supported on both mobile (as a bottom sheet) and tablet (as a sidebar). +class GlassPropertiesPanel extends StatelessWidget { + final ScrollController? scrollController; + final VoidCallback? onClose; + final int selectedIndex; + final double atmosphere; + final ValueChanged onStyleSelected; + final ValueChanged onAtmosphereChanged; + final bool showDragHandle; + final VoidCallback? onRegenerate; + final bool isRegenerating; + final Widget? customContent; + final VoidCallback? onSave; + + const GlassPropertiesPanel({ + super.key, + this.scrollController, + this.onClose, + required this.selectedIndex, + required this.atmosphere, + required this.onStyleSelected, + required this.onAtmosphereChanged, + this.showDragHandle = true, + this.onRegenerate, + this.isRegenerating = false, + this.customContent, + this.onSave, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + if (showDragHandle) + Center( + child: Container( + margin: const EdgeInsets.only(top: 12, bottom: 8), + width: 40, + height: 4, + decoration: BoxDecoration( + color: GlowTheme.colors.surfaceContainerHigh, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + if (onClose != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: Icon( + Icons.close, + color: GlowTheme.colors.onBackgroundSecondary, + ), + onPressed: onClose, + ), + ], + ), + ), + Expanded( + child: SingleChildScrollView( + controller: scrollController, + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeader(context), + const SizedBox(height: 16), + if (customContent != null) + customContent! + else + _buildDefaultContent(context), + const SizedBox(height: 32), + _buildActions(context), + SizedBox(height: MediaQuery.of(context).padding.bottom + 24), + ], + ), + ), + ), + ], + ); + } + + Widget _buildHeader(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Text( + customContent != null ? l10n.adjustWallpaper : l10n.wallpaperStyle, + style: GlowTheme.textStyles.bodyMedium.copyWith( + fontWeight: FontWeight.w600, + ), + ); + } + + Widget _buildActions(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Row( + children: [ + Expanded( + child: GlowButton( + text: l10n.regenerate, + icon: Icons.refresh, + isPrimary: true, + onTap: onRegenerate, + isLoading: isRegenerating, + ), + ), + const SizedBox(width: 16), + Expanded( + child: GlowButton( + text: l10n.save, + icon: Icons.check, + isPrimary: false, + onTap: onSave, + ), + ), + ], + ); + } + + Widget _buildDefaultContent(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Style Thumbnails + SizedBox( + height: 110, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + GlowStyleCard( + label: l10n.styleCosmic, + isSelected: selectedIndex == 0, + onTap: () => onStyleSelected(0), + color: GlowTheme.colors.secondary, + ), + GlowStyleCard( + label: l10n.styleAbstract, + isSelected: selectedIndex == 1, + onTap: () => onStyleSelected(1), + color: GlowTheme.colors.secondary, + ), + GlowStyleCard( + label: l10n.styleNature, + isSelected: selectedIndex == 2, + onTap: () => onStyleSelected(2), + color: GlowTheme.colors.tertiary, + ), + GlowStyleCard( + label: l10n.styleCrystal, + isSelected: selectedIndex == 3, + onTap: () => onStyleSelected(3), + color: GlowTheme.colors.secondary, + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Atmosphere Slider + GlowSlider( + value: atmosphere, + onChanged: onAtmosphereChanged, + leftIcon: Icons.wb_sunny_outlined, + rightIcon: Icons.nightlight_round, + leftLabel: l10n.atmosphereWarmCalm, + rightLabel: l10n.atmosphereCoolEnergetic, + label: l10n.atmosphereLabel, + ), + + const SizedBox(height: 24), + + // Key Elements (Toggles) + Text( + l10n.keyElementLabel, + style: GlowTheme.textStyles.bodyMedium.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: const [ + GlowElementToggle(icon: Icons.local_florist, isActive: true), + GlowElementToggle(icon: Icons.grain, isActive: false), + GlowElementToggle(icon: Icons.water_drop, isActive: true), + ], + ), + ], + ); + } +} diff --git a/examples/glow/lib/widgets/quiz/quiz_choice_inputs.dart b/examples/glow/lib/widgets/quiz/quiz_choice_inputs.dart new file mode 100644 index 000000000..8efb3a995 --- /dev/null +++ b/examples/glow/lib/widgets/quiz/quiz_choice_inputs.dart @@ -0,0 +1,129 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import 'package:flutter/material.dart'; +import 'package:glow/models/quiz_question.dart'; +import 'package:glow/widgets/cards/glow_cards.dart'; + +// --- Reusable Image Grid --- +class GlowImageGrid extends StatelessWidget { + final List options; + final String? selectedId; + final void Function(String id) onOptionSelected; + + const GlowImageGrid({ + super.key, + required this.options, + this.selectedId, + required this.onOptionSelected, + }); + + @override + Widget build(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: options.length, + itemBuilder: (context, index) { + final option = options[index]; + // We will lift the selection state up. + // For the static quiz, the parent handles checking selection. + // For dynamic UI, we'll need a way to checking against DataModel. + // The most flexible way is to ask the parent "isSelected". + return GlowQuizImageOption( + label: option.label, + imageSeed: option.imageSeed, + isSelected: + selectedId == + option.id, // Simplification for now, might need list + onTap: () => onOptionSelected(option.id), + ); + }, + ); + } +} + +// Redefining for flexibility: +class GlowImageGridSelector extends StatelessWidget { + final List options; + final bool Function(String id) isSelected; + final void Function(String id) onToggle; + + const GlowImageGridSelector({ + super.key, + required this.options, + required this.isSelected, + required this.onToggle, + }); + + @override + Widget build(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1.2, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: options.length, + itemBuilder: (context, index) { + final option = options[index]; + return GlowQuizImageOption( + label: option.label, + imageSeed: option.imageSeed, + isSelected: isSelected(option.id), + onTap: () => onToggle(option.id), + ); + }, + ); + } +} + +// --- Reusable Text Choices --- +class GlowTextChoiceSelector extends StatelessWidget { + final List options; + final bool Function(String id) isSelected; + final void Function(String id) onToggle; + + const GlowTextChoiceSelector({ + super.key, + required this.options, + required this.isSelected, + required this.onToggle, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: options.map((option) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: GlowQuizOptionCard( + label: option.label, + isSelected: isSelected(option.id), + onTap: () => onToggle(option.id), + ), + ); + }).toList(), + ); + } +} diff --git a/examples/glow/linux/.gitignore b/examples/glow/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/examples/glow/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/examples/glow/linux/CMakeLists.txt b/examples/glow/linux/CMakeLists.txt new file mode 100644 index 000000000..3f37f7d9d --- /dev/null +++ b/examples/glow/linux/CMakeLists.txt @@ -0,0 +1,142 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "glow") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.example.glow") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/examples/glow/linux/flutter/CMakeLists.txt b/examples/glow/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..7a20eebe8 --- /dev/null +++ b/examples/glow/linux/flutter/CMakeLists.txt @@ -0,0 +1,102 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/examples/glow/linux/flutter/generated_plugin_registrant.cc b/examples/glow/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..464061c8f --- /dev/null +++ b/examples/glow/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_saver_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); + file_saver_plugin_register_with_registrar(file_saver_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/examples/glow/linux/flutter/generated_plugin_registrant.h b/examples/glow/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..e0f0a47bc --- /dev/null +++ b/examples/glow/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/examples/glow/linux/flutter/generated_plugins.cmake b/examples/glow/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000..4c6b412f6 --- /dev/null +++ b/examples/glow/linux/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_saver + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/examples/glow/linux/runner/CMakeLists.txt b/examples/glow/linux/runner/CMakeLists.txt new file mode 100644 index 000000000..59afeb0e7 --- /dev/null +++ b/examples/glow/linux/runner/CMakeLists.txt @@ -0,0 +1,40 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/examples/glow/linux/runner/main.cc b/examples/glow/linux/runner/main.cc new file mode 100644 index 000000000..09df67b65 --- /dev/null +++ b/examples/glow/linux/runner/main.cc @@ -0,0 +1,20 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/examples/glow/linux/runner/my_application.cc b/examples/glow/linux/runner/my_application.cc new file mode 100644 index 000000000..351adbaba --- /dev/null +++ b/examples/glow/linux/runner/my_application.cc @@ -0,0 +1,162 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "glow"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "glow"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/examples/glow/linux/runner/my_application.h b/examples/glow/linux/runner/my_application.h new file mode 100644 index 000000000..85d75060c --- /dev/null +++ b/examples/glow/linux/runner/my_application.h @@ -0,0 +1,37 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/examples/glow/macos/.gitignore b/examples/glow/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/examples/glow/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/examples/glow/macos/Flutter/Flutter-Debug.xcconfig b/examples/glow/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/examples/glow/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/glow/macos/Flutter/Flutter-Release.xcconfig b/examples/glow/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/examples/glow/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/examples/glow/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/glow/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 000000000..48b23d351 --- /dev/null +++ b/examples/glow/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import file_saver +import shared_preferences_foundation +import url_launcher_macos + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) +} diff --git a/examples/glow/macos/Podfile b/examples/glow/macos/Podfile new file mode 100644 index 000000000..ff5ddb3b8 --- /dev/null +++ b/examples/glow/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.15' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/examples/glow/macos/Podfile.lock b/examples/glow/macos/Podfile.lock new file mode 100644 index 000000000..1a15fab04 --- /dev/null +++ b/examples/glow/macos/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - file_saver (0.0.1): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - objective_c (0.0.1): + - FlutterMacOS + - shared_preferences_foundation (0.0.1): + - Flutter + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + +DEPENDENCIES: + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - objective_c (from `Flutter/ephemeral/.symlinks/plugins/objective_c/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + +EXTERNAL SOURCES: + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos + FlutterMacOS: + :path: Flutter/ephemeral + objective_c: + :path: Flutter/ephemeral/.symlinks/plugins/objective_c/macos + shared_preferences_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + +SPEC CHECKSUMS: + file_saver: e35bd97de451dde55ff8c38862ed7ad0f3418d0f + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba + shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb + url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/examples/glow/macos/Runner.xcodeproj/project.pbxproj b/examples/glow/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..ce9a37822 --- /dev/null +++ b/examples/glow/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,801 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 065CD099748507F1B05F16D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6884DC6CCF59CAB256C29A1 /* Pods_Runner.framework */; }; + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 8980766310D7794BF6ED3B4C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E3F2AEC4BBA76E325AFC92D6 /* Pods_RunnerTests.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 07A790F89211BCD8DBC13E65 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 2FF7EB400786FC0A1BFB08D6 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* glow.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = glow.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 3C69387F3314667AE598C3A3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 695A8FB3AD2B920F67C9F98E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 6AED2D40419BDA9098DBA829 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + A5D20808EABA4A798BF00360 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + C6884DC6CCF59CAB256C29A1 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E3F2AEC4BBA76E325AFC92D6 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8980766310D7794BF6ED3B4C /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 065CD099748507F1B05F16D1 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + 7CE1B2D77E97534C316C743C /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* glow.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + 7CE1B2D77E97534C316C743C /* Pods */ = { + isa = PBXGroup; + children = ( + A5D20808EABA4A798BF00360 /* Pods-Runner.debug.xcconfig */, + 07A790F89211BCD8DBC13E65 /* Pods-Runner.release.xcconfig */, + 695A8FB3AD2B920F67C9F98E /* Pods-Runner.profile.xcconfig */, + 6AED2D40419BDA9098DBA829 /* Pods-RunnerTests.debug.xcconfig */, + 2FF7EB400786FC0A1BFB08D6 /* Pods-RunnerTests.release.xcconfig */, + 3C69387F3314667AE598C3A3 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C6884DC6CCF59CAB256C29A1 /* Pods_Runner.framework */, + E3F2AEC4BBA76E325AFC92D6 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 8DEEB0B2EA471386F6EAF305 /* [CP] Check Pods Manifest.lock */, + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B62F41843AFB1208063CCD4C /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + F6FB031FA6AF53D338DCF2E9 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* glow.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 8DEEB0B2EA471386F6EAF305 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + B62F41843AFB1208063CCD4C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F6FB031FA6AF53D338DCF2E9 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6AED2D40419BDA9098DBA829 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/glow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/glow"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2FF7EB400786FC0A1BFB08D6 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/glow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/glow"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 3C69387F3314667AE598C3A3 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.glow.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/glow.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/glow"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/examples/glow/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/glow/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/glow/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/glow/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/examples/glow/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..033295eec --- /dev/null +++ b/examples/glow/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/glow/macos/Runner.xcworkspace/contents.xcworkspacedata b/examples/glow/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/examples/glow/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/examples/glow/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/glow/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/glow/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples/glow/macos/Runner/AppDelegate.swift b/examples/glow/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..cf1e917ca --- /dev/null +++ b/examples/glow/macos/Runner/AppDelegate.swift @@ -0,0 +1,27 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..82b6f9d9a Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 000000000..13b35eba5 Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 000000000..0a3f5fa40 Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 000000000..bdb57226d Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 000000000..f083318e0 Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 000000000..326c0e72c Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 000000000..2f1632cfd Binary files /dev/null and b/examples/glow/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/examples/glow/macos/Runner/Base.lproj/MainMenu.xib b/examples/glow/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/examples/glow/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/glow/macos/Runner/Configs/AppInfo.xcconfig b/examples/glow/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..45cbd519d --- /dev/null +++ b/examples/glow/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = glow + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.glow + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/examples/glow/macos/Runner/Configs/Debug.xcconfig b/examples/glow/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/examples/glow/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/glow/macos/Runner/Configs/Release.xcconfig b/examples/glow/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/examples/glow/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/examples/glow/macos/Runner/Configs/Warnings.xcconfig b/examples/glow/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/examples/glow/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/examples/glow/macos/Runner/DebugProfile.entitlements b/examples/glow/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..3ba6c1266 --- /dev/null +++ b/examples/glow/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/examples/glow/macos/Runner/Info.plist b/examples/glow/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/examples/glow/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/examples/glow/macos/Runner/MainFlutterWindow.swift b/examples/glow/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..e2b7c5a60 --- /dev/null +++ b/examples/glow/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,29 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/examples/glow/macos/Runner/Release.entitlements b/examples/glow/macos/Runner/Release.entitlements new file mode 100644 index 000000000..ee95ab7e5 --- /dev/null +++ b/examples/glow/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/examples/glow/macos/RunnerTests/RunnerTests.swift b/examples/glow/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000..4d74dcb5b --- /dev/null +++ b/examples/glow/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,26 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/examples/glow/pubspec.yaml b/examples/glow/pubspec.yaml new file mode 100644 index 000000000..2977ae059 --- /dev/null +++ b/examples/glow/pubspec.yaml @@ -0,0 +1,50 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +name: glow +description: "A new Flutter project." +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.11.0-144.0.dev + +dependencies: + file_saver: ^0.3.1 + flutter: + sdk: flutter + flutter_localizations: + sdk: flutter + intl: any + genui: ^0.6.0 + genui_google_generative_ai: ^0.6.0 + go_router: ^17.0.0 + google_cloud_ai_generativelanguage_v1beta: ^0.3.0 + json_schema_builder: ^0.1.3 + logging: ^1.3.0 + shared_preferences: ^2.5.3 + url_launcher: ^6.3.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + generate: true + uses-material-design: true + shaders: + - shaders/orb_shader.frag + - shaders/mesh_gradient.frag + - shaders/ripple_frost.frag diff --git a/examples/glow/research/genui.md b/examples/glow/research/genui.md new file mode 100644 index 000000000..bc96319c7 --- /dev/null +++ b/examples/glow/research/genui.md @@ -0,0 +1,500 @@ +# genui + +A Flutter package for building dynamic, conversational user interfaces powered by generative AI models. + +`genui` allows you to create applications where the UI is not static or predefined, but is instead constructed by an AI in real-time based on a conversation with the user. This enables highly flexible, context-aware, and interactive user experiences. + +This package provides the core functionality for GenUI. For concrete implementations, see the `genui_firebase_ai` package (for Firebase AI) or the `genui_a2ui` package (for a generic A2UI server). + +## Features + +- **Dynamic UI Generation**: Render Flutter UIs from structured data returned by a generative AI. +- **Simplified Conversation Flow**: A high-level `GenUiConversation` facade manages the interaction loop with the AI. +- **Customizable Widget Catalog**: Define a "vocabulary" of Flutter widgets that the AI can use to build the interface. +- **Extensible Content Generator**: Abstract interface for connecting to different AI model backends. +- **Event Handling**: Capture user interactions (button clicks, text input), update a client-side data model, and send the state back to the AI as context for the next turn in the conversation. +- **Reactive UI**: Widgets automatically rebuild when the data they are bound to changes in the data model. + +## Core Concepts + +The package is built around the following main components: + +1. **`GenUiConversation`**: The primary facade and entry point for the package. It encapsulates the `A2uiMessageProcessor` and `ContentGenerator`, manages the conversation history, and orchestrates the entire generative UI process. + +2. **`Catalog`**: A collection of `CatalogItem`s that defines the set of widgets the AI is allowed to use. Each `CatalogItem` specifies a widget's name (for the AI to reference), a data schema for its properties, and a builder function to render the Flutter widget. + +3. **`DataModel`**: A centralized, observable store for all dynamic UI state. Widgets are "bound" to data in this model. When data changes, only the widgets that depend on that specific piece of data are rebuilt. + +4. **`ContentGenerator`**: An interface for communicating with a generative AI model. This interface uses streams to send `A2uiMessage` commands, text responses, and errors back to the `GenUiConversation`. + +5. **`A2uiMessage`**: A message sent from the AI (via the `ContentGenerator`) to the UI, instructing it to perform actions like `beginRendering`, `surfaceUpdate`, `dataModelUpdate`, or `deleteSurface`. + +## How It Works + +The `GenUiConversation` manages the interaction cycle: + +1. **User Input**: The user provides a prompt (e.g., through a text field). The app calls `genUiConversation.sendRequest()`. +2. **AI Invocation**: The `GenUiConversation` adds the user's message to its internal conversation history and calls `contentGenerator.sendRequest()`. +3. **AI Response**: The `ContentGenerator` interacts with the AI model. The AI, guided by the widget schemas, sends back responses. +4. **Stream Handling**: The `ContentGenerator` emits `A2uiMessage`s, text responses, or errors on its streams. +5. **UI State Update**: `GenUiConversation` listens to these streams. `A2uiMessage`s are passed to `A2uiMessageProcessor.handleMessage()`, which updates the UI state and `DataModel`. +6. **UI Rendering**: The `A2uiMessageProcessor` broadcasts an update, and any `GenUiSurface` widgets listening for that surface ID will rebuild. Widgets are bound to the `DataModel`, so they update automatically when their data changes. +7. **Callbacks**: Text responses and errors trigger the `onTextResponse` and `onError` callbacks on `GenUiConversation`. +8. **User Interaction**: The user interacts with the newly generated UI (e.g., by typing in a text field). This interaction directly updates the `DataModel`. If the interaction is an action (like a button click), the `GenUiSurface` captures the event and forwards it to the `GenUiConversation`'s `A2uiMessageProcessor`, which automatically creates a new `UserMessage` containing the current state of the data model and restarts the cycle. + +```mermaid +graph TD + subgraph "User" + UserInput("Provide Prompt") + UserInteraction("Interact with UI") + end + + subgraph "GenUI Framework" + GenUiConversation("GenUiConversation") + ContentGenerator("ContentGenerator") + A2uiMessageProcessor("A2uiMessageProcessor") + GenUiSurface("GenUiSurface") + end + + UserInput -- "calls sendRequest()" --> GenUiConversation; + GenUiConversation -- "sends prompt" --> ContentGenerator; + ContentGenerator -- "returns A2UI messages" --> GenUiConversation; + GenUiConversation -- "handles messages" --> A2uiMessageProcessor; + A2uiMessageProcessor -- "notifies of updates" --> GenUiSurface; + GenUiSurface -- "renders UI" --> UserInteraction; + UserInteraction -- "creates event" --> GenUiSurface; + GenUiSurface -- "sends event to host" --> A2uiMessageProcessor; + A2uiMessageProcessor -- "sends user input to" --> GenUiConversation; +``` + +See [DESIGN.md](./DESIGN.md) for more detailed information about the design. + +## Getting Started with `genui` + +This guidance explains how to quickly get started with the +[`genui`](https://pub.dev/packages/genui) package. + +### 1. Add `genui` to your app + +Use the following instructions to add `genui` to your Flutter app. The +code examples show how to perform the instructions on a brand new app created by +running `flutter create`. + +### 2. Configure your agent provider + +`genui` can connect to a variety of agent providers. Choose the section +below for your preferred provider. + +#### Configure Firebase AI Logic + +To use the built-in `FirebaseAiContentGenerator` to connect to Gemini via Firebase AI +Logic, follow these instructions: + +1. [Create a new Firebase project](https://support.google.com/appsheet/answer/10104995) + using the Firebase Console. +2. [Enable the Gemini API](https://firebase.google.com/docs/gemini-in-firebase/set-up-gemini) + for that project. +3. Follow the first three steps in + [Firebase's Flutter Setup guide](https://firebase.google.com/docs/flutter/setup) + to add Firebase to your app. +4. In `pubspec.yaml`, add `genui` and `genui_firebase_ai` to the + `dependencies` section. + + ```yaml + dependencies: + # ... + genui: 0.5.0 + genui_firebase_ai: 0.5.0 + ``` + +5. In your app's `main` method, ensure that the widget bindings are initialized, + and then initialize Firebase. + + ```dart + void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); + runApp(const MyApp()); + } + ``` + +#### Configure another agent provider + +To use `genui` with another agent provider, you need to follow that +provider's instructions to configure your app, and then create your own subclass +of `ContentGenerator` to connect to that provider. Use `FirebaseAiContentGenerator` or +`A2uiContentGenerator` (from the `genui_a2ui` package) as examples +of how to do so. + +### 3. Create the connection to an agent + +If you build your Flutter project for iOS or macOS, add this key to your +`{ios,macos}/Runner/*.entitlements` file(s) to enable outbound network +requests: + +```xml + +... +com.apple.security.network.client + + +``` + +Next, use the following instructions to connect your app to your chosen agent +provider. + +1. Create a `A2uiMessageProcessor`, and provide it with the catalog of widgets you want + to make available to the agent. +2. Create a `ContentGenerator`, and provide it with a system instruction and a set of + tools (functions you want the agent to be able to invoke). You should always + include those provided by `A2uiMessageProcessor`, but feel free to include others. +3. Create a `GenUiConversation` using the instances of `ContentGenerator` and `A2uiMessageProcessor`. Your + app will primarily interact with this object to get things done. + + For example: + + ```dart + class _MyHomePageState extends State { + late final A2uiMessageProcessor _a2uiMessageProcessor; + late final GenUiConversation _genUiConversation; + + @override + void initState() { + super.initState(); + + // Create a A2uiMessageProcessor with a widget catalog. + // The CoreCatalogItems contain basic widgets for text, markdown, and images. + _a2uiMessageProcessor = A2uiMessageProcessor(catalogs: [CoreCatalogItems.asCatalog()]); + + // Create a ContentGenerator to communicate with the LLM. + // Provide system instructions and the tools from the A2uiMessageProcessor. + final contentGenerator = FirebaseAiContentGenerator( + catalog: CoreCatalogItems.asCatalog(), + systemInstruction: ''' + You are an expert in creating funny riddles. Every time I give you a word, + you should generate UI that displays one new riddle related to that word. + Each riddle should have both a question and an answer. + ''', + ); + + // Create the GenUiConversation to orchestrate everything. + _genUiConversation = GenUiConversation( + a2uiMessageProcessor: _a2uiMessageProcessor, + contentGenerator: contentGenerator, + onSurfaceAdded: _onSurfaceAdded, // Added in the next step. + onSurfaceDeleted: _onSurfaceDeleted, // Added in the next step. + ); + } + + @override + void dispose() { + _textController.dispose(); + _genUiConversation.dispose(); + + super.dispose(); + } + } + ``` + +### 4. Send messages and display the agent's responses + +Send a message to the agent using the `sendRequest` method in the `GenUiConversation` +class. + +To receive and display generated UI: + +1. Use `GenUiConversation`'s callbacks to track the addition and removal of UI surfaces as + they are generated. These events include a "surface ID" for each surface. +2. Build a `GenUiSurface` widget for each active surface using the surface IDs + received in the previous step. + + For example: + + ```dart + class _MyHomePageState extends State { + + // ... + + final _textController = TextEditingController(); + final _surfaceIds = []; + + // Send a message containing the user's text to the agent. + void _sendMessage(String text) { + if (text.trim().isEmpty) return; + _genUiConversation.sendRequest(UserMessage.text(text)); + } + + // A callback invoked by the [GenUiConversation] when a new UI surface is generated. + // Here, the ID is stored so the build method can create a GenUiSurface to + // display it. + void _onSurfaceAdded(SurfaceAdded update) { + setState(() { + _surfaceIds.add(update.surfaceId); + }); + } + + // A callback invoked by GenUiConversation when a UI surface is removed. + void _onSurfaceDeleted(SurfaceRemoved update) { + setState(() { + _surfaceIds.remove(update.surfaceId); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: Text(widget.title), + ), + body: Column( + children: [ + Expanded( + child: ListView.builder( + itemCount: _surfaceIds.length, + itemBuilder: (context, index) { + // For each surface, create a GenUiSurface to display it. + final id = _surfaceIds[index]; + return GenUiSurface(host: _genUiConversation.host, surfaceId: id); + }, + ), + ), + SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _textController, + decoration: const InputDecoration( + hintText: 'Enter a message', + ), + ), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () { + // Send the user's text to the agent. + _sendMessage(_textController.text); + _textController.clear(); + }, + child: const Text('Send'), + ), + ], + ), + ), + ), + ], + ), + ); + } + } + ``` + +### 5. [Optional] Add your own widgets to the catalog + +In addition to using the catalog of widgets in `CoreCatalogItems`, you can +create custom widgets for the agent to generate. Use the following +instructions. + +#### Import `json_schema_builder` + +Add the `json_schema_builder` package as a dependency in `pubspec.yaml`. Use the +same commit reference as the one for `genui`. + +```yaml +dependencies: + # ... + json_schema_builder: + git: + url: https://github.com/flutter/genui.git + path: packages/json_schema_builder + +``` + +#### Create the new widget's schema + +Each catalog item needs a schema that defines the data required to populate it. +Using the `json_schema_builder` package, define one for the new widget. + +```dart +import 'package:json_schema_builder/json_schema_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:genui/genui.dart'; + +final _schema = S.object( + properties: { + 'question': S.string(description: 'The question part of a riddle.'), + 'answer': S.string(description: 'The answer part of a riddle.'), + }, + required: ['question', 'answer'], +); +``` + +#### Create a `CatalogItem` + +Each `CatalogItem` represents a type of widget that the agent is allowed to +generate. To do that, combines a name, a schema, and a builder function that +produces the widgets that compose the generated UI. + +```dart +final riddleCard = CatalogItem( + name: 'RiddleCard', + dataSchema: _schema, + widgetBuilder: (context) { + final questionNotifier = context.dataContext.subscribeToString( + context.data['question'] as Map?, + ); + final answerNotifier = context.dataContext.subscribeToString( + context.data['answer'] as Map?, + ); + + return ValueListenableBuilder( + valueListenable: questionNotifier, + builder: (context, question, _) { + return ValueListenableBuilder( + valueListenable: answerNotifier, + builder: (context, answer, _) { + return Container( + constraints: const BoxConstraints(maxWidth: 400), + decoration: BoxDecoration(border: Border.all()), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + question ?? '', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 8.0), + Text( + answer ?? '', + style: Theme.of(context).textTheme.headlineSmall, + ), + ], + ), + ); + }, + ); + }, + ); + }, +); +``` + +#### Add the `CatalogItem` to the catalog + +Include your catalog items when instantiating `A2uiMessageProcessor`. + +```dart +_a2uiMessageProcessor = A2uiMessageProcessor( + catalogs: [CoreCatalogItems.asCatalog().copyWith([riddleCard])], +); +``` + +#### Update the system instruction to use the new widget + +In order to make sure the agent knows to use your new widget, use the system +instruction to explicitly tell it how and when to do so. Provide the name from +the CatalogItem when you do. + +```dart +final contentGenerator = FirebaseAiContentGenerator( + systemInstruction: ''' + You are an expert in creating funny riddles. Every time I give you a word, + you should generate a RiddleCard that displays one new riddle related to that word. + Each riddle should have both a question and an answer. + ''', + tools: _a2uiMessageProcessor.getTools(), +); +``` + +### Data Model and Data Binding + +A core concept in `genui` is the **`DataModel`**, a centralized, observable store for all dynamic UI state. Instead of widgets managing their own state, their state is stored in the `DataModel`. + +Widgets are "bound" to data in this model. When data in the model changes, only the widgets that depend on that specific piece of data are rebuilt. This is achieved through a `DataContext` object that is passed to each widget's builder function. + +#### Binding to the Data Model + +To bind a widget's property to the data model, you use a special JSON object in the data sent from the AI. This object can contain either a `literalString` (for static values) or a `path` (to bind to a value in the data model). + +For example, to display a user's name in a `Text` widget, the AI would generate: + +```json +{ + "Text": { + "text": { + "literalString": "Welcome to GenUI" + }, + "hint": "h1" + } +} +``` + +#### Image + +```json +{ + "Image": { + "url": { + "literalString": "https://example.com/image.png" + }, + "hint": "mediumFeature" + } +} +``` + +#### Updating the Data Model + +Input widgets, like `TextField`, update the `DataModel` directly. When the user types in a text field that is bound to `/user/name`, the `DataModel` is updated, and any other widgets bound to that same path will automatically rebuild to show the new value. + +This reactive data flow simplifies state management and creates a powerful, high-bandwidth interaction loop between the user, the UI, and the AI. + +### Next steps + +Check out the [examples](../../examples) included in this repo! The +[travel app](../../examples/travel_app) shows how to define your own widget +`Catalog` that the agent can use to generate domain-specific UI. + +If something is unclear or missing, please +[create an issue](https://github.com/flutter/genui/issues/new/choose). + +### System instructions + +The `genui` package gives the LLM a set of tools it can use to generate +UI. To get the LLM to use these tools, the `systemInstruction` provided to +`ContentGenerator` must explicitly tell it to do so. This is why the previous example +includes a system instruction for the agent with the line "Every time I give +you a word, you should generate UI that displays one new riddle...". + +### Troubleshooting / FAQ + +#### How can I configure logging? + +To observe communication between your app and the agent, enable logging in your +`main` method. + +```dart +import 'package:logging/logging.dart'; +import 'package:genui/genui.dart'; + +final logger = configureGenUiLogging(level: Level.ALL); + +void main() async { + logger.onRecord.listen((record) { + debugPrint('${record.loggerName}: ${record.message}'); + }); + + // Additional initialization of bindings and Firebase. +} +``` + +#### I'm getting errors about my minimum macOS/iOS version. + +Firebase has a +[minimum version requirement](https://firebase.google.com/support/release-notes/ios) +for Apple's platforms, which might be higher than Flutter's default. Check your +`Podfile` (for iOS) and `CMakeLists.txt` (for macOS) to ensure you're targeting +a version that meets or exceeds Firebase's requirements. \ No newline at end of file diff --git a/examples/glow/research/genui_generative_ui.md b/examples/glow/research/genui_generative_ui.md new file mode 100644 index 000000000..fd96df56d --- /dev/null +++ b/examples/glow/research/genui_generative_ui.md @@ -0,0 +1,68 @@ +# genui_google_generative_ai + +This package provides the integration between `genui` and the Google Cloud Generative Language API. It allows you to use the power of Google's Gemini models to generate dynamic user interfaces in your Flutter applications. + +## Features + +- **GoogleGenerativeAiContentGenerator:** An implementation of `ContentGenerator` that connects to the Google Cloud Generative Language API. +- **GoogleContentConverter:** Converts between the generic `ChatMessage` and the `google_cloud_ai_generativelanguage_v1beta` specific `Content` classes. +- **GoogleSchemaAdapter:** Adapts schemas from `json_schema_builder` to the Google Cloud API format. + +## Getting Started + +To use this package, you will need a Gemini API key. If you don't already have one, you can get it for free in [Google AI Studio](https://aistudio.google.com/apikey). + +### Installation + +Add this package to your `pubspec.yaml`: "genui_google_generative_ai" + +### Usage + +Create an instance of `GoogleGenerativeAiContentGenerator` and pass it to your `GenUiConversation`: + +```dart +import 'package:genui/genui.dart'; +import 'package:genui_google_generative_ai/genui_google_generative_ai.dart'; + +final catalog = Catalog(components: [...]); + +final contentGenerator = GoogleGenerativeAiContentGenerator( + catalog: catalog, + systemInstruction: 'You are a helpful assistant.', + modelName: 'models/gemini-2.5-flash', + apiKey: 'YOUR_API_KEY', // Or set GEMINI_API_KEY environment variable +); + +final conversation = GenUiConversation( + contentGenerator: contentGenerator, +); +``` + +### API Key Configuration + +The API key can be provided in two ways: + +1. **Environment Variable** (recommended): Set the `GEMINI_API_KEY` environment variable +2. **Constructor Parameter**: Pass the API key directly to the constructor + +If neither is provided, the package will attempt to use the default environment variable. + +## Differences from Firebase AI + +This package uses the Google Cloud Generative Language API instead of Firebase AI Logic. + +This API is meant for quick explorations and local testing or prototyping, +not for production or deployment. + +**Flutter apps built for production should use Firebase AI**: For mobile and +web applications, consider using `genui_firebase_ai` instead, which provides client-side access + +## Documentation + +For more information on the Google Cloud Generative Language API, see: +- [API Documentation](https://pub.dev/documentation/google_cloud_ai_generativelanguage_v1beta/latest/) +- [Gemini API Guide](https://ai.google.dev/gemini-api/docs) + +## License + +This package is licensed under the BSD-3-Clause license. See [LICENSE](LICENSE) for details. \ No newline at end of file diff --git a/examples/glow/shaders/mesh_gradient.frag b/examples/glow/shaders/mesh_gradient.frag new file mode 100644 index 000000000..914079b4a --- /dev/null +++ b/examples/glow/shaders/mesh_gradient.frag @@ -0,0 +1,48 @@ +#include + +uniform vec2 uSize; +uniform float uTime; +uniform vec3 uColor1; +uniform vec3 uColor2; +uniform vec3 uColor3; +uniform vec3 uColor4; + +out vec4 fragColor; + +void main() { + vec2 pos = FlutterFragCoord().xy; + + // Normalize coordinates to 0.0 - 1.0 range + vec2 uv = pos / uSize; + + // 2. Animate the "mesh points" using Sine waves + // We create 4 moving points (blobs) that drift around the screen + vec2 p1 = vec2(0.5 + 0.4 * sin(uTime * 0.5), 0.5 + 0.4 * cos(uTime * 0.3)); + vec2 p2 = vec2(0.5 + 0.4 * sin(uTime * 0.8 + 2.0), 0.5 + 0.4 * cos(uTime * 0.6 + 1.0)); + vec2 p3 = vec2(0.5 + 0.4 * sin(uTime * 0.4 + 4.0), 0.5 + 0.4 * cos(uTime * 0.5 + 3.0)); + vec2 p4 = vec2(0.5 + 0.4 * sin(uTime * 0.7 + 5.0), 0.5 + 0.4 * cos(uTime * 0.4 + 2.0)); + + // 3. Calculate distance from current pixel to each moving point + // We increase the 'blob' size by dividing by distance (soft glow effect) + float d1 = distance(uv, p1); + float d2 = distance(uv, p2); + float d3 = distance(uv, p3); + float d4 = distance(uv, p4); + + // 4. Create weighted blend based on proximity + // The closer a pixel is to a point, the more of that color it gets + float w1 = 1.0 / (d1 * d1 + 0.1); + float w2 = 1.0 / (d2 * d2 + 0.1); + float w3 = 1.0 / (d3 * d3 + 0.1); + float w4 = 1.0 / (d4 * d4 + 0.1); + + // Normalize weights so they sum to 1.0 + float total = w1 + w2 + w3 + w4; + vec3 color = (uColor1 * w1 + uColor2 * w2 + uColor3 * w3 + uColor4 * w4) / total; + + // Add a subtle noise/dither to prevent color banding (optional) + // float noise = fract(sin(dot(uv, vec2(12.9898, 78.233))) * 43758.5453); + // color += (noise - 0.5) * 0.02; + + fragColor = vec4(color, 1.0); +} diff --git a/examples/glow/shaders/orb_shader.frag b/examples/glow/shaders/orb_shader.frag new file mode 100644 index 000000000..6eb77c235 --- /dev/null +++ b/examples/glow/shaders/orb_shader.frag @@ -0,0 +1,91 @@ +#include + +uniform vec2 uSize; +uniform float uTime; + +out vec4 fragColor; + +// --- Noise Functions --- +vec3 hash33(vec3 p3) { + p3 = fract(p3 * vec3(.1031, .1030, .0973)); + p3 += dot(p3, p3.yxz+33.33); + return fract((p3.xxy + p3.yxx)*p3.zyx); +} + +float snoise3(vec3 p) { + const float K1 = 0.333333333; + const float K2 = 0.166666667; + vec3 i = floor(p + (p.x + p.y + p.z) * K1); + vec3 d0 = p - (i - (i.x + i.y + i.z) * K2); + vec3 e = step(vec3(0.0), d0 - d0.yzx); + vec3 i1 = e * (1.0 - e.zxy); + vec3 i2 = 1.0 - e.zxy * (1.0 - e); + vec3 d1 = d0 - (i1 - 1.0 * K2); + vec3 d2 = d0 - (i2 - 2.0 * K2); + vec3 d3 = d0 - (1.0 - 3.0 * K2); + vec4 h = max(0.6 - vec4(dot(d0, d0), dot(d1, d1), dot(d2, d2), dot(d3, d3)), 0.0); + vec4 n = h * h * h * h * vec4(dot(d0, hash33(i)), dot(d1, hash33(i + i1)), dot(d2, hash33(i + i2)), dot(d3, hash33(i + 1.0))); + return dot(vec4(31.316), n); +} + +void main() { + vec2 pos = FlutterFragCoord().xy; + vec2 uv = (pos - 0.5 * uSize) / min(uSize.x, uSize.y) * 2.0; + + // 1. Polar Coordinates + float r = length(uv); + float a = atan(uv.y, uv.x); + + // 2. The "Galaxy" Twist + // We modify the angle 'a' based on the radius 'r'. + // The further out we go, the more we twist the angle. + // This creates the spiral shape for the coordinate system. + float twistAmount = 4.0; + float rotationSpeed = uTime * 0.4; + + // The Twist Calculation: + // a: Start with base angle + // + r * twistAmount: Twist more as we get further from center + // - rotationSpeed: Rotate the whole thing over time + float spiralAngle = a + (r * twistAmount) - rotationSpeed; + + // 3. Map back to Cartesian to sample noise + // We use this 'warped' coordinate system to sample the noise. + // Because the coordinates are twisted, the noise will appear twisted. + vec2 spiralUV = vec2(cos(spiralAngle), sin(spiralAngle)) * r; + + // 4. Sample Noise + // We use a lower frequency (1.5) to get "thick" bands like the reference + float n = snoise3(vec3(spiralUV * 1.5, uTime * 0.2)); + + // Add a little secondary detail noise + float nDetail = snoise3(vec3(uv * 3.0, uTime * 0.1)); + + // 5. Colors + vec3 purple = vec3(0.35, 0.20, 0.60); // Darker Deep Purple base + vec3 orange = vec3(0.98, 0.65, 0.45); // Bright Peachy Orange + vec3 blue = vec3(0.50, 0.65, 1.00); // Light Periwinkle Blue + + // Start with Purple + vec3 color = purple; + + // Add Orange Bands + // We use smoothstep to create distinct "ribbons" rather than a blurry mix + float orangeMix = smoothstep(0.1, 0.4, n); + color = mix(color, orange, orangeMix); + + // Add Blue Highlights + // These appear in the "negative" space of the noise + float blueMix = smoothstep(-0.3, -0.05, n); + color = mix(color, blue, blueMix * 0.8); + + // 6. Center Glow + // Add a strong white/orange hot core + float coreGlow = 1.0 - smoothstep(0.0, 0.35, r); + color += vec3(1.0, 0.8, 0.6) * coreGlow * 0.9; + + // 7. Circle Mask & Premultiplied Alpha + float alpha = 1.0 - smoothstep(0.9, 1.0, r); + + fragColor = vec4(color * alpha, alpha); +} \ No newline at end of file diff --git a/examples/glow/shaders/ripple_frost.frag b/examples/glow/shaders/ripple_frost.frag new file mode 100644 index 000000000..0f934d073 --- /dev/null +++ b/examples/glow/shaders/ripple_frost.frag @@ -0,0 +1,78 @@ +#include + +uniform vec2 uSize; // x, y resolution +uniform float uTime; // For animating the ripple phase +uniform float uProgress; // 0.0 to 1.0, controls the overall effect strength +uniform sampler2D uTexture; // The input image + +out vec4 fragColor; + +void main() { + vec2 pos = FlutterFragCoord().xy; + vec2 uv = pos / uSize; + vec2 center = vec2(0.5, 0.5); + + // Aspect ratio correction for circular ripples + float ratio = uSize.x / uSize.y; + vec2 uvCorrected = uv; + uvCorrected.x *= ratio; + vec2 centerCorrected = center; + centerCorrected.x *= ratio; + + // --- 1. Multiple Ripples --- + vec2 toCenter = uvCorrected - centerCorrected; + float dist = length(toCenter); + + // Frequency: How many ripples + float frequency = 30.0; + // Speed: How fast they move outwards + float speed = 10.0; + + // Continuous concentric waves moving outwards + float wave = sin(dist * frequency - uTime * speed); + + // Attenuation: Ripples get weaker further from center + float attenuation = 1.0 / (1.0 + dist * 2.0); + + // Distortion direction + vec2 dir = vec2(0.0); + if (dist > 0.001) { + dir = normalize(toCenter); + } + + // Apply distortion + // Strength is modulated by uProgress (fade in/out) and attenuation + float strength = 0.02 * uProgress * attenuation; + vec2 distortion = dir * wave * strength; + vec2 distortedUV = uv + distortion; + + // --- 2. Frosted Glass (Blur) --- + // Increased blur radius for stronger frost + float blurRadius = 0.025 * uProgress; + vec4 blurredColor = vec4(0.0); + float totalWeight = 0.0; + + // 5x5 Sample Grid for smoother, stronger blur + for (float x = -2.0; x <= 2.0; x++) { + for (float y = -2.0; y <= 2.0; y++) { + // Add pseudo-random noise to the offset for a "frosted" texture look + float noise = fract(sin(dot(uv + vec2(x, y), vec2(12.9898, 78.233))) * 43758.5453); + + // Offset with noise influence + vec2 offset = vec2(x, y) * blurRadius * (0.7 + 0.6 * noise); + + // Gaussian-like weight + float weight = 1.0 / (1.0 + length(vec2(x, y))); + + blurredColor += texture(uTexture, distortedUV + offset) * weight; + totalWeight += weight; + } + } + blurredColor /= totalWeight; + + // --- 3. Final Mix --- + vec4 originalColor = texture(uTexture, uv); + + // Mix based on uProgress + fragColor = mix(originalColor, blurredColor, uProgress); +} diff --git a/examples/glow/web/favicon.png b/examples/glow/web/favicon.png new file mode 100644 index 000000000..8aaa46ac1 Binary files /dev/null and b/examples/glow/web/favicon.png differ diff --git a/examples/glow/web/icons/Icon-192.png b/examples/glow/web/icons/Icon-192.png new file mode 100644 index 000000000..b749bfef0 Binary files /dev/null and b/examples/glow/web/icons/Icon-192.png differ diff --git a/examples/glow/web/icons/Icon-512.png b/examples/glow/web/icons/Icon-512.png new file mode 100644 index 000000000..88cfd48df Binary files /dev/null and b/examples/glow/web/icons/Icon-512.png differ diff --git a/examples/glow/web/icons/Icon-maskable-192.png b/examples/glow/web/icons/Icon-maskable-192.png new file mode 100644 index 000000000..eb9b4d76e Binary files /dev/null and b/examples/glow/web/icons/Icon-maskable-192.png differ diff --git a/examples/glow/web/icons/Icon-maskable-512.png b/examples/glow/web/icons/Icon-maskable-512.png new file mode 100644 index 000000000..d69c56691 Binary files /dev/null and b/examples/glow/web/icons/Icon-maskable-512.png differ diff --git a/examples/glow/web/index.html b/examples/glow/web/index.html new file mode 100644 index 000000000..960420fb6 --- /dev/null +++ b/examples/glow/web/index.html @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + glow + + + + + + + + + \ No newline at end of file diff --git a/examples/glow/web/manifest.json b/examples/glow/web/manifest.json new file mode 100644 index 000000000..bc3d6d6e1 --- /dev/null +++ b/examples/glow/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "glow", + "short_name": "glow", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/examples/glow/windows/.gitignore b/examples/glow/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/examples/glow/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/examples/glow/windows/CMakeLists.txt b/examples/glow/windows/CMakeLists.txt new file mode 100644 index 000000000..314fa76f3 --- /dev/null +++ b/examples/glow/windows/CMakeLists.txt @@ -0,0 +1,122 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(glow LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "glow") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/examples/glow/windows/flutter/CMakeLists.txt b/examples/glow/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..3a2498832 --- /dev/null +++ b/examples/glow/windows/flutter/CMakeLists.txt @@ -0,0 +1,123 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + ${FLUTTER_TARGET_PLATFORM} $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/examples/glow/windows/flutter/generated_plugin_registrant.cc b/examples/glow/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000..391ef5626 --- /dev/null +++ b/examples/glow/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,17 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSaverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSaverPlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/examples/glow/windows/flutter/generated_plugin_registrant.h b/examples/glow/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000..dc139d85a --- /dev/null +++ b/examples/glow/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/examples/glow/windows/flutter/generated_plugins.cmake b/examples/glow/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000..047111654 --- /dev/null +++ b/examples/glow/windows/flutter/generated_plugins.cmake @@ -0,0 +1,25 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_saver + url_launcher_windows +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/examples/glow/windows/runner/CMakeLists.txt b/examples/glow/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..2c6068ac2 --- /dev/null +++ b/examples/glow/windows/runner/CMakeLists.txt @@ -0,0 +1,54 @@ +# Copyright 2026 Google LLC +# +# 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 +# +# http://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. + +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/examples/glow/windows/runner/Runner.rc b/examples/glow/windows/runner/Runner.rc new file mode 100644 index 000000000..eac2f013a --- /dev/null +++ b/examples/glow/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD +#else +#define VERSION_AS_NUMBER 1,0,0,0 +#endif + +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "glow" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "glow" "\0" + VALUE "LegalCopyright", "Copyright (C) 2025 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "glow.exe" "\0" + VALUE "ProductName", "glow" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/examples/glow/windows/runner/flutter_window.cpp b/examples/glow/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..79f09361a --- /dev/null +++ b/examples/glow/windows/runner/flutter_window.cpp @@ -0,0 +1,85 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/examples/glow/windows/runner/flutter_window.h b/examples/glow/windows/runner/flutter_window.h new file mode 100644 index 000000000..a37e867a8 --- /dev/null +++ b/examples/glow/windows/runner/flutter_window.h @@ -0,0 +1,49 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/examples/glow/windows/runner/main.cpp b/examples/glow/windows/runner/main.cpp new file mode 100644 index 000000000..8958bf796 --- /dev/null +++ b/examples/glow/windows/runner/main.cpp @@ -0,0 +1,57 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.Create(L"glow", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/examples/glow/windows/runner/resource.h b/examples/glow/windows/runner/resource.h new file mode 100644 index 000000000..d1868f025 --- /dev/null +++ b/examples/glow/windows/runner/resource.h @@ -0,0 +1,32 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/examples/glow/windows/runner/resources/app_icon.ico b/examples/glow/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/examples/glow/windows/runner/resources/app_icon.ico differ diff --git a/examples/glow/windows/runner/runner.exe.manifest b/examples/glow/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..153653e8d --- /dev/null +++ b/examples/glow/windows/runner/runner.exe.manifest @@ -0,0 +1,14 @@ + + + + + PerMonitorV2 + + + + + + + + + diff --git a/examples/glow/windows/runner/utils.cpp b/examples/glow/windows/runner/utils.cpp new file mode 100644 index 000000000..83ead5a8f --- /dev/null +++ b/examples/glow/windows/runner/utils.cpp @@ -0,0 +1,79 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + unsigned int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + input_length, utf8_string.data(), target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/examples/glow/windows/runner/utils.h b/examples/glow/windows/runner/utils.h new file mode 100644 index 000000000..daa2220a8 --- /dev/null +++ b/examples/glow/windows/runner/utils.h @@ -0,0 +1,35 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/examples/glow/windows/runner/win32_window.cpp b/examples/glow/windows/runner/win32_window.cpp new file mode 100644 index 000000000..121401cdb --- /dev/null +++ b/examples/glow/windows/runner/win32_window.cpp @@ -0,0 +1,302 @@ +// Copyright 2026 Google LLC +// +// 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 +// +// http://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. + +#include "win32_window.h" + +#include +#include + +#include "resource.h" + +namespace { + +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + } + FreeLibrary(user32_module); +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registrar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + UpdateTheme(window); + + return OnCreate(); +} + +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/examples/glow/windows/runner/win32_window.h b/examples/glow/windows/runner/win32_window.h new file mode 100644 index 000000000..138976f23 --- /dev/null +++ b/examples/glow/windows/runner/win32_window.h @@ -0,0 +1,118 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * http://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. + */ + +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates a win32 window with |title| that is positioned and sized using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_