From d59a659769b285974b3456a2f10e04da67d693a5 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 05:31:13 +1100 Subject: [PATCH 01/15] wip --- CLAUDE.md | 237 +++++ example/.gitignore | 45 + example/.metadata | 30 + example/README.md | 3 + example/analysis_options.yaml | 1 + example/ios/.gitignore | 34 + example/ios/Flutter/AppFrameworkInfo.plist | 24 + example/ios/Flutter/Debug.xcconfig | 2 + example/ios/Flutter/Release.xcconfig | 2 + example/ios/Podfile | 43 + example/ios/Podfile.lock | 30 + example/ios/Runner.xcodeproj/project.pbxproj | 732 +++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 101 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + example/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 +++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + example/ios/Runner/Base.lproj/Main.storyboard | 26 + example/ios/Runner/Info.plist | 70 ++ example/ios/Runner/Runner-Bridging-Header.h | 1 + example/ios/Runner/SceneDelegate.swift | 6 + example/ios/RunnerTests/RunnerTests.swift | 12 + example/lib/main.dart | 831 ++++++++++++++++++ example/macos/.gitignore | 7 + example/macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 14 + example/macos/Podfile | 42 + example/macos/Podfile.lock | 30 + .../macos/Runner.xcodeproj/project.pbxproj | 801 +++++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 99 +++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + example/macos/Runner/AppDelegate.swift | 13 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 102994 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 5680 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 520 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 14142 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1066 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 36406 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 2218 bytes example/macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ example/macos/Runner/Configs/AppInfo.xcconfig | 14 + example/macos/Runner/Configs/Debug.xcconfig | 2 + example/macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 14 + example/macos/Runner/Info.plist | 32 + example/macos/Runner/MainFlutterWindow.swift | 15 + example/macos/Runner/Release.entitlements | 10 + example/macos/RunnerTests/RunnerTests.swift | 12 + example/pubspec.lock | 433 +++++++++ example/pubspec.yaml | 22 + example/test/widget_test.dart | 30 + lib/pixelify_flutter.dart | 15 + lib/src/animations/fade_animation.dart | 5 +- lib/src/animations/wave_animation.dart | 131 +++ lib/src/effects/noise_effect.dart | 14 +- lib/src/widgets/pixel_notification_card.dart | 380 ++++++++ lib/src/widgets/pixel_progress_bar.dart | 10 +- lib/src/widgets/pixel_text.dart | 2 +- pubspec.yaml | 1 + shaders/bloom.frag | 2 +- shaders/{pixalate.frag => pixelate.frag} | 0 test/animations_test.dart | 382 ++++++++ test/effects_test.dart | 272 ++++++ test/pixelify_flutter_test.dart | 694 ++++++++++++++- test/utils_test.dart | 226 +++++ 95 files changed, 6644 insertions(+), 18 deletions(-) create mode 100644 CLAUDE.md create mode 100644 example/.gitignore create mode 100644 example/.metadata create mode 100644 example/README.md create mode 100644 example/analysis_options.yaml create mode 100644 example/ios/.gitignore create mode 100644 example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 example/ios/Flutter/Debug.xcconfig create mode 100644 example/ios/Flutter/Release.xcconfig create mode 100644 example/ios/Podfile create mode 100644 example/ios/Podfile.lock create mode 100644 example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner/AppDelegate.swift create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 example/ios/Runner/Info.plist create mode 100644 example/ios/Runner/Runner-Bridging-Header.h create mode 100644 example/ios/Runner/SceneDelegate.swift create mode 100644 example/ios/RunnerTests/RunnerTests.swift create mode 100644 example/lib/main.dart create mode 100644 example/macos/.gitignore create mode 100644 example/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 example/macos/Flutter/Flutter-Release.xcconfig create mode 100644 example/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 example/macos/Podfile create mode 100644 example/macos/Podfile.lock create mode 100644 example/macos/Runner.xcodeproj/project.pbxproj create mode 100644 example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 example/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/macos/Runner/AppDelegate.swift create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 example/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 example/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 example/macos/Runner/Configs/Debug.xcconfig create mode 100644 example/macos/Runner/Configs/Release.xcconfig create mode 100644 example/macos/Runner/Configs/Warnings.xcconfig create mode 100644 example/macos/Runner/DebugProfile.entitlements create mode 100644 example/macos/Runner/Info.plist create mode 100644 example/macos/Runner/MainFlutterWindow.swift create mode 100644 example/macos/Runner/Release.entitlements create mode 100644 example/macos/RunnerTests/RunnerTests.swift create mode 100644 example/pubspec.lock create mode 100644 example/pubspec.yaml create mode 100644 example/test/widget_test.dart create mode 100644 lib/src/widgets/pixel_notification_card.dart rename shaders/{pixalate.frag => pixelate.frag} (100%) create mode 100644 test/animations_test.dart create mode 100644 test/effects_test.dart create mode 100644 test/utils_test.dart diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ba9a8b7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,237 @@ +# CLAUDE.md - Pixelify Flutter + +## Project Overview +Pixelify Flutter is a retro pixel-art UI component library for Flutter. It provides pixel-style widgets, animations, effects, and utilities for creating nostalgic gaming-inspired interfaces. + +## Build & Test Commands +```bash +# Run all tests +flutter test + +# Run specific test file +flutter test test/pixelify_flutter_test.dart + +# Run tests with coverage +flutter test --coverage + +# Build example app for macOS +cd example && flutter build macos + +# Run example app +cd example && flutter run -d macos + +# Analyze code +flutter analyze + +# Format code +dart format lib test +``` + +## Project Structure +``` +pixelify_flutter/ +├── lib/ +│ ├── pixelify_flutter.dart # Main export file +│ └── src/ +│ ├── widgets/ # UI Components +│ │ ├── pixel_button.dart +│ │ ├── pixel_text.dart +│ │ ├── pixel_progress_bar.dart +│ │ ├── pixel_notification_card.dart +│ │ └── pixel_container.dart +│ ├── animations/ # Animation widgets +│ │ ├── fade_animation.dart +│ │ ├── flicker_animation.dart +│ │ ├── jitter_animation.dart +│ │ └── wave_animation.dart +│ ├── effects/ # Visual effects +│ │ ├── pixelate_effect.dart +│ │ ├── scanline_effect.dart +│ │ ├── bloom_effect.dart +│ │ ├── glitch_effect.dart +│ │ └── noise_effect.dart +│ └── utils/ # Utilities +│ ├── pixel_palette.dart +│ ├── dithering.dart +│ ├── shader_loader.dart +│ └── pixel_theme.dart +├── shaders/ # GLSL fragment shaders +│ ├── bloom.frag +│ ├── pixelate.frag +│ └── scanline.frag +├── test/ # Test files +│ ├── pixelify_flutter_test.dart +│ ├── animations_test.dart +│ ├── effects_test.dart +│ └── utils_test.dart +└── example/ # Example app + └── lib/main.dart +``` + +## Testing Guidelines + +### Widget Testing Best Practices + +1. **Use specific finders** - Avoid `find.byType(Transform)` which may find multiple widgets from MaterialApp/Scaffold. Use descendant finders: +```dart +expect( + find.descendant( + of: find.byType(MyWidget), + matching: find.byType(Transform), + ), + findsOneWidget, +); +``` + +2. **Handle infinite animations** - Widgets with continuous animations will cause `pumpAndSettle()` to timeout. Use `pump()` with specific duration instead: +```dart +// BAD - will timeout on infinite animations +await tester.pumpAndSettle(); + +// GOOD - pump specific duration +await tester.pump(const Duration(milliseconds: 100)); +``` + +3. **Disable animations in tests** - For widgets with optional animations, disable them for reliable testing: +```dart +PixelButton( + label: 'TEST', + enableGlowAnimation: false, + enableScanlineAnimation: false, + onPressed: () {}, +) +``` + +4. **Avoid pending timers** - If using `Future.delayed`, ensure tests pump past the delay duration to avoid "Timer is still pending" errors: +```dart +// Widget has 200ms delay +await tester.pump(const Duration(milliseconds: 300)); // Pump past delay +``` + +5. **Tap the widget, not the text** - For buttons with overlays, tap the widget type rather than text: +```dart +// May fail if text is obscured +await tester.tap(find.text('BUTTON')); + +// More reliable +await tester.tap(find.byType(PixelButton)); +``` + +### Animation Widget Patterns + +When creating stateful animation widgets: + +1. **Track disposal state** to prevent callbacks after dispose: +```dart +class _MyAnimationState extends State { + bool _isDisposed = false; + + @override + void initState() { + super.initState(); + Future.delayed(widget.delay, () { + if (!_isDisposed && mounted) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _isDisposed = true; + _controller.dispose(); + super.dispose(); + } +} +``` + +2. **Initialize lists before use** - Don't use `late` for lists that might be accessed before initialization: +```dart +// BAD - can cause LateInitializationError +late List _points; + +// GOOD - initialize empty +List _points = []; +``` + +3. **Check mounted in post-frame callbacks**: +```dart +WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + // Safe to call setState + setState(() { /* ... */ }); +}); +``` + +### Layout Debugging + +For RenderFlex overflow errors: + +1. **Account for borders** - Container borders reduce available inner space: +```dart +// If container has border width 1, inner width is totalWidth - 2 +final double innerWidth = segments * (segmentWidth + margin); +final double totalWidth = innerWidth + 2; // +2 for border +``` + +2. **Use Clip.hardEdge** for Stack children that might overflow: +```dart +Stack( + clipBehavior: Clip.hardEdge, + children: [...], +) +``` + +## Key APIs + +### PixelButton +```dart +PixelButton( + label: 'CLICK', + onPressed: () {}, + color: Colors.blue, + outlineColor: Colors.black, + pixelEdgeThickness: 3.0, + enableGlowAnimation: true, + enableScanlineAnimation: true, +) +``` + +### PixelProgressBar +```dart +PixelProgressBar( + progress: 0.5, + style: PixelProgressBarStyle.segmented, // segmented, smooth, iconFilled + segments: 15, + fillColor: Colors.cyanAccent, + showScanlines: true, + showGlow: true, +) +``` + +### GlitchEffect +```dart +GlitchEffect( + intensity: 0.3, + frequency: const Duration(milliseconds: 200), + child: Text('GLITCH'), +) +``` + +### NoiseEffect +```dart +NoiseEffect( + intensity: 0.2, + density: 2000, + animate: true, + color: Colors.white, +) +``` + +## Shader Notes + +Fragment shaders are in `/shaders/` directory. Key considerations: + +1. **Array size must match initialization**: `float weights[5] = float[](...)` +2. **Use proper GLSL ES precision**: `precision highp float;` +3. **Shader loading is async** - handle loading states in widgets diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,45 @@ +# 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 diff --git a/example/.metadata b/example/.metadata new file mode 100644 index 0000000..5fe0fa8 --- /dev/null +++ b/example/.metadata @@ -0,0 +1,30 @@ +# 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: "2bc9a741b3aad0ebe9e2a0e76097d31033795db4" + channel: "master" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 2bc9a741b3aad0ebe9e2a0e76097d31033795db4 + base_revision: 2bc9a741b3aad0ebe9e2a0e76097d31033795db4 + - platform: macos + create_revision: 2bc9a741b3aad0ebe9e2a0e76097d31033795db4 + base_revision: 2bc9a741b3aad0ebe9e2a0e76097d31033795db4 + + # 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/example/README.md b/example/README.md new file mode 100644 index 0000000..1b7a4e3 --- /dev/null +++ b/example/README.md @@ -0,0 +1,3 @@ +# example + +A new Flutter project. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..f9b3034 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/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/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/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/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/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/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/example/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/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..1cfd159 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - audioplayers_darwin (0.0.1): + - Flutter + - FlutterMacOS + - Flutter (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`) + - Flutter (from `Flutter`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + +EXTERNAL SOURCES: + audioplayers_darwin: + :path: ".symlinks/plugins/audioplayers_darwin/darwin" + Flutter: + :path: Flutter + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + +SPEC CHECKSUMS: + audioplayers_darwin: 4027b33a8f471d996c13f71cb77f0b1583b5d923 + Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 + path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba + +PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e + +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..24dc4ab --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,732 @@ +// !$*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 */; }; + 640A41B0628702D62801CE90 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 17DE389C4A195D730550B0E8 /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 7C53B771C67BDB21B11C112B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 909DA773C58DB79B4C8BE077 /* Pods_Runner.framework */; }; + 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 = ""; }; + 17DE389C4A195D730550B0E8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 55439F9217D0E4E618C317D5 /* 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 = ""; }; + 5A5D05B64BF731F916F0C8D1 /* 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 = ""; }; + 63201D6AB5F283C58C74D953 /* 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 = ""; }; + 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 = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 854A242577BB327B8152FAA0 /* 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 = ""; }; + 909DA773C58DB79B4C8BE077 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + B9F7E939AEA155E7AC8F2879 /* 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 = ""; }; + D03786E089D541E9B2D0DF67 /* 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 = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7C53B771C67BDB21B11C112B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FDFBBA7EE171D8CDBE2A97E7 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 640A41B0628702D62801CE90 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 518F85A476CAC3B7AE2833B8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 909DA773C58DB79B4C8BE077 /* Pods_Runner.framework */, + 17DE389C4A195D730550B0E8 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 85DBC12522367E58D314B8FA /* Pods */ = { + isa = PBXGroup; + children = ( + 5A5D05B64BF731F916F0C8D1 /* Pods-Runner.debug.xcconfig */, + 854A242577BB327B8152FAA0 /* Pods-Runner.release.xcconfig */, + 63201D6AB5F283C58C74D953 /* Pods-Runner.profile.xcconfig */, + D03786E089D541E9B2D0DF67 /* Pods-RunnerTests.debug.xcconfig */, + 55439F9217D0E4E618C317D5 /* Pods-RunnerTests.release.xcconfig */, + B9F7E939AEA155E7AC8F2879 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + 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 */, + 85DBC12522367E58D314B8FA /* Pods */, + 518F85A476CAC3B7AE2833B8 /* Frameworks */, + ); + 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 */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.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 = ( + 6CF97EAC8EC76EA976AA5744 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + FDFBBA7EE171D8CDBE2A97E7 /* Frameworks */, + ); + 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 = ( + 4CE7F4CFFEB7781D43855383 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + EFE9EAF14D26FB95ABA058A8 /* [CP] Embed Pods Frameworks */, + ); + 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"; + }; + 4CE7F4CFFEB7781D43855383 /* [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; + }; + 6CF97EAC8EC76EA976AA5744 /* [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; + }; + 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"; + }; + EFE9EAF14D26FB95ABA058A8 /* [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 */ + 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 */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift 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.example; + 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; + baseConfigurationReference = D03786E089D541E9B2D0DF67 /* Pods-RunnerTests.debug.xcconfig */; + 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.example.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; + baseConfigurationReference = 55439F9217D0E4E618C317D5 /* Pods-RunnerTests.release.xcconfig */; + 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.example.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; + baseConfigurationReference = B9F7E939AEA155E7AC8F2879 /* Pods-RunnerTests.profile.xcconfig */; + 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.example.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.example; + 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.example; + 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/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/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/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 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 literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/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/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/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/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..cd88d65 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/Runner/SceneDelegate.swift b/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +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/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..0cbd8f0 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,831 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:pixelify_flutter/pixelify_flutter.dart'; + +void main() { + runApp(const PixelifyExampleApp()); +} + +class PixelifyExampleApp extends StatelessWidget { + const PixelifyExampleApp({super.key}); + + @override + Widget build(BuildContext context) { + return PixelTheme( + accentColor: Colors.cyanAccent, + child: MaterialApp( + title: 'Pixelify Flutter Demo', + debugShowCheckedModeBanner: false, + theme: ThemeData.dark().copyWith( + scaffoldBackgroundColor: const Color(0xFF1A1C24), + // Fallback text theme in case Google Fonts fail + textTheme: ThemeData.dark().textTheme.apply( + fontFamily: 'monospace', + ), + ), + home: const ComponentListPage(), + ), + ); + } +} + +/// List of all component demos with individual pages +class ComponentListPage extends StatelessWidget { + const ComponentListPage({super.key}); + + @override + Widget build(BuildContext context) { + final components = [ + ComponentItem('PixelButton', const PixelButtonPage()), + ComponentItem('PixelText', const PixelTextPage()), + ComponentItem('PixelPanel', const PixelPanelPage()), + ComponentItem('PixelSlider', const PixelSliderPage()), + ComponentItem('PixelToggle', const PixelTogglePage()), + ComponentItem('PixelProgressBar', const PixelProgressBarPage()), + ComponentItem('PixelNotificationCard', const PixelNotificationCardPage()), + ComponentItem('FadeAnimation', const FadeAnimationPage()), + ComponentItem('FlickerAnimation', const FlickerAnimationPage()), + ComponentItem('JitterAnimation', const JitterAnimationPage()), + ComponentItem('WaveAnimation', const WaveAnimationPage()), + ComponentItem('GlitchEffect', const GlitchEffectPage()), + ComponentItem('NoiseEffect', const NoiseEffectPage()), + ]; + + return Scaffold( + appBar: AppBar( + backgroundColor: const Color(0xFF2A2D3A), + title: Text( + 'PIXELIFY COMPONENTS', + style: GoogleFonts.pressStart2p( + fontSize: 12, + color: Colors.cyanAccent, + ), + ), + centerTitle: true, + ), + body: ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: components.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final item = components[index]; + return ListTile( + tileColor: const Color(0xFF2A2D3A), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + side: const BorderSide(color: Color(0xFF3D4155), width: 2), + ), + title: Text( + item.name, + style: GoogleFonts.pressStart2p( + fontSize: 10, + color: Colors.white, + ), + ), + trailing: const Icon(Icons.chevron_right, color: Colors.cyanAccent), + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => item.page), + ), + ); + }, + ), + ); + } +} + +class ComponentItem { + final String name; + final Widget page; + ComponentItem(this.name, this.page); +} + +/// Base page template for component demos +class ComponentDemoPage extends StatelessWidget { + final String title; + final Widget child; + + const ComponentDemoPage({ + super.key, + required this.title, + required this.child, + }); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + backgroundColor: const Color(0xFF2A2D3A), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.cyanAccent), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + title, + style: GoogleFonts.pressStart2p( + fontSize: 10, + color: Colors.cyanAccent, + ), + ), + iconTheme: const IconThemeData(color: Colors.cyanAccent), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: child, + ), + ); + } +} + +// ============== Individual Component Pages ============== + +class PixelButtonPage extends StatelessWidget { + const PixelButtonPage({super.key}); + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return ComponentDemoPage( + title: 'PixelButton', + child: Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelButton( + label: 'DEFAULT', + textStyle: pixelFont, + onPressed: () {}, + ), + PixelButton( + label: 'CYAN OUTLINE', + textStyle: pixelFont, + onPressed: () {}, + outlineColor: Colors.cyanAccent, + ), + PixelButton( + label: 'GREEN BUTTON', + textStyle: pixelFont, + onPressed: () {}, + outlineColor: Colors.greenAccent, + color: const Color(0xFF1A3A2A), + ), + PixelButton( + label: 'RED BUTTON', + textStyle: pixelFont, + onPressed: () {}, + outlineColor: Colors.redAccent, + color: const Color(0xFF3A1A1A), + ), + PixelButton( + label: 'DISABLED', + textStyle: pixelFont, + enabled: false, + ), + PixelButton( + label: 'WITH GLOW', + textStyle: pixelFont, + onPressed: () {}, + enableGlowAnimation: true, + outlineColor: Colors.purpleAccent, + ), + PixelButton( + label: 'WITH SCANLINES', + textStyle: pixelFont, + onPressed: () {}, + enableScanlineAnimation: true, + outlineColor: Colors.amber, + ), + ], + ), + ); + } +} + +class PixelTextPage extends StatelessWidget { + const PixelTextPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'PixelText', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PixelText( + text: 'NORMAL TEXT', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.white), + ), + const SizedBox(height: 24), + PixelText( + text: 'FLICKER EFFECT', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.cyanAccent), + effect: PixelTextEffect.flicker, + ), + const SizedBox(height: 24), + SizedBox( + height: 30, + child: PixelText( + text: 'GLITCH EFFECT', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.greenAccent), + effect: PixelTextEffect.glitch, + effectDuration: const Duration(milliseconds: 150), + ), + ), + const SizedBox(height: 24), + PixelText( + text: 'SCANLINE EFFECT', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.purpleAccent), + effect: PixelTextEffect.scanline, + ), + const SizedBox(height: 24), + PixelText( + text: 'PALETTE COLORS', + style: GoogleFonts.pressStart2p(fontSize: 12), + enablePaletteColors: true, + palette: const [ + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.blue, + Colors.purple, + ], + ), + ], + ), + ); + } +} + +class PixelPanelPage extends StatelessWidget { + const PixelPanelPage({super.key}); + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return ComponentDemoPage( + title: 'PixelPanel', + child: Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.pixelOutline, + borderColor: Colors.cyanAccent, + child: Center( + child: Text('PIXEL\nOUTLINE', style: pixelFont, textAlign: TextAlign.center), + ), + ), + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.glowingBorder, + borderColor: Colors.greenAccent, + child: Center( + child: Text('GLOWING\nBORDER', style: pixelFont, textAlign: TextAlign.center), + ), + ), + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.oldScreenCRT, + borderColor: Colors.amber, + child: Center( + child: Text( + 'OLD CRT\nSCREEN', + style: pixelFont.copyWith(color: Colors.amber), + textAlign: TextAlign.center, + ), + ), + ), + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.paperGrain, + child: Center( + child: Text( + 'PAPER\nGRAIN', + style: pixelFont.copyWith(color: Colors.black87), + textAlign: TextAlign.center, + ), + ), + ), + ], + ), + ); + } +} + +class PixelSliderPage extends StatefulWidget { + const PixelSliderPage({super.key}); + + @override + State createState() => _PixelSliderPageState(); +} + +class _PixelSliderPageState extends State { + double _value1 = 0.5; + double _value2 = 0.3; + double _value3 = 0.7; + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return ComponentDemoPage( + title: 'PixelSlider', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('DEFAULT: ${(_value1 * 100).toInt()}%', style: pixelFont), + const SizedBox(height: 8), + SizedBox( + width: 300, + child: PixelSlider( + value: _value1, + onChanged: (v) => setState(() => _value1 = v), + divisions: 10, + ), + ), + const SizedBox(height: 24), + Text('CYAN: ${(_value2 * 100).toInt()}%', style: pixelFont), + const SizedBox(height: 8), + SizedBox( + width: 300, + child: PixelSlider( + value: _value2, + onChanged: (v) => setState(() => _value2 = v), + activeColor: Colors.cyanAccent, + inactiveColor: const Color(0xFF3D4155), + ), + ), + const SizedBox(height: 24), + Text('GREEN: ${(_value3 * 100).toInt()}%', style: pixelFont), + const SizedBox(height: 8), + SizedBox( + width: 300, + child: PixelSlider( + value: _value3, + onChanged: (v) => setState(() => _value3 = v), + activeColor: Colors.greenAccent, + inactiveColor: const Color(0xFF3D4155), + ), + ), + ], + ), + ); + } +} + +class PixelTogglePage extends StatefulWidget { + const PixelTogglePage({super.key}); + + @override + State createState() => _PixelTogglePageState(); +} + +class _PixelTogglePageState extends State { + bool _value1 = true; + bool _value2 = false; + bool _value3 = true; + bool _value4 = true; + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return ComponentDemoPage( + title: 'PixelToggle', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('DEFAULT:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _value1, + onChanged: (v) => setState(() => _value1 = v), + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + Text('GREEN:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _value2, + onChanged: (v) => setState(() => _value2 = v), + onColor: Colors.greenAccent, + offColor: const Color(0xFF3D4155), + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + Text('BLINKING:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _value3, + onChanged: (v) => setState(() => _value3 = v), + blinking: true, + onColor: Colors.cyanAccent, + ), + ], + ), + const SizedBox(height: 24), + Row( + children: [ + Text('FLIP ANIM:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _value4, + onChanged: (v) => setState(() => _value4 = v), + flipAnimation: true, + onColor: Colors.purpleAccent, + ), + ], + ), + ], + ), + ); + } +} + +class PixelProgressBarPage extends StatefulWidget { + const PixelProgressBarPage({super.key}); + + @override + State createState() => _PixelProgressBarPageState(); +} + +class _PixelProgressBarPageState extends State { + double _progress = 0.7; + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return ComponentDemoPage( + title: 'PixelProgressBar', + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('SEGMENTED', style: pixelFont), + const SizedBox(height: 8), + PixelProgressBar( + progress: _progress, + style: PixelProgressBarStyle.segmented, + segments: 15, + fillColor: Colors.cyanAccent, + backgroundColor: const Color(0xFF2A2D3A), + showGlow: true, + ), + const SizedBox(height: 24), + Text('SMOOTH', style: pixelFont), + const SizedBox(height: 8), + PixelProgressBar( + progress: _progress, + style: PixelProgressBarStyle.smooth, + fillColor: Colors.greenAccent, + backgroundColor: const Color(0xFF2A2D3A), + showScanlines: true, + ), + const SizedBox(height: 24), + Text('ICON FILLED', style: pixelFont), + const SizedBox(height: 8), + PixelProgressBar( + progress: _progress, + style: PixelProgressBarStyle.iconFilled, + fillColor: Colors.purpleAccent, + backgroundColor: const Color(0xFF2A2D3A), + icon: const Icon(Icons.star, color: Colors.yellowAccent, size: 20), + iconSize: 20, + height: 24, + showGlow: true, + ), + const SizedBox(height: 24), + Text('PROGRESS: ${(_progress * 100).toInt()}%', style: pixelFont), + const SizedBox(height: 8), + SizedBox( + width: 300, + child: PixelSlider( + value: _progress, + onChanged: (v) => setState(() => _progress = v), + activeColor: Colors.cyanAccent, + ), + ), + ], + ), + ); + } +} + +class PixelNotificationCardPage extends StatelessWidget { + const PixelNotificationCardPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'PixelNotificationCard', + child: Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelNotificationCard( + title: 'John Doe', + action: 'Liked your post', + avatar: const PixelAvatar(), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + onTap: () {}, + ), + PixelNotificationCard( + title: 'Jane Smith', + action: 'Commented', + avatar: const PixelAvatar(backgroundColor: Color(0xFF4A4E65)), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + onTap: () {}, + ), + PixelNotificationCard( + title: 'Alex Johnson', + action: 'Followed you', + avatar: const PixelAvatar(backgroundColor: Color(0xFF3D5A80)), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + showScanlines: true, + onTap: () {}, + ), + PixelNotificationCard( + title: 'Sara Wilson', + action: 'Shared a post', + avatar: const PixelAvatar(backgroundColor: Color(0xFF5A3D80)), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + width: 320, + onTap: () {}, + ), + ], + ), + ); + } +} + +class FadeAnimationPage extends StatelessWidget { + const FadeAnimationPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'FadeAnimation', + child: Column( + children: [ + FadeAnimation( + duration: const Duration(milliseconds: 1500), + child: Container( + width: 150, + height: 150, + color: Colors.cyanAccent, + child: Center( + child: Text( + 'FADE', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.black), + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Looping fade animation', + style: GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70), + ), + ], + ), + ); + } +} + +class FlickerAnimationPage extends StatelessWidget { + const FlickerAnimationPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'FlickerAnimation', + child: Column( + children: [ + FlickerAnimation( + duration: const Duration(milliseconds: 300), + minOpacity: 0.5, + child: Container( + width: 150, + height: 150, + color: Colors.greenAccent, + child: Center( + child: Text( + 'FLICKER', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.black), + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Random opacity flickering', + style: GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70), + ), + ], + ), + ); + } +} + +class JitterAnimationPage extends StatelessWidget { + const JitterAnimationPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'JitterAnimation', + child: Column( + children: [ + JitterAnimation( + strengthX: 3, + strengthY: 3, + duration: const Duration(milliseconds: 100), + child: Container( + width: 150, + height: 150, + color: Colors.redAccent, + child: Center( + child: Text( + 'JITTER', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Random position shaking', + style: GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70), + ), + ], + ), + ); + } +} + +class WaveAnimationPage extends StatelessWidget { + const WaveAnimationPage({super.key}); + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return ComponentDemoPage( + title: 'WaveAnimation', + child: Column( + children: [ + Text('VERTICAL', style: pixelFont), + const SizedBox(height: 8), + WaveAnimation( + amplitude: 8, + frequency: 2, + direction: WaveDirection.vertical, + child: Container( + width: 100, + height: 100, + color: Colors.purpleAccent, + child: Center( + child: Text( + 'WAVE', + style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 32), + Text('HORIZONTAL', style: pixelFont), + const SizedBox(height: 8), + WaveAnimation( + amplitude: 8, + frequency: 2, + direction: WaveDirection.horizontal, + child: Container( + width: 100, + height: 100, + color: Colors.orangeAccent, + child: Center( + child: Text( + 'WAVE', + style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.black), + ), + ), + ), + ), + const SizedBox(height: 32), + Text('DIAGONAL', style: pixelFont), + const SizedBox(height: 8), + WaveAnimation( + amplitude: 8, + frequency: 2, + direction: WaveDirection.diagonal, + child: Container( + width: 100, + height: 100, + color: Colors.tealAccent, + child: Center( + child: Text( + 'WAVE', + style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.black), + ), + ), + ), + ), + ], + ), + ); + } +} + +class GlitchEffectPage extends StatelessWidget { + const GlitchEffectPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'GlitchEffect', + child: Column( + children: [ + GlitchEffect( + intensity: 0.5, + frequency: const Duration(milliseconds: 200), + child: Container( + width: 200, + height: 200, + color: Colors.cyan, + child: Center( + child: Text( + 'GLITCH', + style: GoogleFonts.pressStart2p(fontSize: 16, color: Colors.white), + ), + ), + ), + ), + const SizedBox(height: 16), + Text( + 'Color split glitch effect', + style: GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70), + ), + ], + ), + ); + } +} + +class NoiseEffectPage extends StatelessWidget { + const NoiseEffectPage({super.key}); + + @override + Widget build(BuildContext context) { + return ComponentDemoPage( + title: 'NoiseEffect', + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: Stack( + children: [ + Container( + color: Colors.blue, + child: Center( + child: Text( + 'NOISE', + style: GoogleFonts.pressStart2p(fontSize: 16, color: Colors.white), + ), + ), + ), + const Positioned.fill( + child: NoiseEffect( + intensity: 0.3, + animate: true, + color: Colors.white, + animationSpeed: Duration(milliseconds: 100), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + Text( + 'Animated noise overlay', + style: GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70), + ), + ], + ), + ); + } +} diff --git a/example/macos/.gitignore b/example/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/example/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/example/macos/Flutter/Flutter-Debug.xcconfig b/example/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..4b81f9b --- /dev/null +++ b/example/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/example/macos/Flutter/Flutter-Release.xcconfig b/example/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..5caa9d1 --- /dev/null +++ b/example/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/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..a9f2f23 --- /dev/null +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,14 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import audioplayers_darwin +import path_provider_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) +} diff --git a/example/macos/Podfile b/example/macos/Podfile new file mode 100644 index 0000000..ff5ddb3 --- /dev/null +++ b/example/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/example/macos/Podfile.lock b/example/macos/Podfile.lock new file mode 100644 index 0000000..dc09b13 --- /dev/null +++ b/example/macos/Podfile.lock @@ -0,0 +1,30 @@ +PODS: + - audioplayers_darwin (0.0.1): + - Flutter + - FlutterMacOS + - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + +DEPENDENCIES: + - audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin`) + - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + +EXTERNAL SOURCES: + audioplayers_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/darwin + FlutterMacOS: + :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + +SPEC CHECKSUMS: + audioplayers_darwin: 4027b33a8f471d996c13f71cb77f0b1583b5d923 + FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 + path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba + +PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 + +COCOAPODS: 1.16.2 diff --git a/example/macos/Runner.xcodeproj/project.pbxproj b/example/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..137e9aa --- /dev/null +++ b/example/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 */ + 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 */; }; + C6CDE62E4551AD072D4A373B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BC1979EEC02BE59648893179 /* Pods_Runner.framework */; }; + F4EB8C6F9307624A6CA51E23 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBC4B99596AD60F5079ACD3E /* 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 */ + 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 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.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 = ""; }; + 34B6EAC692D23195302168F3 /* 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 = ""; }; + 6604E166F0689C09BD56A23E /* 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 = ""; }; + 74C76507B486D8D5E32935B1 /* 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 = ""; }; + 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 = ""; }; + A6898A1B204A03417BA08047 /* 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 = ""; }; + BC1979EEC02BE59648893179 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + BD0E2547C71727E8B2F63F46 /* 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 = ""; }; + E53DD2B593D735BF7CDAA463 /* 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 = ""; }; + FBC4B99596AD60F5079ACD3E /* 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 = ( + F4EB8C6F9307624A6CA51E23 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + C6CDE62E4551AD072D4A373B /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2AA703B63ECF15A88B966644 /* Pods */ = { + isa = PBXGroup; + children = ( + BD0E2547C71727E8B2F63F46 /* Pods-Runner.debug.xcconfig */, + A6898A1B204A03417BA08047 /* Pods-Runner.release.xcconfig */, + 74C76507B486D8D5E32935B1 /* Pods-Runner.profile.xcconfig */, + 34B6EAC692D23195302168F3 /* Pods-RunnerTests.debug.xcconfig */, + E53DD2B593D735BF7CDAA463 /* Pods-RunnerTests.release.xcconfig */, + 6604E166F0689C09BD56A23E /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 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 */, + 2AA703B63ECF15A88B966644 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.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 = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + BC1979EEC02BE59648893179 /* Pods_Runner.framework */, + FBC4B99596AD60F5079ACD3E /* 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 = ( + 62581C11508A5913920C03AD /* [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 = ( + 050E90381E15FFCAC42228FD /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 73A954682B48ABD742050C25 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.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 */ + 050E90381E15FFCAC42228FD /* [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; + }; + 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"; + }; + 62581C11508A5913920C03AD /* [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; + }; + 73A954682B48ABD742050C25 /* [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 = 34B6EAC692D23195302168F3 /* 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.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E53DD2B593D735BF7CDAA463 /* 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.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6604E166F0689C09BD56A23E /* 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.example.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/example.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/example"; + }; + 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/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..ac78810 --- /dev/null +++ b/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/example/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/macos/Runner/AppDelegate.swift b/example/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/example/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +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/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/example/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/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..82b6f9d9a33e198f5747104729e1fcef999772a5 GIT binary patch literal 102994 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ⅅ8MglG&ER{i5lN315cO?EdHNLrg? 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 z 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 Pe7HEdAEg|=c=LY&YVNkY literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..13b35eba55c6dabc3aac36f33d859266c18fa0d0 GIT binary patch literal 5680 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 literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000000000000000000000000000000000000..0a3f5fa40fb3d1e0710331a48de5d256da3f275d GIT binary patch literal 520 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 literal 0 HcmV?d00001 diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1632cfddf3d9dade342351e627a0a75609fb46 GIT binary patch literal 2218 zcmV;b2vzrqP)Px#L}ge>W=%~1DgXcg2mk?xX#fNO00031000^Q000001E2u_0{{R30RRC20H6W@ z1ONa40RR91K%fHv1ONa40RR91KmY&$07g+lumAuE6iGxuRCodHTWf3-RTMruyW6Fu zQYeUM04eX6D5c0FCjKKPrco1(K`<0SL=crI{PC3-^hZU0kQie$gh-5!7z6SH6Q0J% zqot*`H1q{R5fHFYS}dje@;kG=v$L0(yY0?wY2%*c?A&{2?!D*x?m71{of2gv!$5|C z3>qG_BW}7K_yUcT3A5C6QD<+{aq?x;MAUyAiJn#Jv8_zZtQ{P zTRzbL3U9!qVuZzS$xKU10KiW~Bgdcv1-!uAhQxf3a7q+dU6lj?yoO4Lq4TUN4}h{N z*fIM=SS8|C2$(T>w$`t@3Tka!(r!7W`x z-isCVgQD^mG-MJ;XtJuK3V{Vy72GQ83KRWsHU?e*wrhKk=ApIYeDqLi;JI1e zuvv}5^Dc=k7F7?nm3nIw$NVmU-+R>> zyqOR$-2SDpJ}Pt;^RkJytDVXNTsu|mI1`~G7yw`EJR?VkGfNdqK9^^8P`JdtTV&tX4CNcV4 z&N06nZa??Fw1AgQOUSE2AmPE@WO(Fvo`%m`cDgiv(fAeRA%3AGXUbsGw{7Q`cY;1BI#ac3iN$$Hw z0LT0;xc%=q)me?Y*$xI@GRAw?+}>=9D+KTk??-HJ4=A>`V&vKFS75@MKdSF1JTq{S zc1!^8?YA|t+uKigaq!sT;Z!&0F2=k7F0PIU;F$leJLaw2UI6FL^w}OG&!;+b%ya1c z1n+6-inU<0VM-Y_s5iTElq)ThyF?StVcebpGI znw#+zLx2@ah{$_2jn+@}(zJZ{+}_N9BM;z)0yr|gF-4=Iyu@hI*Lk=-A8f#bAzc9f z`Kd6K--x@t04swJVC3JK1cHY-Hq+=|PN-VO;?^_C#;coU6TDP7Bt`;{JTG;!+jj(` zw5cLQ-(Cz-Tlb`A^w7|R56Ce;Wmr0)$KWOUZ6ai0PhzPeHwdl0H(etP zUV`va_i0s-4#DkNM8lUlqI7>YQLf)(lz9Q3Uw`)nc(z3{m5ZE77Ul$V%m)E}3&8L0 z-XaU|eB~Is08eORPk;=<>!1w)Kf}FOVS2l&9~A+@R#koFJ$Czd%Y(ENTV&A~U(IPI z;UY+gf+&6ioZ=roly<0Yst8ck>(M=S?B-ys3mLdM&)ex!hbt+ol|T6CTS+Sc0jv(& z7ijdvFwBq;0a{%3GGwkDKTeG`b+lyj0jjS1OMkYnepCdoosNY`*zmBIo*981BU%%U z@~$z0V`OVtIbEx5pa|Tct|Lg#ZQf5OYMUMRD>Wdxm5SAqV2}3!ceE-M2 z@O~lQ0OiKQp}o9I;?uxCgYVV?FH|?Riri*U$Zi_`V2eiA>l zdSm6;SEm6#T+SpcE8Ro_f2AwxzI z44hfe^WE3!h@W3RDyA_H440cpmYkv*)6m1XazTqw%=E5Xv7^@^^T7Q2wxr+Z2kVYr + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/macos/Runner/Configs/AppInfo.xcconfig b/example/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..dda9752 --- /dev/null +++ b/example/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 = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2025 com.example. All rights reserved. diff --git a/example/macos/Runner/Configs/Debug.xcconfig b/example/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/example/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Release.xcconfig b/example/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/example/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/example/macos/Runner/Configs/Warnings.xcconfig b/example/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/example/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/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..08c3ab1 --- /dev/null +++ b/example/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + com.apple.security.network.client + + + diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/example/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/example/macos/Runner/MainFlutterWindow.swift b/example/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/example/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +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/example/macos/Runner/Release.entitlements b/example/macos/Runner/Release.entitlements new file mode 100644 index 0000000..ee95ab7 --- /dev/null +++ b/example/macos/Runner/Release.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.network.client + + + diff --git a/example/macos/RunnerTests/RunnerTests.swift b/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/example/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +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/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..c608396 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,433 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + audioplayers: + dependency: transitive + description: + name: audioplayers + sha256: "5441fa0ceb8807a5ad701199806510e56afde2b4913d9d17c2f19f2902cf0ae4" + url: "https://pub.dev" + source: hosted + version: "6.5.1" + audioplayers_android: + dependency: transitive + description: + name: audioplayers_android + sha256: "60a6728277228413a85755bd3ffd6fab98f6555608923813ce383b190a360605" + url: "https://pub.dev" + source: hosted + version: "5.2.1" + audioplayers_darwin: + dependency: transitive + description: + name: audioplayers_darwin + sha256: "0811d6924904ca13f9ef90d19081e4a87f7297ddc19fc3d31f60af1aaafee333" + url: "https://pub.dev" + source: hosted + version: "6.3.0" + audioplayers_linux: + dependency: transitive + description: + name: audioplayers_linux + sha256: f75bce1ce864170ef5e6a2c6a61cd3339e1a17ce11e99a25bae4474ea491d001 + url: "https://pub.dev" + source: hosted + version: "4.2.1" + audioplayers_platform_interface: + dependency: transitive + description: + name: audioplayers_platform_interface + sha256: "0e2f6a919ab56d0fec272e801abc07b26ae7f31980f912f24af4748763e5a656" + url: "https://pub.dev" + source: hosted + version: "7.1.1" + audioplayers_web: + dependency: transitive + description: + name: audioplayers_web + sha256: "1c0f17cec68455556775f1e50ca85c40c05c714a99c5eb1d2d57cc17ba5522d7" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + audioplayers_windows: + dependency: transitive + description: + name: audioplayers_windows + sha256: "4048797865105b26d47628e6abb49231ea5de84884160229251f37dfcbe52fd7" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + url: "https://pub.dev" + source: hosted + version: "0.12.18" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + url: "https://pub.dev" + source: hosted + version: "1.17.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e + url: "https://pub.dev" + source: hosted + version: "2.2.22" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pixelify_flutter: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + url: "https://pub.dev" + source: hosted + version: "0.7.8" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: a11b666489b1954e01d992f3d601b1804a33937b5a8fe677bd26b8a9f96f96e8 + url: "https://pub.dev" + source: hosted + version: "4.5.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..996c45d --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: example +description: "Pixelify Flutter Example App" +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: ^3.9.0 + +dependencies: + flutter: + sdk: flutter + pixelify_flutter: + path: ../ + google_fonts: ^6.2.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^6.0.0 + +flutter: + uses-material-design: true diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/lib/pixelify_flutter.dart b/lib/pixelify_flutter.dart index 0a0f42f..47f2568 100644 --- a/lib/pixelify_flutter.dart +++ b/lib/pixelify_flutter.dart @@ -9,6 +9,7 @@ export 'src/wrapper_widgets/pixel_animations.dart'; //Widgets export 'src/widgets/pixel_button.dart'; export 'src/widgets/pixel_future_shimmer.dart'; +export 'src/widgets/pixel_notification_card.dart'; export 'src/widgets/pixel_panel.dart'; export 'src/widgets/pixel_progress_bar.dart'; export 'src/widgets/pixel_shimmer.dart'; @@ -17,3 +18,17 @@ export 'src/widgets/pixel_text_field.dart'; export 'src/widgets/pixel_text.dart'; export 'src/widgets/pixel_toggle.dart'; +//Animations +export 'src/animations/fade_animation.dart'; +export 'src/animations/flicker_animation.dart'; +export 'src/animations/jitter_animation.dart'; +export 'src/animations/wave_animation.dart'; + +//Effects +export 'src/effects/glitch_effect.dart'; +export 'src/effects/noise_effect.dart'; + +//Utils +export 'src/utils/palette.dart'; +export 'src/utils/dithering.dart'; + diff --git a/lib/src/animations/fade_animation.dart b/lib/src/animations/fade_animation.dart index 79d1691..d028786 100644 --- a/lib/src/animations/fade_animation.dart +++ b/lib/src/animations/fade_animation.dart @@ -39,6 +39,7 @@ class FadeAnimation extends StatefulWidget { class _FadeAnimationState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _animation; + bool _isDisposed = false; @override void initState() { @@ -53,6 +54,7 @@ class _FadeAnimationState extends State with SingleTickerProvider _animation = Tween(begin: widget.beginOpacity, end: widget.endOpacity).animate(curvedAnimation) ..addStatusListener((status) { + if (_isDisposed) return; if (status == AnimationStatus.completed) { if (!widget.loop) { widget.onFadeComplete?.call(); @@ -68,7 +70,7 @@ class _FadeAnimationState extends State with SingleTickerProvider if (widget.delay > Duration.zero) { Future.delayed(widget.delay, () { - if (mounted) _controller.forward(); + if (!_isDisposed && mounted) _controller.forward(); }); } else { _controller.forward(); @@ -77,6 +79,7 @@ class _FadeAnimationState extends State with SingleTickerProvider @override void dispose() { + _isDisposed = true; _controller.dispose(); super.dispose(); } diff --git a/lib/src/animations/wave_animation.dart b/lib/src/animations/wave_animation.dart index e69de29..efe569c 100644 --- a/lib/src/animations/wave_animation.dart +++ b/lib/src/animations/wave_animation.dart @@ -0,0 +1,131 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:pixelify_flutter/src/utils/pixel_animation_params.dart'; + +/// Direction for wave animation movement +enum WaveDirection { horizontal, vertical, diagonal } + +/// WaveAnimation: creates a smooth wave-like motion effect. +/// Features include: +/// - Duration and curve for smooth wave transitions +/// - Looping and reversal control +/// - Amplitude control for wave height +/// - Frequency control for wave density +/// - Phase offset for staggered start timing +/// - Direction control (horizontal, vertical, diagonal) +class WaveAnimation extends StatefulWidget { + final Widget child; + final Duration duration; + final bool loop; + final bool reverse; + final Curve curve; + final double phaseOffset; + final double amplitude; + final double frequency; + final WaveDirection direction; + + const WaveAnimation({ + super.key, + required this.child, + this.duration = const Duration(milliseconds: 1000), + this.loop = true, + this.reverse = false, + this.curve = Curves.easeInOut, + this.phaseOffset = 0.0, + this.amplitude = 10.0, + this.frequency = 1.0, + this.direction = WaveDirection.vertical, + }); + + @override + State createState() => _WaveAnimationState(); +} + +class _WaveAnimationState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _animation; + + @override + void initState() { + super.initState(); + + _controller = AnimationController(vsync: this, duration: widget.duration); + + final curved = CurvedAnimation(parent: _controller, curve: widget.curve); + + _animation = Tween(begin: 0.0, end: 1.0).animate(curved) + ..addStatusListener((status) { + if (status == AnimationStatus.completed) { + if (!widget.loop) { + _controller.stop(); + } else if (widget.reverse) { + _controller.reverse(); + } else { + _controller.forward(from: 0.0); + } + } else if (status == AnimationStatus.dismissed && widget.loop && widget.reverse) { + _controller.forward(); + } + }); + + _controller.forward(from: widget.phaseOffset); + } + + Offset _calculateOffset(double t) { + final waveValue = sin(2 * pi * widget.frequency * t) * widget.amplitude; + + switch (widget.direction) { + case WaveDirection.horizontal: + return Offset(waveValue, 0); + case WaveDirection.vertical: + return Offset(0, waveValue); + case WaveDirection.diagonal: + return Offset(waveValue * 0.7, waveValue * 0.7); + } + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animation, + child: widget.child, + builder: (context, child) { + final offset = _calculateOffset(_animation.value); + return Transform.translate( + offset: offset, + child: child, + ); + }, + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} + + +class WaveAnimationParams extends PixelAnimationParams { + final Duration duration; + final bool loop; + final bool reverse; + final Curve curve; + final double phaseOffset; + final double amplitude; + final double frequency; + final WaveDirection direction; + + const WaveAnimationParams({ + this.duration = const Duration(milliseconds: 1000), + this.loop = true, + this.reverse = false, + this.curve = Curves.easeInOut, + this.phaseOffset = 0.0, + this.amplitude = 10.0, + this.frequency = 1.0, + this.direction = WaveDirection.vertical, + }); +} diff --git a/lib/src/effects/noise_effect.dart b/lib/src/effects/noise_effect.dart index 3d45cb8..07b1ea3 100644 --- a/lib/src/effects/noise_effect.dart +++ b/lib/src/effects/noise_effect.dart @@ -32,13 +32,12 @@ class NoiseEffect extends StatefulWidget { class _NoiseEffectState extends State with SingleTickerProviderStateMixin { late final AnimationController _controller; - late List _points; + List _points = []; final Random _random = Random(); @override void initState() { super.initState(); - _generateNoisePoints(); _controller = AnimationController(vsync: this, duration: widget.animationSpeed); @@ -56,15 +55,18 @@ class _NoiseEffectState extends State with SingleTickerProviderStat void _generateNoisePoints() { WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; final Size? size = context.size; if (size == null || size.width == 0 || size.height == 0) return; final Rect clipRect = widget.region ?? Offset.zero & size; - _points = List.generate(widget.density, (index) { - final dx = _random.nextDouble() * clipRect.width + clipRect.left; - final dy = _random.nextDouble() * clipRect.height + clipRect.top; - return Offset(dx, dy); + setState(() { + _points = List.generate(widget.density, (index) { + final dx = _random.nextDouble() * clipRect.width + clipRect.left; + final dy = _random.nextDouble() * clipRect.height + clipRect.top; + return Offset(dx, dy); + }); }); }); } diff --git a/lib/src/widgets/pixel_notification_card.dart b/lib/src/widgets/pixel_notification_card.dart new file mode 100644 index 0000000..25fd0cd --- /dev/null +++ b/lib/src/widgets/pixel_notification_card.dart @@ -0,0 +1,380 @@ +import 'package:flutter/material.dart'; + +/// Pixel-art style notification card with avatar, title and action text. +/// Inspired by retro game UI notifications. +/// +/// Features: +/// - Pixel-perfect raised border style +/// - Optional avatar with pixel border +/// - Title and action text in pixel font +/// - Customizable colors and sizes +/// - Optional press animation +/// - Scanline overlay option +class PixelNotificationCard extends StatefulWidget { + final String title; + final String action; + final Widget? avatar; + final VoidCallback? onTap; + final Color backgroundColor; + final Color borderColor; + final Color highlightColor; + final Color shadowColor; + final Color titleColor; + final Color actionColor; + final double borderWidth; + final double width; + final double? height; + final EdgeInsets padding; + final TextStyle? titleStyle; + final TextStyle? actionStyle; + final bool showScanlines; + final double avatarSize; + final bool enablePressAnimation; + + const PixelNotificationCard({ + super.key, + required this.title, + required this.action, + this.avatar, + this.onTap, + this.backgroundColor = const Color(0xFF2A2D3A), + this.borderColor = const Color(0xFF3D4155), + this.highlightColor = const Color(0xFF4A4E65), + this.shadowColor = const Color(0xFF1A1C24), + this.titleColor = Colors.white, + this.actionColor = const Color(0xFF8A8E9E), + this.borderWidth = 3.0, + this.width = 280, + this.height, + this.padding = const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + this.titleStyle, + this.actionStyle, + this.showScanlines = false, + this.avatarSize = 44, + this.enablePressAnimation = true, + }); + + @override + State createState() => _PixelNotificationCardState(); +} + +class _PixelNotificationCardState extends State { + bool _isPressed = false; + + @override + Widget build(BuildContext context) { + final defaultTitleStyle = widget.titleStyle ?? + TextStyle( + fontFamily: 'PressStart2P', + fontSize: 11, + color: widget.titleColor, + letterSpacing: 0.5, + height: 1.2, + ); + + final defaultActionStyle = widget.actionStyle ?? + TextStyle( + fontFamily: 'PressStart2P', + fontSize: 8, + color: widget.actionColor, + letterSpacing: 0.5, + height: 1.2, + ); + + return GestureDetector( + onTapDown: widget.onTap != null && widget.enablePressAnimation + ? (_) => setState(() => _isPressed = true) + : null, + onTapUp: widget.onTap != null + ? (_) { + setState(() => _isPressed = false); + widget.onTap?.call(); + } + : null, + onTapCancel: widget.onTap != null && widget.enablePressAnimation + ? () => setState(() => _isPressed = false) + : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + width: widget.width, + height: widget.height, + transform: _isPressed ? Matrix4.translationValues(1, 1, 0) : Matrix4.identity(), + child: CustomPaint( + painter: _PixelBorderPainter( + backgroundColor: widget.backgroundColor, + borderColor: widget.borderColor, + highlightColor: widget.highlightColor, + shadowColor: widget.shadowColor, + borderWidth: widget.borderWidth, + isPressed: _isPressed, + ), + child: Stack( + children: [ + Padding( + padding: widget.padding.add(EdgeInsets.all(widget.borderWidth)), + child: Row( + children: [ + if (widget.avatar != null) ...[ + _buildAvatar(), + const SizedBox(width: 10), + ], + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + widget.title.toUpperCase(), + style: defaultTitleStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + widget.action.toUpperCase(), + style: defaultActionStyle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ), + if (widget.showScanlines) + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: _ScanlinePainter(), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildAvatar() { + return CustomPaint( + painter: _PixelAvatarBorderPainter( + borderColor: widget.borderColor, + shadowColor: widget.shadowColor, + backgroundColor: widget.backgroundColor, + borderWidth: 2, + ), + child: Container( + width: widget.avatarSize, + height: widget.avatarSize, + padding: const EdgeInsets.all(4), + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.avatarSize / 2), + child: widget.avatar ?? + Container( + color: widget.borderColor, + child: Icon( + Icons.person, + color: widget.shadowColor, + size: widget.avatarSize * 0.6, + ), + ), + ), + ), + ); + } +} + +/// Custom painter for pixel-art style raised border effect +class _PixelBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color highlightColor; + final Color shadowColor; + final double borderWidth; + final bool isPressed; + + _PixelBorderPainter({ + required this.backgroundColor, + required this.borderColor, + required this.highlightColor, + required this.shadowColor, + required this.borderWidth, + required this.isPressed, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..style = PaintingStyle.fill; + + // Draw outer shadow (bottom-right) + paint.color = shadowColor; + canvas.drawRect( + Rect.fromLTWH(borderWidth, borderWidth, size.width - borderWidth, size.height - borderWidth), + paint, + ); + + // Draw main border + paint.color = borderColor; + canvas.drawRect( + Rect.fromLTWH(0, 0, size.width - borderWidth, size.height - borderWidth), + paint, + ); + + // Draw inner highlight (top-left edges when not pressed) + if (!isPressed) { + paint.color = highlightColor; + // Top edge + canvas.drawRect( + Rect.fromLTWH(borderWidth, borderWidth, size.width - borderWidth * 3, borderWidth), + paint, + ); + // Left edge + canvas.drawRect( + Rect.fromLTWH(borderWidth, borderWidth * 2, borderWidth, size.height - borderWidth * 4), + paint, + ); + } + + // Draw background + paint.color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH( + borderWidth * 2, + borderWidth * 2, + size.width - borderWidth * 4, + size.height - borderWidth * 4, + ), + paint, + ); + + // Draw inner shadow (bottom-right edges inside the card) + paint.color = shadowColor.withOpacity(0.5); + // Bottom inner edge + canvas.drawRect( + Rect.fromLTWH( + borderWidth * 2, + size.height - borderWidth * 3, + size.width - borderWidth * 4, + borderWidth * 0.5, + ), + paint, + ); + // Right inner edge + canvas.drawRect( + Rect.fromLTWH( + size.width - borderWidth * 3, + borderWidth * 2, + borderWidth * 0.5, + size.height - borderWidth * 4, + ), + paint, + ); + } + + @override + bool shouldRepaint(covariant _PixelBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.highlightColor != highlightColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.borderWidth != borderWidth || + oldDelegate.isPressed != isPressed; + } +} + +/// Custom painter for circular avatar with pixel border +class _PixelAvatarBorderPainter extends CustomPainter { + final Color borderColor; + final Color shadowColor; + final Color backgroundColor; + final double borderWidth; + + _PixelAvatarBorderPainter({ + required this.borderColor, + required this.shadowColor, + required this.backgroundColor, + required this.borderWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = size.width / 2; + final paint = Paint()..style = PaintingStyle.fill; + + // Draw shadow circle (offset) + paint.color = shadowColor; + canvas.drawCircle( + Offset(center.dx + borderWidth, center.dy + borderWidth), + radius - borderWidth, + paint, + ); + + // Draw border circle + paint.color = borderColor; + canvas.drawCircle(center, radius - borderWidth, paint); + + // Draw inner circle + paint.color = backgroundColor; + canvas.drawCircle(center, radius - borderWidth * 2.5, paint); + } + + @override + bool shouldRepaint(covariant _PixelAvatarBorderPainter oldDelegate) { + return oldDelegate.borderColor != borderColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderWidth != borderWidth; + } +} + +/// Simple scanline painter for overlay effect +class _ScanlinePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.black.withOpacity(0.1) + ..strokeWidth = 1; + + for (double y = 0; y < size.height; y += 3) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + +/// Default pixel avatar with person icon +class PixelAvatar extends StatelessWidget { + final Color backgroundColor; + final Color iconColor; + final double size; + + const PixelAvatar({ + super.key, + this.backgroundColor = const Color(0xFF3D4155), + this.iconColor = const Color(0xFF5A5E72), + this.size = 36, + }); + + @override + Widget build(BuildContext context) { + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: backgroundColor, + shape: BoxShape.circle, + ), + child: Icon( + Icons.person, + color: iconColor, + size: size * 0.6, + ), + ); + } +} diff --git a/lib/src/widgets/pixel_progress_bar.dart b/lib/src/widgets/pixel_progress_bar.dart index 1c2aa86..faf7d8a 100644 --- a/lib/src/widgets/pixel_progress_bar.dart +++ b/lib/src/widgets/pixel_progress_bar.dart @@ -151,8 +151,11 @@ class _PixelProgressBarState extends State } Widget _buildIconFilledBar() { - final double totalWidth = widget.segmentWidth * widget.segments + (widget.segments - 1) * 2; - double fillWidth = totalWidth * _animation.value; + // Account for margins (2px per segment) in total width calculation + // Plus 2px for the outer border (1px each side) + final double innerWidth = widget.segments * (widget.segmentWidth + 2); + final double totalWidth = innerWidth + 2; // +2 for border + double fillWidth = innerWidth * _animation.value; int fillCount = (widget.segments * _animation.value).floor(); @@ -164,6 +167,7 @@ class _PixelProgressBarState extends State border: Border.all(color: Colors.black, width: 1), ), child: Stack( + clipBehavior: Clip.hardEdge, children: [ Row( mainAxisSize: MainAxisSize.min, @@ -171,7 +175,7 @@ class _PixelProgressBarState extends State bool filled = index < fillCount; return Container( width: widget.segmentWidth, - height: widget.height, + height: widget.height - 2, // Account for outer border margin: const EdgeInsets.symmetric(horizontal: 1), decoration: BoxDecoration( color: filled ? Colors.transparent : widget.backgroundColor, diff --git a/lib/src/widgets/pixel_text.dart b/lib/src/widgets/pixel_text.dart index ffae963..a6688c8 100644 --- a/lib/src/widgets/pixel_text.dart +++ b/lib/src/widgets/pixel_text.dart @@ -52,7 +52,7 @@ class _PixelTextState extends State with SingleTickerProviderStateMix Offset _glitchOffset1 = Offset.zero; Offset _glitchOffset2 = Offset.zero; - late Timer? _effectTimer; + Timer? _effectTimer; final Random _random = Random(); @override diff --git a/pubspec.yaml b/pubspec.yaml index a949e46..87e58c6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: flutter: sdk: flutter audioplayers: ^6.5.0 + google_fonts: ^6.2.1 dev_dependencies: flutter_test: diff --git a/shaders/bloom.frag b/shaders/bloom.frag index c708351..a349e84 100644 --- a/shaders/bloom.frag +++ b/shaders/bloom.frag @@ -12,7 +12,7 @@ varying vec2 v_texCoord; vec4 blur(in vec2 uv) { vec4 sum = vec4(0.0); float offsets[5] = float[](0.0, 1.3846, 3.2308, -1.3846, -3.2308); - float weights[1] = float[](0.2270, 0.3162, 0.0703, 0.3162, 0.0703); + float weights[5] = float[](0.2270, 0.3162, 0.0703, 0.3162, 0.0703); for(int i = 0; i < 5; i++) { sum += texture2D(u_texture, uv + vec2(offsets[i] * u_intensity / 512.0, 0.0)) * weights[i]; diff --git a/shaders/pixalate.frag b/shaders/pixelate.frag similarity index 100% rename from shaders/pixalate.frag rename to shaders/pixelate.frag diff --git a/test/animations_test.dart b/test/animations_test.dart new file mode 100644 index 0000000..18afff1 --- /dev/null +++ b/test/animations_test.dart @@ -0,0 +1,382 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pixelify_flutter/src/animations/fade_animation.dart'; +import 'package:pixelify_flutter/src/animations/flicker_animation.dart'; +import 'package:pixelify_flutter/src/animations/jitter_animation.dart'; +import 'package:pixelify_flutter/src/animations/wave_animation.dart'; + +void main() { + group('FadeAnimation', () { + testWidgets('renders child', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FadeAnimation( + child: Text('FADE'), + ), + ), + ), + ); + + expect(find.text('FADE'), findsOneWidget); + }); + + testWidgets('animates opacity', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FadeAnimation( + duration: Duration(milliseconds: 500), + beginOpacity: 0.0, + endOpacity: 1.0, + loop: false, + child: Text('FADE'), + ), + ), + ), + ); + + // Initial state + await tester.pump(); + + // Mid-animation + await tester.pump(const Duration(milliseconds: 250)); + + // End animation + await tester.pumpAndSettle(); + + expect(find.byType(FadeAnimation), findsOneWidget); + }); + + testWidgets('respects delay', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FadeAnimation( + delay: Duration(milliseconds: 200), + duration: Duration(milliseconds: 300), + loop: false, + child: Text('DELAYED'), + ), + ), + ), + ); + + // Pump past the delay and animation duration to avoid pending timer + await tester.pump(const Duration(milliseconds: 600)); + expect(find.text('DELAYED'), findsOneWidget); + }); + + testWidgets('loops when enabled', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FadeAnimation( + duration: Duration(milliseconds: 200), + loop: true, + reverse: true, + child: Text('LOOP'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 200)); + await tester.pump(const Duration(milliseconds: 200)); + + expect(find.byType(FadeAnimation), findsOneWidget); + }); + + testWidgets('calls onFadeComplete', (tester) async { + bool completed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: FadeAnimation( + duration: const Duration(milliseconds: 200), + loop: false, + onFadeComplete: () => completed = true, + child: const Text('COMPLETE'), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + expect(completed, true); + }); + + test('FadeAnimationParams has correct defaults', () { + const params = FadeAnimationParams(); + expect(params.duration, const Duration(milliseconds: 600)); + expect(params.curve, Curves.linear); + expect(params.loop, true); + expect(params.reverse, true); + expect(params.delay, Duration.zero); + expect(params.beginOpacity, 0.0); + expect(params.endOpacity, 1.0); + expect(params.onFadeComplete, isNull); + }); + }); + + group('FlickerAnimation', () { + testWidgets('renders child', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FlickerAnimation( + child: Text('FLICKER'), + ), + ), + ), + ); + + expect(find.text('FLICKER'), findsOneWidget); + }); + + testWidgets('animates opacity randomly', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FlickerAnimation( + duration: Duration(milliseconds: 100), + minOpacity: 0.5, + maxOpacity: 1.0, + child: Text('FLICKER'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + await tester.pump(const Duration(milliseconds: 50)); + + expect(find.byType(FlickerAnimation), findsOneWidget); + }); + + testWidgets('respects phase offset', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: FlickerAnimation( + phaseOffset: 0.5, + child: Text('OFFSET'), + ), + ), + ), + ); + + expect(find.text('OFFSET'), findsOneWidget); + }); + + test('FlickerAnimationParams has correct defaults', () { + const params = FlickerAnimationParams(); + expect(params.duration, const Duration(milliseconds: 500)); + expect(params.loop, true); + expect(params.reverse, true); + expect(params.delay, Duration.zero); + expect(params.minOpacity, 0.8); + expect(params.maxOpacity, 1.0); + expect(params.randomness, 0.3); + expect(params.curve, Curves.linear); + expect(params.colorShiftAmount, 0.0); + expect(params.phaseOffset, 0.0); + }); + }); + + group('JitterAnimation', () { + testWidgets('renders child', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: JitterAnimation( + child: Text('JITTER'), + ), + ), + ), + ); + + expect(find.text('JITTER'), findsOneWidget); + }); + + testWidgets('applies transform', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: JitterAnimation( + strengthX: 5.0, + strengthY: 5.0, + duration: Duration(milliseconds: 100), + child: Text('SHAKE'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + + // Find Transform that is descendant of JitterAnimation + expect( + find.descendant( + of: find.byType(JitterAnimation), + matching: find.byType(Transform), + ), + findsOneWidget, + ); + }); + + testWidgets('respects decay option', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: JitterAnimation( + decay: true, + duration: Duration(milliseconds: 200), + child: Text('DECAY'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 100)); + await tester.pump(const Duration(milliseconds: 100)); + + expect(find.text('DECAY'), findsOneWidget); + }); + + test('JitterAnimationParams has correct defaults', () { + const params = JitterAnimationParams(); + expect(params.strengthX, 2.0); + expect(params.strengthY, 2.0); + expect(params.duration, const Duration(milliseconds: 120)); + expect(params.randomness, 1.0); + expect(params.decay, false); + expect(params.loop, true); + expect(params.reverse, true); + expect(params.curve, Curves.linear); + expect(params.phaseOffset, 0.0); + expect(params.delay, isNull); + }); + }); + + group('WaveAnimation', () { + testWidgets('renders child', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: WaveAnimation( + child: Text('WAVE'), + ), + ), + ), + ); + + expect(find.text('WAVE'), findsOneWidget); + }); + + testWidgets('applies horizontal direction', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: WaveAnimation( + direction: WaveDirection.horizontal, + amplitude: 10.0, + frequency: 1.0, + child: Text('HORIZONTAL'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 500)); + + // Find Transform that is descendant of WaveAnimation + expect( + find.descendant( + of: find.byType(WaveAnimation), + matching: find.byType(Transform), + ), + findsOneWidget, + ); + }); + + testWidgets('applies vertical direction', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: WaveAnimation( + direction: WaveDirection.vertical, + amplitude: 10.0, + child: Text('VERTICAL'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.text('VERTICAL'), findsOneWidget); + }); + + testWidgets('applies diagonal direction', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: WaveAnimation( + direction: WaveDirection.diagonal, + amplitude: 10.0, + child: Text('DIAGONAL'), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 500)); + + expect(find.text('DIAGONAL'), findsOneWidget); + }); + + testWidgets('respects loop setting', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: WaveAnimation( + loop: false, + duration: Duration(milliseconds: 200), + child: Text('NO LOOP'), + ), + ), + ), + ); + + await tester.pumpAndSettle(); + + expect(find.text('NO LOOP'), findsOneWidget); + }); + + testWidgets('respects phase offset', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: WaveAnimation( + phaseOffset: 0.5, + child: Text('OFFSET'), + ), + ), + ), + ); + + expect(find.text('OFFSET'), findsOneWidget); + }); + + test('WaveAnimationParams has correct defaults', () { + const params = WaveAnimationParams(); + expect(params.duration, const Duration(milliseconds: 1000)); + expect(params.loop, true); + expect(params.reverse, false); + expect(params.amplitude, 10.0); + expect(params.frequency, 1.0); + expect(params.direction, WaveDirection.vertical); + }); + }); +} diff --git a/test/effects_test.dart b/test/effects_test.dart new file mode 100644 index 0000000..f7bdf74 --- /dev/null +++ b/test/effects_test.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pixelify_flutter/src/effects/glitch_effect.dart'; +import 'package:pixelify_flutter/src/effects/noise_effect.dart'; + +void main() { + group('GlitchEffect', () { + testWidgets('renders child', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: GlitchEffect( + child: Container( + color: Colors.blue, + child: const Text('GLITCH'), + ), + ), + ), + ), + ), + ); + + expect(find.text('GLITCH'), findsWidgets); + }); + + testWidgets('applies intensity', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: GlitchEffect( + intensity: 0.8, + child: Container(color: Colors.red), + ), + ), + ), + ), + ); + + expect(find.byType(GlitchEffect), findsOneWidget); + }); + + testWidgets('respects frequency', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: GlitchEffect( + frequency: const Duration(milliseconds: 100), + child: Container(color: Colors.green), + ), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 150)); + + expect(find.byType(GlitchEffect), findsOneWidget); + }); + + testWidgets('shows noise overlay when enabled', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: GlitchEffect( + enableNoise: true, + child: Container(color: Colors.blue), + ), + ), + ), + ), + ); + + expect(find.byType(CustomPaint), findsWidgets); + }); + + testWidgets('hides noise overlay when disabled', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: GlitchEffect( + enableNoise: false, + enableScanLines: false, + child: Container(color: Colors.blue), + ), + ), + ), + ), + ); + + expect(find.byType(GlitchEffect), findsOneWidget); + }); + + testWidgets('applies custom glitch colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: GlitchEffect( + glitchColors: const [Colors.purple, Colors.yellow, Colors.cyan], + child: Container(color: Colors.white), + ), + ), + ), + ), + ); + + expect(find.byType(GlitchEffect), findsOneWidget); + }); + + test('GlitchEffectParams has correct defaults', () { + const params = GlitchEffectParams(); + expect(params.intensity, 0.3); + expect(params.frequency, const Duration(milliseconds: 200)); + expect(params.glitchColors, const [Colors.red, Colors.green, Colors.blue]); + expect(params.enableNoise, true); + expect(params.enableScanLines, true); + expect(params.scanLineOpacity, 0.2); + expect(params.noiseIntensity, 0.1); + }); + }); + + group('NoiseEffect', () { + testWidgets('renders as overlay', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Stack( + children: [ + Container( + width: 200, + height: 100, + color: Colors.blue, + child: const Text('NOISE'), + ), + const SizedBox( + width: 200, + height: 100, + child: NoiseEffect(), + ), + ], + ), + ), + ), + ); + + expect(find.text('NOISE'), findsOneWidget); + expect(find.byType(NoiseEffect), findsOneWidget); + }); + + testWidgets('applies intensity', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: NoiseEffect( + intensity: 0.5, + ), + ), + ), + ), + ); + + expect(find.byType(NoiseEffect), findsOneWidget); + }); + + testWidgets('respects density parameter', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: NoiseEffect( + density: 100, + ), + ), + ), + ), + ); + + expect(find.byType(NoiseEffect), findsOneWidget); + }); + + testWidgets('animates over time', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: NoiseEffect( + animationSpeed: const Duration(milliseconds: 100), + ), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 50)); + await tester.pump(const Duration(milliseconds: 50)); + + expect(find.byType(NoiseEffect), findsOneWidget); + }); + + testWidgets('applies custom dot size', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: NoiseEffect( + dotSize: 3.0, + ), + ), + ), + ), + ); + + expect(find.byType(NoiseEffect), findsOneWidget); + }); + + testWidgets('applies custom color', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + width: 200, + height: 100, + child: NoiseEffect( + color: Colors.red, + ), + ), + ), + ), + ); + + expect(find.byType(NoiseEffect), findsOneWidget); + }); + + test('NoiseEffectParams has correct defaults', () { + const params = NoiseEffectParams(); + expect(params.intensity, 0.2); + expect(params.density, 2000); + expect(params.animationSpeed, const Duration(milliseconds: 100)); + expect(params.dotSize, 1.5); + expect(params.color, Colors.white); + expect(params.animate, true); + expect(params.blendMode, BlendMode.screen); + expect(params.region, isNull); + }); + }); +} diff --git a/test/pixelify_flutter_test.dart b/test/pixelify_flutter_test.dart index da69dc8..31e17a4 100644 --- a/test/pixelify_flutter_test.dart +++ b/test/pixelify_flutter_test.dart @@ -1,12 +1,694 @@ +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; - import 'package:pixelify_flutter/pixelify_flutter.dart'; void main() { - test('adds one to input values', () { - final calculator = Calculator(); - expect(calculator.addOne(2), 3); - expect(calculator.addOne(-7), -6); - expect(calculator.addOne(0), 1); + group('PixelTheme', () { + testWidgets('provides default values', (tester) async { + late PixelTheme theme; + await tester.pumpWidget( + MaterialApp( + home: PixelTheme( + child: Builder( + builder: (context) { + theme = PixelTheme.of(context); + return const SizedBox(); + }, + ), + ), + ), + ); + + expect(theme.accentColor, Colors.cyan); + expect(theme.pixelScale, 1.0); + expect(theme.enableShaders, true); + }); + + testWidgets('provides custom values', (tester) async { + late PixelTheme theme; + await tester.pumpWidget( + MaterialApp( + home: PixelTheme( + accentColor: Colors.red, + pixelScale: 2.0, + enableShaders: false, + child: Builder( + builder: (context) { + theme = PixelTheme.of(context); + return const SizedBox(); + }, + ), + ), + ), + ); + + expect(theme.accentColor, Colors.red); + expect(theme.pixelScale, 2.0); + expect(theme.enableShaders, false); + }); + + testWidgets('returns fallback when no ancestor', (tester) async { + late PixelTheme theme; + await tester.pumpWidget( + MaterialApp( + home: Builder( + builder: (context) { + theme = PixelTheme.of(context); + return const SizedBox(); + }, + ), + ), + ); + + expect(theme.accentColor, Colors.cyan); + }); + }); + + group('PixelButton', () { + testWidgets('renders with label', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelButton(label: 'TEST'), + ), + ), + ); + + expect(find.text('TEST'), findsOneWidget); + }); + + testWidgets('calls onPressed when tapped', (tester) async { + bool pressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelButton( + label: 'TAP ME', + onPressed: () => pressed = true, + // Disable animations for testing + enableGlowAnimation: false, + enableScanlineAnimation: false, + ), + ), + ), + ); + + // Tap the button itself, not the text + await tester.tap(find.byType(PixelButton)); + await tester.pump(); + + expect(pressed, true); + }); + + testWidgets('does not call onPressed when disabled', (tester) async { + bool pressed = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelButton( + label: 'DISABLED', + enabled: false, + onPressed: () => pressed = true, + enableGlowAnimation: false, + enableScanlineAnimation: false, + ), + ), + ), + ); + + await tester.tap(find.byType(PixelButton)); + await tester.pump(); + + expect(pressed, false); + }); + + testWidgets('applies custom colors', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelButton( + label: 'COLORED', + color: Colors.red, + outlineColor: Colors.blue, + ), + ), + ), + ); + + expect(find.text('COLORED'), findsOneWidget); + }); + + testWidgets('applies custom text style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelButton( + label: 'STYLED', + textStyle: TextStyle(fontSize: 20, color: Colors.yellow), + ), + ), + ), + ); + + final text = tester.widget(find.text('STYLED')); + expect(text.style?.fontSize, 20); + }); + + testWidgets('has correct pixel edge thickness', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelButton( + label: 'THICK', + pixelEdgeThickness: 5.0, + ), + ), + ), + ); + + expect(find.text('THICK'), findsOneWidget); + }); + }); + + group('PixelText', () { + testWidgets('renders basic text', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'HELLO', + style: TextStyle(fontSize: 16), + ), + ), + ), + ); + + expect(find.text('HELLO'), findsOneWidget); + }); + + testWidgets('renders with flicker effect', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'FLICKER', + style: TextStyle(fontSize: 16), + effect: PixelTextEffect.flicker, + ), + ), + ), + ); + + expect(find.text('FLICKER'), findsOneWidget); + expect(find.byType(Opacity), findsOneWidget); + }); + + testWidgets('renders with glitch effect', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'GLITCH', + style: TextStyle(fontSize: 16), + effect: PixelTextEffect.glitch, + ), + ), + ), + ); + + // Glitch uses Stack with 3 text layers - find Stack within PixelText + expect( + find.descendant( + of: find.byType(PixelText), + matching: find.byType(Stack), + ), + findsOneWidget, + ); + }); + + testWidgets('renders with scanline effect', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'SCAN', + style: TextStyle(fontSize: 16), + effect: PixelTextEffect.scanline, + ), + ), + ), + ); + + expect(find.byType(ScanlineOverlay), findsOneWidget); + }); + + testWidgets('renders with pixelate effect', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'PIXEL', + style: TextStyle(fontSize: 16), + effect: PixelTextEffect.pixelate, + pixelScale: 2.0, + ), + ), + ), + ); + + expect(find.text('PIXEL'), findsOneWidget); + }); + + testWidgets('renders with palette colors', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'COLORS', + style: TextStyle(fontSize: 16), + enablePaletteColors: true, + palette: [Colors.red, Colors.green, Colors.blue], + ), + ), + ), + ); + + expect(find.byType(RichText), findsOneWidget); + }); + + testWidgets('asserts pixelScale >= 1.0', (tester) async { + expect( + () => PixelText( + text: 'TEST', + style: const TextStyle(fontSize: 16), + pixelScale: 0.5, + ), + throwsAssertionError, + ); + }); + }); + + group('PixelPanel', () { + testWidgets('renders with default style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelPanel( + child: Text('PANEL'), + ), + ), + ), + ); + + expect(find.text('PANEL'), findsOneWidget); + }); + + testWidgets('renders with pixelOutline style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelPanel( + style: PixelPanelStyle.pixelOutline, + child: Text('OUTLINE'), + ), + ), + ), + ); + + expect(find.text('OUTLINE'), findsOneWidget); + }); + + testWidgets('renders with glowingBorder style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelPanel( + style: PixelPanelStyle.glowingBorder, + borderColor: Colors.cyan, + child: Text('GLOW'), + ), + ), + ), + ); + + expect(find.text('GLOW'), findsOneWidget); + }); + + testWidgets('renders with paperGrain style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelPanel( + style: PixelPanelStyle.paperGrain, + child: Text('PAPER'), + ), + ), + ), + ); + + expect(find.text('PAPER'), findsOneWidget); + }); + + testWidgets('renders with oldScreenCRT style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelPanel( + style: PixelPanelStyle.oldScreenCRT, + child: Text('CRT'), + ), + ), + ), + ); + + expect(find.text('CRT'), findsOneWidget); + }); + + testWidgets('applies custom dimensions', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelPanel( + width: 400, + height: 300, + child: Text('SIZED'), + ), + ), + ), + ); + + final container = tester.widget( + find.descendant( + of: find.byType(PixelPanel), + matching: find.byType(Container).first, + ), + ); + expect(container.constraints?.maxWidth, 400); + expect(container.constraints?.maxHeight, 300); + }); + }); + + group('PixelSlider', () { + testWidgets('renders with initial value', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelSlider( + value: 0.5, + onChanged: (_) {}, + ), + ), + ), + ); + + expect(find.byType(PixelSlider), findsOneWidget); + }); + + testWidgets('calls onChanged on drag', (tester) async { + double value = 0.5; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: PixelSlider( + value: value, + onChanged: (v) => value = v, + ), + ), + ), + ), + ), + ); + + await tester.drag(find.byType(PixelSlider), const Offset(50, 0)); + await tester.pumpAndSettle(); + + expect(value, isNot(0.5)); + }); + + testWidgets('respects divisions', (tester) async { + double value = 0.0; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: PixelSlider( + value: value, + onChanged: (v) => value = v, + divisions: 4, + ), + ), + ), + ), + ), + ); + + expect(find.byType(PixelSlider), findsOneWidget); + }); + + testWidgets('applies custom colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelSlider( + value: 0.5, + onChanged: (_) {}, + activeColor: Colors.red, + inactiveColor: Colors.blue, + ), + ), + ), + ); + + expect(find.byType(PixelSlider), findsOneWidget); + }); + }); + + group('PixelToggle', () { + testWidgets('renders with initial value', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelToggle( + value: true, + onChanged: (_) {}, + ), + ), + ), + ); + + expect(find.byType(PixelToggle), findsOneWidget); + }); + + testWidgets('toggles on tap', (tester) async { + bool value = false; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelToggle( + value: value, + onChanged: (v) => value = v, + ), + ), + ), + ); + + await tester.tap(find.byType(PixelToggle)); + await tester.pumpAndSettle(); + + expect(value, true); + }); + + testWidgets('renders with blinking effect', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelToggle( + value: true, + onChanged: (_) {}, + blinking: true, + ), + ), + ), + ); + + expect(find.byType(PixelToggle), findsOneWidget); + }); + + testWidgets('renders with flip animation', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelToggle( + value: true, + onChanged: (_) {}, + flipAnimation: true, + ), + ), + ), + ); + + expect(find.byType(PixelToggle), findsOneWidget); + }); + + testWidgets('applies custom colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelToggle( + value: true, + onChanged: (_) {}, + onColor: Colors.green, + offColor: Colors.red, + ), + ), + ), + ); + + expect(find.byType(PixelToggle), findsOneWidget); + }); + }); + + group('PixelProgressBar', () { + testWidgets('renders segmented style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelProgressBar( + progress: 0.5, + style: PixelProgressBarStyle.segmented, + ), + ), + ), + ); + + expect(find.byType(PixelProgressBar), findsOneWidget); + }); + + testWidgets('renders smooth style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelProgressBar( + progress: 0.5, + style: PixelProgressBarStyle.smooth, + ), + ), + ), + ); + + expect(find.byType(PixelProgressBar), findsOneWidget); + }); + + testWidgets('renders iconFilled style', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelProgressBar( + progress: 0.5, + style: PixelProgressBarStyle.iconFilled, + icon: Icon(Icons.star), + ), + ), + ), + ); + + expect(find.byType(PixelProgressBar), findsOneWidget); + }); + + testWidgets('animates progress changes', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelProgressBar( + progress: 0.0, + fillAnimationDuration: Duration(milliseconds: 500), + ), + ), + ), + ); + + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelProgressBar( + progress: 1.0, + fillAnimationDuration: Duration(milliseconds: 500), + ), + ), + ), + ); + + await tester.pump(const Duration(milliseconds: 250)); + await tester.pumpAndSettle(); + + expect(find.byType(PixelProgressBar), findsOneWidget); + }); + + testWidgets('applies custom segment count', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelProgressBar( + progress: 0.5, + segments: 10, + ), + ), + ), + ); + + expect(find.byType(PixelProgressBar), findsOneWidget); + }); + }); + + group('ScanlineOverlay', () { + testWidgets('renders over child', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ScanlineOverlay( + child: Text('CONTENT'), + ), + ), + ), + ); + + expect(find.text('CONTENT'), findsOneWidget); + expect(find.byType(ScanlineOverlay), findsOneWidget); + }); + + testWidgets('applies custom opacity', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ScanlineOverlay( + opacity: 0.5, + child: Text('CONTENT'), + ), + ), + ), + ); + + expect(find.byType(ScanlineOverlay), findsOneWidget); + }); + + testWidgets('applies custom spacing', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ScanlineOverlay( + spacing: 8.0, + child: Text('CONTENT'), + ), + ), + ), + ); + + expect(find.byType(ScanlineOverlay), findsOneWidget); + }); }); } diff --git a/test/utils_test.dart b/test/utils_test.dart new file mode 100644 index 0000000..3a355d3 --- /dev/null +++ b/test/utils_test.dart @@ -0,0 +1,226 @@ +import 'dart:typed_data'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:pixelify_flutter/src/utils/palette.dart'; +import 'package:pixelify_flutter/src/utils/dithering.dart'; + +void main() { + group('Palette', () { + test('creates custom palette', () { + final palette = Palette([Colors.red, Colors.green, Colors.blue]); + expect(palette.colors.length, 3); + }); + + test('gameboy palette has 4 colors', () { + expect(Palette.gameboy.colors.length, 4); + }); + + test('gameboy palette has correct colors', () { + final colors = Palette.gameboy.colors; + expect(colors[0], const Color(0xFF0F380F)); + expect(colors[1], const Color(0xFF306230)); + expect(colors[2], const Color(0xFF8BAC0F)); + expect(colors[3], const Color(0xFF9BBC0F)); + }); + + test('nearest returns exact match', () { + final palette = Palette([Colors.red, Colors.green, Colors.blue]); + expect(palette.nearest(Colors.red), Colors.red); + expect(palette.nearest(Colors.green), Colors.green); + expect(palette.nearest(Colors.blue), Colors.blue); + }); + + test('nearest returns closest color', () { + final palette = Palette([Colors.black, Colors.white]); + + // Dark gray should be closer to black + final darkGray = Color(0xFF333333); + expect(palette.nearest(darkGray), Colors.black); + + // Light gray should be closer to white + final lightGray = Color(0xFFCCCCCC); + expect(palette.nearest(lightGray), Colors.white); + }); + + test('nearest handles mid-range colors', () { + final palette = Palette([ + const Color(0xFF000000), // black + const Color(0xFFFF0000), // red + const Color(0xFF00FF00), // green + const Color(0xFF0000FF), // blue + ]); + + // Orange should be closest to red + final orange = Color(0xFFFF8800); + expect(palette.nearest(orange), const Color(0xFFFF0000)); + + // Cyan should be closest to green or blue + final cyan = Color(0xFF00FFFF); + final result = palette.nearest(cyan); + expect(result == const Color(0xFF00FF00) || result == const Color(0xFF0000FF), true); + }); + + test('nearest with single color palette returns that color', () { + final palette = Palette([Colors.purple]); + expect(palette.nearest(Colors.red), Colors.purple); + expect(palette.nearest(Colors.blue), Colors.purple); + expect(palette.nearest(Colors.green), Colors.purple); + }); + + test('gameboy nearest finds closest retro color', () { + final result = Palette.gameboy.nearest(const Color(0xFF00FF00)); + // Bright green should match one of the gameboy greens + expect(Palette.gameboy.colors.contains(result), true); + }); + }); + + group('Dithering', () { + test('floydSteinbergDither returns same size buffer', () { + final width = 4; + final height = 4; + final rgba = Uint8List(width * height * 4); + + // Fill with gray + for (int i = 0; i < rgba.length; i += 4) { + rgba[i] = 128; // R + rgba[i + 1] = 128; // G + rgba[i + 2] = 128; // B + rgba[i + 3] = 255; // A + } + + final result = floydSteinbergDither(rgba, width, height, Palette.gameboy); + + expect(result.length, rgba.length); + }); + + test('floydSteinbergDither maps colors to palette', () { + final width = 2; + final height = 2; + final rgba = Uint8List(width * height * 4); + + // Fill with a color not in the gameboy palette + for (int i = 0; i < rgba.length; i += 4) { + rgba[i] = 255; // R - bright red + rgba[i + 1] = 0; // G + rgba[i + 2] = 0; // B + rgba[i + 3] = 255; // A + } + + final result = floydSteinbergDither(rgba, width, height, Palette.gameboy); + + // Check that resulting colors are from the palette + for (int i = 0; i < result.length; i += 4) { + final color = Color.fromARGB(255, result[i], result[i + 1], result[i + 2]); + expect(Palette.gameboy.colors.contains(color), true); + } + }); + + test('floydSteinbergDither preserves alpha', () { + final width = 2; + final height = 2; + final rgba = Uint8List(width * height * 4); + + // Fill with different alpha values + for (int i = 0; i < rgba.length; i += 4) { + rgba[i] = 100; + rgba[i + 1] = 100; + rgba[i + 2] = 100; + rgba[i + 3] = 200; // Custom alpha + } + + final result = floydSteinbergDither(rgba, width, height, Palette.gameboy); + + // Alpha should be preserved + for (int i = 0; i < result.length; i += 4) { + expect(result[i + 3], 200); + } + }); + + test('floydSteinbergDither handles 1x1 image', () { + final rgba = Uint8List(4); + rgba[0] = 128; + rgba[1] = 128; + rgba[2] = 128; + rgba[3] = 255; + + final result = floydSteinbergDither(rgba, 1, 1, Palette.gameboy); + + expect(result.length, 4); + final color = Color.fromARGB(255, result[0], result[1], result[2]); + expect(Palette.gameboy.colors.contains(color), true); + }); + + test('floydSteinbergDither handles all black image', () { + final width = 4; + final height = 4; + final rgba = Uint8List(width * height * 4); + + // All black + for (int i = 0; i < rgba.length; i += 4) { + rgba[i] = 0; + rgba[i + 1] = 0; + rgba[i + 2] = 0; + rgba[i + 3] = 255; + } + + final result = floydSteinbergDither(rgba, width, height, Palette.gameboy); + + // Should map to darkest gameboy color + final darkestColor = Palette.gameboy.colors.first; + for (int i = 0; i < result.length; i += 4) { + expect(result[i], darkestColor.red); + expect(result[i + 1], darkestColor.green); + expect(result[i + 2], darkestColor.blue); + } + }); + + test('floydSteinbergDither handles all white image', () { + final width = 4; + final height = 4; + final rgba = Uint8List(width * height * 4); + + // All white + for (int i = 0; i < rgba.length; i += 4) { + rgba[i] = 255; + rgba[i + 1] = 255; + rgba[i + 2] = 255; + rgba[i + 3] = 255; + } + + final result = floydSteinbergDither(rgba, width, height, Palette.gameboy); + + // Should map to lightest gameboy color + final lightestColor = Palette.gameboy.colors.last; + for (int i = 0; i < result.length; i += 4) { + expect(result[i], lightestColor.red); + expect(result[i + 1], lightestColor.green); + expect(result[i + 2], lightestColor.blue); + } + }); + + test('floydSteinbergDither handles gradient', () { + final width = 8; + final height = 1; + final rgba = Uint8List(width * height * 4); + + // Create gradient from black to white + for (int x = 0; x < width; x++) { + final i = x * 4; + final value = (x * 255 ~/ (width - 1)); + rgba[i] = value; + rgba[i + 1] = value; + rgba[i + 2] = value; + rgba[i + 3] = 255; + } + + final result = floydSteinbergDither(rgba, width, height, Palette.gameboy); + + // All resulting colors should be from the palette + for (int i = 0; i < result.length; i += 4) { + final color = Color.fromARGB(255, result[i], result[i + 1], result[i + 2]); + expect(Palette.gameboy.colors.contains(color), true); + } + }); + }); +} From 0d665edd28e6b72a56a40fb0f243196cc9e89b9e Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 06:01:58 +1100 Subject: [PATCH 02/15] Rewrite PixelButton with pixel-perfect stepped corner borders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CustomPainter draws authentic retro game-style pixel borders - New API: borderDark, borderMid, borderLight, pixelSize - Multi-layer border effect: dark outer, mid, light inner highlight - Improved default colors with better contrast - Updated example app and tests for new API 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- example/lib/main.dart | 10 +- lib/src/widgets/pixel_button.dart | 279 +++++++++++++++--------------- test/pixelify_flutter_test.dart | 4 +- 3 files changed, 148 insertions(+), 145 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 0cbd8f0..6ed43db 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -160,20 +160,20 @@ class PixelButtonPage extends StatelessWidget { label: 'CYAN OUTLINE', textStyle: pixelFont, onPressed: () {}, - outlineColor: Colors.cyanAccent, + borderDark: Colors.cyanAccent, ), PixelButton( label: 'GREEN BUTTON', textStyle: pixelFont, onPressed: () {}, - outlineColor: Colors.greenAccent, + borderDark: Colors.greenAccent, color: const Color(0xFF1A3A2A), ), PixelButton( label: 'RED BUTTON', textStyle: pixelFont, onPressed: () {}, - outlineColor: Colors.redAccent, + borderDark: Colors.redAccent, color: const Color(0xFF3A1A1A), ), PixelButton( @@ -186,14 +186,14 @@ class PixelButtonPage extends StatelessWidget { textStyle: pixelFont, onPressed: () {}, enableGlowAnimation: true, - outlineColor: Colors.purpleAccent, + borderDark: Colors.purpleAccent, ), PixelButton( label: 'WITH SCANLINES', textStyle: pixelFont, onPressed: () {}, enableScanlineAnimation: true, - outlineColor: Colors.amber, + borderDark: Colors.amber, ), ], ), diff --git a/lib/src/widgets/pixel_button.dart b/lib/src/widgets/pixel_button.dart index 0ab32b1..3efeb2c 100644 --- a/lib/src/widgets/pixel_button.dart +++ b/lib/src/widgets/pixel_button.dart @@ -1,12 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:audioplayers/audioplayers.dart'; - -/// PixelButton: -/// - Pixel-perfect pixel edge outline -/// - Customizable colors for normal, hover, pressed, disabled, focused states -/// - Supports retro bitmap/font style text via TextStyle parameter -/// - Plays optional sound effects on click and hover -/// - Supports animations: glow on hover, scanline overlay on press + +/// PixelButton with authentic retro stepped-corner border +/// Replicates classic game UI button style with pixel-perfect edges class PixelButton extends StatefulWidget { final VoidCallback? onPressed; final bool enabled; @@ -17,136 +12,65 @@ class PixelButton extends StatefulWidget { final Color hoverColor; final Color pressedColor; final Color disabledColor; - final Color focusColor; - final Color outlineColor; - final double pixelEdgeThickness; + // Border colors for 3D effect (outer to inner) + final Color borderDark; // Darkest outer edge + final Color borderMid; // Middle border + final Color borderLight; // Inner highlight + + final double pixelSize; // Size of each "pixel" in the border final bool enableGlowAnimation; final bool enableScanlineAnimation; - final String? clickSoundAsset; // e.g., 'assets/sounds/click.wav' - final String? hoverSoundAsset; // e.g., 'assets/sounds/hover.wav' - const PixelButton({ Key? key, required this.label, this.onPressed, this.enabled = true, this.textStyle, - this.color = const Color(0xFF222222), - this.hoverColor = const Color(0xFF444444), - this.pressedColor = const Color(0xFF666666), - this.disabledColor = const Color(0xFF888888), - this.focusColor = const Color(0xFF5555FF), - this.outlineColor = const Color(0xFFFFFFFF), - this.pixelEdgeThickness = 2.0, - this.enableGlowAnimation = true, - this.enableScanlineAnimation = true, - this.clickSoundAsset, - this.hoverSoundAsset, + this.color = const Color(0xFF4A4D5E), + this.hoverColor = const Color(0xFF6A6D7E), // Noticeably lighter on hover + this.pressedColor = const Color(0xFF2A2D3E), // Darker when pressed + this.disabledColor = const Color(0xFF555555), + this.borderDark = const Color(0xFF000000), // Pure black outer edge + this.borderMid = const Color(0xFF1A1C24), // Very dark middle + this.borderLight = const Color(0xFF8A8D9E), // Light inner highlight + this.pixelSize = 2.0, + this.enableGlowAnimation = false, + this.enableScanlineAnimation = false, }) : super(key: key); @override - _PixelButtonState createState() => _PixelButtonState(); + State createState() => _PixelButtonState(); } -class _PixelButtonState extends State with SingleTickerProviderStateMixin { +class _PixelButtonState extends State { bool _hovering = false; bool _pressed = false; - bool _focused = false; - - late final AnimationController _glowController; - late final Animation _glowAnimation; - - late final AudioPlayer _audioPlayer; - - @override - void initState() { - super.initState(); - - _audioPlayer = AudioPlayer(); - - _glowController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1000), - ); - - _glowAnimation = Tween(begin: 0.0, end: 12.0).animate( - CurvedAnimation(parent: _glowController, curve: Curves.easeInOut), - )..addListener(() { - setState(() {}); - }); - - if (widget.enableGlowAnimation) { - _glowController.repeat(reverse: true); - } - } - - @override - void dispose() { - _glowController.dispose(); - _audioPlayer.dispose(); - super.dispose(); - } Color get _backgroundColor { if (!widget.enabled) return widget.disabledColor; if (_pressed) return widget.pressedColor; if (_hovering) return widget.hoverColor; - if (_focused) return widget.focusColor; return widget.color; } - void _playSound(String? asset) { - if (asset == null) return; - _audioPlayer.play(AssetSource(asset)); - } - - Widget _buildScanlineOverlay(Size size) { - // Simple scanline overlay animated with opacity flicker - return AnimatedOpacity( - opacity: _pressed ? 0.25 : 0.0, - duration: const Duration(milliseconds: 200), - child: CustomPaint( - size: size, - painter: _ScanlinePainter(), - ), - ); - } - @override Widget build(BuildContext context) { - TextStyle textStyle = widget.textStyle ?? + final textStyle = widget.textStyle ?? const TextStyle( - fontFamily: 'PressStart2P', // Example retro pixel font, include font in pubspec.yaml - fontSize: 14, + fontFamily: 'PressStart2P', + fontSize: 10, color: Colors.white, - shadows: [ - Shadow(offset: Offset(1, 1), color: Colors.black, blurRadius: 0), - ], ); - return FocusableActionDetector( - enabled: widget.enabled, - autofocus: false, - onShowHoverHighlight: (hovering) { - setState(() => _hovering = hovering); - if (hovering) { - _playSound(widget.hoverSoundAsset); - } - }, - onShowFocusHighlight: (focusing) { - setState(() => _focused = focusing); - }, - mouseCursor: widget.enabled ? SystemMouseCursors.click : SystemMouseCursors.basic, + return MouseRegion( + cursor: widget.enabled ? SystemMouseCursors.click : SystemMouseCursors.basic, + onEnter: (_) => setState(() => _hovering = true), + onExit: (_) => setState(() => _hovering = false), child: GestureDetector( - onTapDown: widget.enabled - ? (_) { - setState(() => _pressed = true); - _playSound(widget.clickSoundAsset); - } - : null, + onTapDown: widget.enabled ? (_) => setState(() => _pressed = true) : null, onTapUp: widget.enabled ? (_) { setState(() => _pressed = false); @@ -154,32 +78,17 @@ class _PixelButtonState extends State with SingleTickerProviderStat } : null, onTapCancel: () => setState(() => _pressed = false), - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 20), - decoration: BoxDecoration( - color: _backgroundColor, - border: Border.all( - color: widget.outlineColor, - width: widget.pixelEdgeThickness, - // For pixel perfect edges, consider a custom painter edge if needed - ), - boxShadow: widget.enableGlowAnimation && _hovering - ? [ - BoxShadow( - color: widget.outlineColor.withOpacity(0.75), - blurRadius: _glowAnimation.value, - spreadRadius: _glowAnimation.value / 2, - ), - ] - : null, + child: CustomPaint( + painter: _PixelBorderPainter( + backgroundColor: _backgroundColor, + borderDark: widget.borderDark, + borderMid: widget.borderMid, + borderLight: widget.borderLight, + pixelSize: widget.pixelSize, ), - child: Stack( - alignment: Alignment.center, - children: [ - Text(widget.label, style: textStyle), - if (widget.enableScanlineAnimation) _buildScanlineOverlay(Size.infinite), - ], + child: Padding( + padding: EdgeInsets.all(widget.pixelSize * 4 + 8), + child: Text(widget.label, style: textStyle), ), ), ), @@ -187,19 +96,113 @@ class _PixelButtonState extends State with SingleTickerProviderStat } } -class _ScanlinePainter extends CustomPainter { +/// Draws pixel-perfect stepped corner borders +class _PixelBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderDark; + final Color borderMid; + final Color borderLight; + final double pixelSize; + + _PixelBorderPainter({ + required this.backgroundColor, + required this.borderDark, + required this.borderMid, + required this.borderLight, + required this.pixelSize, + }); + @override void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = Colors.white.withOpacity(0.1) - ..strokeWidth = 1; + final double p = pixelSize; // Pixel unit size + final double w = size.width; + final double h = size.height; + + // Helper to draw a pixel + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + // Helper to draw horizontal line of pixels + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } - // Draw horizontal scanlines spaced by 4 pixels - for (double y = 0; y < size.height; y += 4) { - canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + // Helper to draw vertical line of pixels + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); } + + // === LAYER 1: Outermost dark border === + // Top edge (with corner cutouts) + drawHLine(p * 2, w - p * 2, 0, borderDark); + // Bottom edge + drawHLine(p * 2, w - p * 2, h - p, borderDark); + // Left edge + drawVLine(0, p * 2, h - p * 2, borderDark); + // Right edge + drawVLine(w - p, p * 2, h - p * 2, borderDark); + + // Corner pixels for stepped effect + drawPixel(p, p, borderDark); + drawPixel(w - p * 2, p, borderDark); + drawPixel(p, h - p * 2, borderDark); + drawPixel(w - p * 2, h - p * 2, borderDark); + + // === LAYER 2: Middle border === + // Top edge + drawHLine(p * 3, w - p * 3, p, borderMid); + // Bottom edge + drawHLine(p * 3, w - p * 3, h - p * 2, borderMid); + // Left edge + drawVLine(p, p * 3, h - p * 3, borderMid); + // Right edge + drawVLine(w - p * 2, p * 3, h - p * 3, borderMid); + + // Corner pixels + drawPixel(p * 2, p * 2, borderMid); + drawPixel(w - p * 3, p * 2, borderMid); + drawPixel(p * 2, h - p * 3, borderMid); + drawPixel(w - p * 3, h - p * 3, borderMid); + + // === LAYER 3: Inner highlight border === + // Top edge + drawHLine(p * 4, w - p * 4, p * 2, borderLight); + // Left edge + drawVLine(p * 2, p * 4, h - p * 4, borderLight); + + // Corner pixels + drawPixel(p * 3, p * 3, borderLight); + + // === LAYER 4: Background fill === + final bgPaint = Paint()..color = backgroundColor; + + // Main body (inside the borders) + canvas.drawRect( + Rect.fromLTWH(p * 3, p * 3, w - p * 6, h - p * 6), + bgPaint, + ); + + // Fill corner areas + canvas.drawRect(Rect.fromLTWH(p * 4, p * 2, w - p * 8, p), bgPaint); + canvas.drawRect(Rect.fromLTWH(p * 2, p * 4, p, h - p * 8), bgPaint); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) => false; + bool shouldRepaint(covariant _PixelBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderDark != borderDark || + oldDelegate.borderMid != borderMid || + oldDelegate.borderLight != borderLight || + oldDelegate.pixelSize != pixelSize; + } } diff --git a/test/pixelify_flutter_test.dart b/test/pixelify_flutter_test.dart index 31e17a4..935a7e3 100644 --- a/test/pixelify_flutter_test.dart +++ b/test/pixelify_flutter_test.dart @@ -129,7 +129,7 @@ void main() { body: PixelButton( label: 'COLORED', color: Colors.red, - outlineColor: Colors.blue, + borderDark: Colors.blue, ), ), ), @@ -160,7 +160,7 @@ void main() { home: Scaffold( body: PixelButton( label: 'THICK', - pixelEdgeThickness: 5.0, + pixelSize: 3.0, ), ), ), From 14b1d41af976d8c1ab67b9748cfb57336a5697d1 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 06:05:53 +1100 Subject: [PATCH 03/15] typewriter effect --- example/lib/main.dart | 24 +++++++ lib/src/widgets/pixel_text.dart | 108 +++++++++++++++++++++++++++++++- test/pixelify_flutter_test.dart | 22 +++++++ 3 files changed, 153 insertions(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6ed43db..5964964 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -168,6 +168,8 @@ class PixelButtonPage extends StatelessWidget { onPressed: () {}, borderDark: Colors.greenAccent, color: const Color(0xFF1A3A2A), + hoverColor: const Color(0xFF2A4A3A), + pressedColor: const Color(0xFF0A2A1A), ), PixelButton( label: 'RED BUTTON', @@ -175,6 +177,8 @@ class PixelButtonPage extends StatelessWidget { onPressed: () {}, borderDark: Colors.redAccent, color: const Color(0xFF3A1A1A), + hoverColor: const Color(0xFF4A2A2A), + pressedColor: const Color(0xFF2A0A0A), ), PixelButton( label: 'DISABLED', @@ -251,6 +255,26 @@ class PixelTextPage extends StatelessWidget { Colors.purple, ], ), + const SizedBox(height: 32), + const Divider(color: Colors.white24), + const SizedBox(height: 16), + Text('TYPEWRITER EFFECT', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + PixelText( + text: 'TYPING CHARACTER BY CHARACTER...', + style: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.amber), + effect: PixelTextEffect.typewriter, + typewriterCharDelay: const Duration(milliseconds: 80), + typewriterCursor: '▌', + ), + const SizedBox(height: 24), + PixelText( + text: 'TAP TO SKIP ANIMATION', + style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.lightGreenAccent), + effect: PixelTextEffect.typewriter, + typewriterCharDelay: const Duration(milliseconds: 100), + typewriterCursor: '_', + ), ], ), ); diff --git a/lib/src/widgets/pixel_text.dart b/lib/src/widgets/pixel_text.dart index a6688c8..c0a0870 100644 --- a/lib/src/widgets/pixel_text.dart +++ b/lib/src/widgets/pixel_text.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; -enum PixelTextEffect { none, flicker, glitch, scanline, pixelate } +enum PixelTextEffect { none, flicker, glitch, scanline, pixelate, typewriter } /// PixelText widget combines pixel font rendering and multiple retro text effects /// into a single configurable widget with styling parameters. @@ -28,6 +28,12 @@ class PixelText extends StatefulWidget { /// If true, flicker effect cycles continuously final bool flickerEnabled; + // Typewriter effect parameters + final Duration typewriterCharDelay; + final VoidCallback? onTypewriterComplete; + final bool typewriterShowCursor; + final String typewriterCursor; + const PixelText({ Key? key, required this.text, @@ -40,6 +46,10 @@ class PixelText extends StatefulWidget { this.scanlineOpacity = 0.15, this.scanlineSpacing = 4.0, this.flickerEnabled = true, + this.typewriterCharDelay = const Duration(milliseconds: 50), + this.onTypewriterComplete, + this.typewriterShowCursor = true, + this.typewriterCursor = '▌', }) : assert(pixelScale >= 1.0), super(key: key); @@ -55,6 +65,14 @@ class _PixelTextState extends State with SingleTickerProviderStateMix Timer? _effectTimer; final Random _random = Random(); + // Typewriter state + String _displayedText = ''; + int _typewriterIndex = 0; + bool _typewriterComplete = false; + Timer? _typewriterTimer; + bool _cursorVisible = true; + Timer? _cursorBlinkTimer; + @override void initState() { super.initState(); @@ -62,6 +80,70 @@ class _PixelTextState extends State with SingleTickerProviderStateMix _startFlickerTimer(); } else if (widget.effect == PixelTextEffect.glitch) { _startGlitchTimer(); + } else if (widget.effect == PixelTextEffect.typewriter) { + _startTypewriterAnimation(); + } + } + + @override + void didUpdateWidget(covariant PixelText oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.text != widget.text && widget.effect == PixelTextEffect.typewriter) { + _resetTypewriter(); + _startTypewriterAnimation(); + } + } + + void _resetTypewriter() { + _typewriterTimer?.cancel(); + _cursorBlinkTimer?.cancel(); + _typewriterIndex = 0; + _displayedText = ''; + _typewriterComplete = false; + _cursorVisible = true; + } + + void _startTypewriterAnimation() { + _resetTypewriter(); + _typewriterTimer = Timer.periodic(widget.typewriterCharDelay, (timer) { + if (_typewriterIndex < widget.text.length) { + setState(() { + _displayedText = widget.text.substring(0, _typewriterIndex + 1); + _typewriterIndex++; + }); + } else { + timer.cancel(); + setState(() { + _typewriterComplete = true; + }); + widget.onTypewriterComplete?.call(); + // Start cursor blink after typing completes + if (widget.typewriterShowCursor) { + _startCursorBlink(); + } + } + }); + } + + void _startCursorBlink() { + _cursorBlinkTimer = Timer.periodic(const Duration(milliseconds: 530), (_) { + setState(() { + _cursorVisible = !_cursorVisible; + }); + }); + } + + /// Skip to the end of the typewriter animation + void skipTypewriter() { + _typewriterTimer?.cancel(); + setState(() { + _displayedText = widget.text; + _typewriterIndex = widget.text.length; + _typewriterComplete = true; + }); + widget.onTypewriterComplete?.call(); + if (widget.typewriterShowCursor) { + _startCursorBlink(); } } @@ -87,6 +169,8 @@ class _PixelTextState extends State with SingleTickerProviderStateMix @override void dispose() { _effectTimer?.cancel(); + _typewriterTimer?.cancel(); + _cursorBlinkTimer?.cancel(); super.dispose(); } @@ -149,6 +233,28 @@ class _PixelTextState extends State with SingleTickerProviderStateMix child: textWidget, ); + case PixelTextEffect.typewriter: + return GestureDetector( + onTap: _typewriterComplete ? null : skipTypewriter, + child: Text.rich( + TextSpan( + children: [ + TextSpan(text: _displayedText, style: baseStyle), + // Blinking cursor + if (widget.typewriterShowCursor && (_cursorVisible || !_typewriterComplete)) + TextSpan( + text: widget.typewriterCursor, + style: baseStyle.copyWith( + color: _cursorVisible + ? baseStyle.color + : Colors.transparent, + ), + ), + ], + ), + ), + ); + case PixelTextEffect.none: default: return textWidget; diff --git a/test/pixelify_flutter_test.dart b/test/pixelify_flutter_test.dart index 935a7e3..9b8d0a7 100644 --- a/test/pixelify_flutter_test.dart +++ b/test/pixelify_flutter_test.dart @@ -276,6 +276,28 @@ void main() { expect(find.byType(RichText), findsOneWidget); }); + testWidgets('renders with typewriter effect', (tester) async { + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: PixelText( + text: 'TYPEWRITER', + style: TextStyle(fontSize: 16), + effect: PixelTextEffect.typewriter, + typewriterCharDelay: Duration(milliseconds: 50), + ), + ), + ), + ); + + // Initially should show partial text with cursor + expect(find.byType(GestureDetector), findsOneWidget); + + // Pump some frames to progress the animation + await tester.pump(const Duration(milliseconds: 200)); + await tester.pumpAndSettle(); + }); + testWidgets('asserts pixelScale >= 1.0', (tester) async { expect( () => PixelText( From 07a0ee7911b7aff076dc6dce1d071886eb16ffa7 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 06:47:43 +1100 Subject: [PATCH 04/15] tests --- example/assets/landscape.jpeg | Bin 0 -> 176666 bytes example/lib/main.dart | 774 +++++++++++++++---- example/pubspec.yaml | 2 + lib/src/widgets/pixel_button.dart | 252 ++++-- lib/src/widgets/pixel_notification_card.dart | 134 +++- lib/src/widgets/pixel_panel.dart | 361 ++++++++- lib/src/widgets/pixel_progress_bar.dart | 235 ++++++ lib/src/widgets/pixel_slider.dart | 448 +++++++++++ test/pixelify_flutter_test.dart | 141 ++++ 9 files changed, 2079 insertions(+), 268 deletions(-) create mode 100644 example/assets/landscape.jpeg diff --git a/example/assets/landscape.jpeg b/example/assets/landscape.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..7d33ed1cd996fe9674c7871a68eacbdb4ca1264b GIT binary patch literal 176666 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

;O^FIc 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

&ZB(`4pP$G14<; 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 z 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�zQc_WB+>Ate@pz{_80I(HO@s@zG#q zqZ5I!e*gM;IW|l_|3v-mBQfz<88R*g-H01>>R0RIdV8-s=FXc-im6d%cu6N(q+Qmc ztV-m!T;?m(o1+D;x-PK95_z-Yi_`OBtghc+qSA3sSN`?KZ=22Fgbqx+F2>(bc-3P& zl_lTTC{~DnXnKKDdanGpyMR$@iF9>FdS3lUtQ>$U)r)ROTZvN5RoOea3E}29x7T2Qct%zoQBcofpCqLFc`%q2tj`-vD7n}+7>3| z60bv?UXEtxctt@cH6=zlw(s#suMHa;eWzHY6qRcx_lh;*?c~V9j;spF{vHrMAC{>& zjjS7ZlYIRO#tH3~N&`CGWRoolXMdnxYlw zAw2e#nL94L73<{nk^9bUis8N8&eDl;n<{v%KSrosell0yOrDc8eWsZuHvYeGW!Bh{ z&v>`7-hNU(4fzSx|Cn4%(B+Krhk}8m?*JOZnn3C&;!fTUQnS}W>(#G0lx@Z#O6 za+W4w<6afxoip7UAG7(|y%>bx%lfKUqn^X*gG$Re>b0 zA(||8p`D$7)VabT`{yop?{2>lVP$CWhNlGKPl15NxR6dInqyu$K6oM{pWj{e@^x9- z*OB5qQ{VS~k)8{8Kkhuzo3S7rHj8d2nRI)Q!(b>Jxy_6rFYRj(4cm$PZ-yc8Vfa-o z9qhOJW}H*vP=9H03RKR6aif3oV1L3ohqir`OxzM+;v}X|T_gr!-*?%(L^ib=Y;6NH}p6)6E|VCM4|bim8A7N<*NP-e#=uHv}WM z)>YYS&UM(JLm?B$KuuaVXTQxXFE41Rk%;2zPiLZ+cg%aIw25MzNk>yzqT?BSh+JKh z-zQ`PFAh3oMRSiL*UUogy4R4AZivt=DM!rdVOB~G|N9_y63Vp<(rY!29+&k3hm@Ov zqCVaPaG_W|Exvu{&hr1d%@@+CV#j@S3JkGUJI?$ipG!wqH#fD^j~F?0*SEi&ZX>xy zVY?hDGgn;PcX#4jEx#n?yRqhkqyO-FHR=+08opQe701S1)@L1nv~tkyX;iZFy^Uj8F>G#JZ%0h^hCXuphfeF}x2O&;Lq7=i*dE(Al^~U%oPi2%-U6- zr^yi~vr_bFiDFC-ab^-<63`ekKX*~!r13I<>vIA`3b8Hs+8>X1y)J|K7Sh|=Co=7z zn5BZojZZ@76yrJle2aCvE!i3ml7`_c{g0x(gLyJ#`oa$VtEe&8rzTS;k0DX`=1+cc z$Ox6^)MT~CCZmsg_PMJr*Oicb@1XVZXVKB}zs44z`A)%!8l5QD6f;tyXwN* z5f@>Dk@n6(cwoW&jh4o~O#?M~Bd62j407kzV0iID-E(z!bba3tpkHjRJg8J#xM{Y_ z`{DL2C4mx=Q)K7ZurQRZqFhAl_iCT}-^pV>D51PGr;yJ=q{?@uCcb04mks!$%b)3B zh4g=8A4HyH0cj7=y=b5sTAIDMhs)+rEpu3ETwzpw2_BfwBX~+C2Zn%-AHFJeR!g}QSO(eQD5aDH4>+^47cGpQ8|!TCP;0K)pBBp*vE!eKOai;koxNtt;Sh- zL=J=trkunwm`bCL$zn=NY?p>kymnIWl`!rM{JkqjF;W;@hgu(vf7oJomg#h7bvDOF z*ggD;^njk!&t}Hr!E%fJ9kmUgw`<5YZi-$7VSrq}lM&^t{fFoVdTM}rgtvb^ZmIiV zNB-S`+R74H951+D6?%XY^}AEL*Gbu`SFrr8ln~F2MtHnc|GnMc7Jrljjz7~qs`1_( zKa>y)nik4V50=#xe&|4pK8EI?gx?svqgJeUazakFu3>~3+0Al1zUMY12rg94qO6MJ zs=&G9Iw(L^@@BmWA`|jJA^Bp94L@~qy6V;(y}0PA%Vk(un=(l)*U66e)nhD|g>b|PlY_#-iZF`~^1w$*{?T1av|=iTuU&#I)sv-ng<4Gx+F7A3VEK7FOE)keygc37&{|Nu7me zEkSE#*{8yDl<7g@qMQH{=g`}WWV`e@MQTKJOXp?5l{F0iF^%lfcq~CLmNln%oJ#Zr zf{QBJInYyBlKM=Y{=I*b1Wuc#2xsR>SFIuwEDEW#=WGQfYWRI!=lYF3cw6%4VTgCk z1b=v;R+N-nc|Us|*QjhP`c#2R(AQ5&z(h|jpb;H5|1njf-}?qRZHDsccdhPGdsJi0 zm#=kcPmw#Koho(oFSqh!sM}JYNS6r#>RA*B4}>H~7^W?+$^1heJh#@Kd6 z3$*7nhh=cTZ{NTFI%h#xlp>)0@IyPZ$`&r6yGBu)&cvTJzoepgAS}`0$42*(O@z!f z{u?GCcr*;KD5oRq!zm5@zx2Z7d5!P0&?{eV7=0J3;Z=|gt-XR*gqa8MN1mt(&|a<| zKk>rR1^kOkX;wB(6~LD2|Jr%#PO@6?#`B%++$VR^KkRAGr)0L=;VCc7@ufZa2UIRA+CS#(-bB8uZZn^jw8h_qx z5i0L%@&u8#>^1z% z)bJ2dZ;Rv;9cM;n8+yo>9r9tZm69%J5)lCJ-c}Df|vAW=T9w+?a%WTK zXLoLnYZNN(0B(JM{7E)m*!uP&BSA%o?CNF{eS`Nxq2UsZqC8XJ3G-an-UMDodW}ig zfDTTvl}$`9W%v@FsVT;spZ49HT{`YBn93!Ve5iJFB&&sZ7X@q6M7`I{_(Y81rYljU z%ZHzLsLaOup7LVLr#A$jJoqrZc=6&GHsN(2Y%O`w2~ezt9v6P@uZRXVS#;5**^4xGm-y?FkHYaNc6rMWy)IN z9oshiPWW?J@@T9or!lgMKAvG_O!zW3kPA4>y?(_3I?plQq7zxw!7lX0&Q_Rby_FMD zg;HjkU-ebPs$8J^ts$o8O$#%7Db`Ki&RX~t6CiALZtFf)J;bUON|#*_JjkZkwF_r| z1EL}*chF$7dTS0^@`yrF(8GcY0i0Jr+{cOOn$gZM+(y=EE>ZovN1_}sUWu}h1L@pb zyG^~iI@L*znydGkz^Eg^vZpvm<`u%AgGYzq@5EtNj7 zNz)m{>{aD_d-()UPD%fa!N_~Vvrh;V^4Hmz4`z=eeL%ICb{vhZ}9X$(Xt zzeKWOURz+iC(5rhsezc6CU3nz;aEVbk98!ugJGKINza}_<9o`E#Uj072F6agSK`De zqEC?wYn}fGG#Y7f{he9xbldIDiYv@yNAUS*J`*wSk#^@D1Il%!P{UVa`>DyBqOY!u z%tNb7lP4AAU%gDfs*TGY8zddq>|qMeU7J_;QxgJVnNh;3m{&{G%T-WAwe?fy;TSyf zSxXxGXU3A#d%3Pch9Lw@W~ur`lWOFmPlw4!_7LY)VDPE{r6agx1?UtXZAGO!BSA*~ z^*;AV(Hu3ToI9pI9~m?!DiPDq*quq2TeU2!icP1|aUU_ykuun49+v6KA|R1H zIX;%ZXJbylT>3qjM(|Wom^I4uPAXzi8TH?D zo=a0F^S{oXCMvtI%O4#y;^g)YASOT`wcV_kH1v~EUHTS#9vpigZ0PHIfF*UdgW_+^ zC4eoZ!!(V_CI>=Ys|$ey&3oumgWd58QSq5t7AgYDivfgMN>5rV-N4XMnWI+Z&z8|{ zBAL}7Hg~i5O>urhEUMXe1h2uO%(sTIY4>;)(cDQo%~u^ zh)$<#1vO$INJg%vt85Nr?>prkKQ{I81PM(C$;i*i4=N$KEu+l5D);o1?Vy>N+k!j* z5+u@oPgo!K=a0{lrkO52??oUD%zx9ekle?NAB2VOAdY|6d81E{? z3DS$*aFZ>eS*4H9Oc>t;Hh1wJQh?lPLSx>1E+W|G5+3Xv&x;9$YN$ehVylU9p%-ga zVxa_6{jmVSFrd)I@h~7XYi`ZjK&q_w`YCkhg16^aulY`fcA`-lcWy0lp4)y_S#N+e zN;!w4OSmWHO&KiGUV!{MgU(cle<-NGb5tjv zW+(Ra<(-#H(zOOk-YpvuPF20WpN|!=>^x+#^4UQ$~rfBdK8D~VGTCr zL98MhUJTZj(#C-PX~TCH9tF0X;zV2P`({zdOg=y^l`uNgv)2qpmaK2ZKuHcov7R*3 zYdBE6qa1w;Hm?dB>t$GcP+POa?}^i?54788aEIFC|3e5bQz+LlZhrsv_C(lk21oN6D2=CnZIO!fZ6vXj0l7o*qK&V8i*)KykAHc5z@ zh68cR-rx!!HFm7RxBR!6oy|b}#%mXnA!dqMpN}XQjID@dnEv%C8`y9nS((I3$sR?a zJ+~(ZxS(S4Hh&Kxvc*HrR^EWo@=F!q4?n(+(ocRcS@q^DMYp#WF?EriNZGk4lYXeb z@RY6Q?>k3JvOp^lk+D|b6hOe@j#F~QLSjil|D2H1Qf&?NSRwdvE33*MP-o5lrXQEh z*QGfOXA7)@F#D^6S7zuczv(BQ1v@*`rG8fD!n0V!)Xd6wVT!TMV4%qgz?hws5{XD` zNG9*l8@Q@GY8l3ffHwF|^y9`Yo{5n?JoVx}^e9%hw?_U>yW(-$4RBNzplTn{FuUWX z3Qh_O*eUMw?J*y7U^4u?r-!MQuo=9#-%9cQF{IS(l3hmcmN`tKWzX2c)L;8Vg%`F# z;CSQY62%2gz-MT*ad7Y{T=n+EaHx4%(b>1ToIF0;N+m@JQcf{hODnl+JYZp=7fT6@ zzNvJ1_!h5nNi7*7JAzay&X)-MCv=uMt?n)YN-<2nD-0Z(;HG#svix=`6rtdqg9}4W zp)VfaML@nv--}I-$tRKw8}{07I$dqz36P+)B|P7g~0 z3hsrYPoIibfqeB1q-Q1*AuU3^w|Dqm&(4u`U64H$_~hf<=5j~V#xrnfG=Nwrdf@aZ z@3zV%5ATS85}|@^*{{qrwq=DFP!5T+x0Q32EzNRVailNI*YS4|7aq`w0d{<&WlxY+ z+`64J@Z?a%#MnQ!d$NYu6V^{zZ!R&j3BgR0#Zqd!;T{L=xAhG&y-pKIb6cde#5YGR zZCCZpc6RK3Bm$qhFjbD__9-{3CMv8J$k8vi-T;0Pl=+FyJ4Jx;l+sS`Hs%jg=nUfh zRl+su8dil;3}e2zxj2U#%bc6+`@7vf9>Wbs&LF!g?XoakC#5*o&t_v$SQ@$`oRk9K z;F&ZT)+jl_!!cLDfKi*JH{#?Ty-gEM%}FOMmdevj)MQ1nc*eu0EO;;<&mt(myPlvU z*^{upULMSDH*n1}U`VzE$`l@@x#~>hlM5=gAk{AE=EZGLW*l|NNc&k!u~njmzGz#9xSzXJ;<2P=UQXMjIzzhPnWqi$ zR`(8?)v@eSrEc0z$|#baQSU7{UEs$(uPy|E z_>YS31ZRC+FjUef9n99BPXqMpO-?DW{iJ=vw)VF1HZcOCuP3nPPRWASSv#u8)P2Jr z0)`88;xtK{@#iQJ;^*y>_$5b5WKv$O6Y_^of|` zEW^e9VD|+f4rezxsS{c}c!E*NfU8`Na7*R@2$2e|+F8K2N z)}2B5oM5$~e2zVjSJrG?0{xipm=Uvt(hS8AbS9| z;)Int=t<)h^*l1Hwu)AVWO8jQ-&m)#J(JOdxT5CXjh&Ws!&;zZg=N9hZ59`ZB2cK) zJ}&erEn@uZM-Sp-ulzdIXBpCt$7W)jRl~d*{;a;#L7EOL%KGNa&}1YE^$)z-WBF~V zW>5l}O;h6AbU!{qJ8BbFiMNQ$LYIb%iagb<4X5B*-0&!TX6DuFbR#6K))eUmbp#dK zwG5KB?p>6E`hC3EuD`am?yIm${EZ7P1P3>XPZf=rdI5Mvc0VhCk+rn8rUaLA;8lQ z+yykn=j~k{HOg`RuJv}0tN;EwbQQ&!rl7m)^EW|!6r%+5{bTq&0v}UJ0J0DQ^Hi6q zHmAHYU|=V`Eqx+kHr}CXXuuA<`!+zaicckJUkHf{c>IC4rz-7@W;62Y!cwL0ho5%r zUoUHQGg$?4CefzdOJ$n~|Bx-fKd$8_{1aeY(VbH{8;~zwW)xEOw{`4$>OBI!>6F=n z_opQycb*E`Kng1BRM^jw;f_(eaf+2uz~;Nyj&oc4wwD?9y~Mf~Xbm;QU{t{mD+!|LmeHcb%YBksU&Cr;rM(Gw z<-brQ>GD(Odp2drG}6z@R2!7jBVhBZQjcn4M6{NUx zEL{G#XKZT&B9>L9-U9UM4cej>Ci_ekTeGj2#$)8>>}mwB7qa`C<`2FcMiY%oNe}+= zv$ha8a#5>3^SY_ns}4hFP?pObg<`-W{^4`>MTj$n6VI1`=W#4=-P`h-CtX%)@y1B* zw~*T)SLI_U(mur+@7^P0f3anvh#~zoK1+-dfM@3_3_P(av5#25|7 zm4=FS_Enn%5naOc-kB!r9*oStec zY&m1?XgO>UaA!Z7Z47rPQ{V`%|J--QN7AVSCf20R=~k-d5iI>3S4-J?VM}*JZHYJ~ zcg`W)-Mqn7zz`mO*0)yo<-12PwW)_QBR~&dp{$Dfha`rv&fF9oI4524K>`4hXIK8@ z$eYwQwv=&r*Q0D@#TC=Nf>}4Xsu+49Nb9Ywo#TXKp~5MPaPZE!oK|2&GES{|>=?If z=Je+-d!zTSt}7ob>oj+5GZ}yD59$W-#x?|sd4B1-|LS2+tW1%z7GW1tDlq?Un zNtb*XSUwLJ^qctMH&dZx{mhfQraj*P-%{dsgjD+HCcj{Y z^gyKb-#?`DEnLCho^)N@1#GL%Z+`KeVd>i|Sk9_q$|&1Ri+mgqET~ZGhAc=tA9}&5 zm`- z+;hTr#)v3%?F{Q!&xIK;ap~+#z*t~W@cE5wnU=GZrk#qCo5o8H3RBa|)uW$vx8Q&% z^N96IwjK&W9r?6n9K*WiuD6EVyMD2d-(P0$@iF(BamKjct~I(;-8||UYFL;!W&D zCR*U`7Zuo*SEu-XPL zGb-+9dYBF~X&))yPUd>Y?et*RN6<+Y2yJ(&X6NPTuLXyYl(+y5+>LbV9GP#IqCZc> zH2#?<;^VT@WL8rS-Ud?(gIHieBkLDz`evvm?l(%s=Vtu7g9i58hie`u{$Sl2S^ef*>g!5)+Uv=?(!!7(KcM z(g+)kG)lL0mvqDE?(VKJpZk4&|8dUFVY@q@-TQvOlDS9jP!jHN3+OW+QrcM1aM8-` z>vR78Lv!?XGBLf+jW)eCz2k2i8lOqt1rg?axefnKvF`vZj*>Pv#?qfM#CcbhhpYO0 zHldZ^?f484N;I3TwgI`Jl^;O(FgRRT->QDV^|6lSbxnhVui^48ay#VBcwp3z+Vkii zaK$=1Pqrbk%Kf>um_EiciVr-WN7pnAK?(2sKtbEBv04*9M2ttJ9#hvFlYkX~U0|sl zSgI>KMaxWaZ#qT#J1Fi4D3k)XF(grBDM+tV?7g_&#`nMN0C8!MW!gm#c#&o5T`77B zd&3y~c%U5>(uRH}9kP?W?ZPtJHz`@usF@l){*yU^e0e+BOqO>;`BVAK!L1+Q7sccZRf_WTUBo(AVSMsGn(5QVu0vU zP0$;yYKSthDP@&z{D$?L#^H+X@CwThSAY>KwG{Iz-|D-@BlfRH}uuv zyVy+$k$V(Y0Q9>PRIcj((6~h}fI=h2(G0Qh2_}F6yPX2n~v)<%3V?iC!Nr zD$w-LS5Ht2ke9)KUK#%`L0pm?fL=bQEN}rpcH84n?kc(0HZkBf1{%TEJ=~vp7%JC> z`N!;yu@zwYGpeiAf7oCO%)EXc=ZW8o<~GdJATp|maw&`~)hNk#FLvML!Yp`QKA;3b^HF`yqa@U6=Yx?Jm#qeW#A!3gk#*>r zh#trmO_fM3zU0C>W2ER{YmA~WoIgKdE*{%2wh6sU znO_ONd*!qSr8bPaF2~;AgPr6Kfl{VuqCk2;C(j%C|E~(lg%b6U@pZYZ5O-1+N%CjKtWiA z8aMz8X_;%L6@}--30g$Ak~*9jd@o6Z*pZTx7IYWJJog`&5vbw+eUAes=mp(vN5ubY z%ZjL6#$myFaDB&pwEV9EWsOmGV`#9hk| zt2+6$2tA{Cd_n6*91x(6faWk2r2V*h>8~Cz!RTSX@Z7WG$C%6lkK$`)pnF`@ISp$hbt) zr{KQUudfT>zNxJE0!3=3#TcC1QRL68wjs}MeHR`r^H1s6uqTc; z<7{;*xrrl#SY9_(pRRXbhz%@vx#mAsJ&1CwNo5UMz|hLG+P8$~3kOM#%R-|fd)HWN zsPO=A*>Jp0U!F&NFebMDk&3U>#4DLaW8uz-^Wp~5lTR6EkXrrZc9tyW>~Hbc_Yjk~ z5fPV9NHuXYhvJz1728^?^)0{#Fe+&T@0!tfIC=xycquhEi$YBe+_4eKUF3I1T3nXd z-2Et5=uKSVv(b}L{GD(t5>Jxm7a#M@AZlyiPKVdIlW`CbeSi(!Qp`y3)8Gj}a!Mch zlc|YUwXmnx_lS-)zbdz-61%Xj2dt7hP6NYvgWo03$KMU$_ES>jHn471;Jv5*G4v&} z*4t|uqQ;+-%;YfA2r(MTz(@%$o44Y^1T<-P3*!`h<~tl-_Y1JgjQGoI?3y#pb@H z@~dSCpBp_so5-$dNm(aMbdK36J6Xy|F1O!wmAup7NCvt1X&;wFv6TL2;MfrZgV(6Y z9eb&BKD|C0|D=^J-``DB$DoL-Og7Qtt*Wx?cwtmpj`c|1@AtBA-gv*MX?5m4``cl)iWcVvNMd4Rv%k7XnkXVzN%)Q!Q=tK3-QRB-bCGV zs_oH8&q@ZnuI*BQ*N|F&eXaXS`A4j|Uvi<3c(zB_!8Ms&*0SA;nCF}}x0sG2&g{F% zyeA{?FH27A<-%0zz|`gyih&C3i{}AmhamT_hvFY)W2y%yLX0I$|1z3XhDXw;xpWKE zVg4aTH2M9a-@%>PLbio(@^uu*y3trCF;hj)#HtN@}(iF(yjnsZQ5rrOT}cAX=qetm>08G zy2dlmNzpxj9xucnGVHT;dRk)o>{8ARjVqVr4O|txYpl4Zsni>Nz3KiD>T)p?8Da zDHC!OS4g*E`>>-$fP#^8LSOot}cybVj2d7eSEiKAsIA5W_7IaP#Mt5KIOS7oU` ze-!rR{WT+kIxk6tG@~fGyDZBwWR6aZgD3kEE9JIf7K}74#mVZ`XxK9PbH#+rBYS!N zr+6@r9@ckUFl$P9v)BD(Z54NhID*!J&LF8)E$dEoY|)HvxShU6-(id6xpd6F zO=M$f5$gq)1(sKfdh>Ki>)`7*h>Ew4=tUY0@1*U$`(&Y8 z4}mh4#mGk)z!GNEcmxqAao?k2)hfyqd$%Ud`g%`vBkr!kJmMF01Pc{3tQYs~0r9O& z3+st3|ro<5}=X}QJa)`&9ox$d9UeQ+~+=-bs zNt^LB{~d0o!Ljh`$Bmp+9hwiCBkas7vitk@{MO;d$VkMCXqEvYr}l1X%Sii;#mFc2 zx!^ZXWcU!<+crIkHO%}!0HaU7f+#S-_nSM!Kl9ct0kf(8@ajeRG65-9;69R{S)*B{ z=PefRVg%ejB6wD!Jx-KaTdLADHAV7giXkL<68=}e;!mY^a5BHkT5C{!jg};^Jw6K3 zrsD9$*1(+h!|v+raB#_|Iib@dS~YJH5D#{4!{L88;%m3IE;JA$qnXp|Wn1$X?&x-v z7dbFdvGOXpk9@t=+3iV+84zqQ&Q{jou(PMD1p6aIVJ%M!V=Ws_%Xlmzs}d}v_i)az z9Dn9Tau6e*&xGNnn{~ibY@H+%?__^_CztDaJzghVu9%?q&FAXK%__g$}7B?>CuuYwaA)SxXyxQU&0YL3VEbnMW23MWu*5eYVu)1FVd=p|7yhP;6pS?q|UU$H_y6Bs#ZtzeYuOWLZg7XTK1SX zKAUWyFsqBQYLbZq!!g6;NpIT8Y+gZPFV`%5*JnrD5#X4XipLGry3g13mBjZQ$OCNe zq~CI;p!?r)zfjtJOsVptH^t@g{-KW($l@Q1LT8W1IN}Gn%RO>B_#|JxFf<+{I4g#`w0)HxoWhIER3Yv??MhK1z$Cu8 z@r^2v9!G|6xS#2r$!w`81D3~)c8!Xh;;BTHy93L8Luqwkj_eLDL2=*+I3s6oLN_he z#J*RX+SO(-k;Lwf8Au!o)42Wg^s>EBHL5I_h+lNQC9EoX=d$#OtQ|z=@~FC^@nfy0 zm9Tm5Ev`%^13Tv9`~!-e)e1uvmq5r2mb~4n7Uq$cA$RzgrmYG-;w-iAKY<*1SOxA| z4P=*!Xl&M$)1CMo*;%wp@FX|{&rk5nKdI5kwV)dP*}~0dLTn41XoqeUUtww~2^on^ zf+=E>$C6v zZwk?SrEj;h0>Z=PQAc7TOz`P8&x(u9AN(~yEWmp3)rI@ zN7V3lUWJVbihI)Ofi~X{_cL2riJPqW?2$rPJUGRRN`98akG~)>D@R10$b-AUu=>wC z^I&N;q&($nXPXzgsf&rc(k=0cu_Ag?DSb>-Zkt(+5i$JIyaZrZ9 znRrt0<%75zd%EnNSsgf?y9n1JpdpZ!*y}p%vh+jL%IEk%q#+Ue8jqfs37(?B>iP=g znzrrzK$U^LhvImO>d2^7gXXpvnQ9Cz0R<>Q=WHwfx)p2s*sAV}-X*wJgmP7CyN#2x zhUWS^=rwpu`NJ1_H8q=iE~>TAF8-onoy|>aAEA~LfH!98M!Ks7vybN=isx5rKwof{ z%-k5CK(2%wSxVpPw6`B1T77S29&88CSk|A1o}Q1c5#?Jw&pNQbPZ%C&rP8ZvqSwQH z;~g{I8eynkdqD;{GU57?Xu+GZpU5m$T3o7$xZYi=mB@axPFoztJyMJ3Gr%<}TXL9Q zqxf(vT@&6(EoUU8nBcG7!y0tWTwfjGQ5WQ$U3^A9!^O__%i+&%TMbu=cjY; zp9{|IV|#`Re$Sa8Xzw7(<%QektMiv7T1+m7Pb7R=Y@lDA_y zr_`ZenP8LN3gJECkx3qWDdT>b(F!lSBep3;lbj zBftH=oLarhf~aPDqFvqc8yUZV4q9JSzErab!~BD)Y0aQLV8?aSZ9&3=HdLP&ne2ib z4qGyX=C%Q_;3lEef$wzxiL0mN)3rw#_``dbHl&KY%aJF}o)2FUTjsi*pc&|r`tfhb z5~*b@|Gab|3;osQ(d~qQF%cL)wcw+>paTuvLAwk4BlyZEvwJ|lI7yH=E^+1j{9)zc zN5vhp?^^u1iK>wh^lAsV`V6k)dH)Qz!tR1+!D2}IY~V5law|v;0_P;2iVw? z0Kx1t)EH_9SpU&37>Kx*^rcxJa?aUv!V-lyQ4~{g)Ze98Hra$MH+?dr%h-^tNBgYr zS3;3wV}H+xr>uq9bOEJUI}N4`MdiE1_e7K1+yO%sDD$G_#l!E)F@FU`t_3y&wnp%4 z{qkBOzcN$#yuzm0+pB@IMTu;%u=ZDb)2P|w^2+o+NM$E(58fWXW;lIi@_}-r5VbMe z)7yQ&eZ@taA!4Ty(^Q?VsG%nLU0VO+JdG$Lp6|NnE$7kYdb$JIAcWkUStLgCPW&Lo zIQ^+BbqNuX+?oRW9XsIu6!$1sd!879vmoz}Nh`}H8egN;`S<7LJm_zW!WL?T7LX_K zI~ko%g>A&Z|xVgdo z8D0faS_RT_z^oX)J29}NAe*ZXo{drVw{nu`LVPzZ$k|F_C}oQ!!d1Pxh%0^%J9ifk zrl7D4@|c4OW)>DKlM-#L)F=-}2F_>N~3j zFU9uznR!@^;~-|{3hkO&srO6_&D95AY!bu2+3XIK;tEgLk9Hk*{3qSwCaZ#7QV(l-V$>MP}3-9^9t}2#8%4)@;UYfna^~^ zmC1wD=40(M=s^J>+wyD*#v#21{rwE2N2X-&NLT<`8QJ)3-wnn`p_EPiN|YL)2A@AS zz4yEYuC4fmV`dA4LG8a}g^>Ch6O|$T8=%eIr=ZVm$J_hMupHBuwy3rj{7~CBK=JNB zG=^JD{dYJ2&;Z%P)cXD?rBL+QKQxsjq_y9KrF{3M7<59rJa)*!34Ii}U-sjQEO##Q z6ryHL`vK)(JMkd^-3|ViGI%9hDqes%?p=6I!1;!ej9|yYduE^(L!Hv0S|QCclY8fG z^81Qd67bnE_&3!VmH<|iXB{wcU5NhCqX^V6nrTZ2e-X@~CV8M9s|T}o_Jy*sN6}>) zIrWzERKeJ!&?))n(16yHVsx^+txbl%snimOq)a5iUGDAl_B)h)PEs^kLE98cGSBWg=n=jX7f@-&Tx^3JmqMih>-e#X!Vwv^tin zy!4#CoXTY70kwE&n3wKbs2JK^qoHU;nPmAp~zsTX#P1V*?h43uSME%scu2OJ$?>OI^(&@^) zl&?(FELVVRs^l z$)bf>WOIxgcNq|3=YDf0a&~+oz6%%aS@(+6VCUYq9{fIl#=;?tQMBu{xTx17i!1Ot zQt)Xv>-lX51yRlceX8S{e;1dW_;t#;YkY}U|2M8b>Mlw>T=mamq0a!Wo+ipMaMmTi zn>1PoL!S0RjqQ0W{AtL25g?h2%pL0C@*+{gOhaJzj}VFuURuBv_geG3E4mG-O4Xhi zCY?#75D~q5c5w*N?}N?0uJPfhf|JZ~e3QQGv}*V@D693N84e~R<_qjEtomamA}~4& z{i|G8QM0K0n)qU?S8-9UW5k)z(>dbD;(dfRolH^1OJ~pA>0{LAjQ?hr7`s09pjz~Ck$&Ln*0wwiD(W~V!E&3@xJVBIAzIe+okU->dXrQ z0(b1Wy(R;1EO`OFuLtF-pDSG-#=P`yNRx<**e~-}dFs8(bo&nkq`n79rx!1#UiY13 zA{s*&sbmf`RgDI=47%(gk$54SUzdQ)2Zt@q7(f-Xim@zG#Z;6O$K6P|NYxeG<%0_Xt_@$WNAN<9Ee|8|%;eQ~I<;(q%$}A!>xo3(w`i_Q zNBYydUY_0vQ^ua?^?fO5tGD;$LZvVTMm<>r-7N5d;{mg`^{yk)C>8$%5{!Cs-8pQq|`;-O?lvVlN|5riTV3u{tr%$ z$zmt8DE>;bCX2}llG=fs>(6>7L&e|RNCBV(8U#@58ZuS0fX|Y*GXz9SlKk=A1>1}?0 zvg>ESHk~h(RGl%fk{SwwEMyEqs1C~XexXpGzwnv@fSxazj#c!MO_N42F`Rx&vdQvV`r|FWp_zIY|e0 zexBEXub9}-GrTU#qt=&M^J9u!HdM5(BMb@+rYm>KK0K+iCR`O8L|TpZ;0ivsAsil7>pi7@A2nSk5tJ=5Kb(Kq3(OjN_)|3+@PSLKhkBVnj}X(z#- ztC3@I^{p5fEFKGy-C?H<8l5A*yX!2M8iatrFOrMje|q==RIyM3RJBb39}*GT!nBlO zs08}EPzmMCn?s;U-uFV`3dB&|^jwIfkN>56G}AZz@H?sR3KFvg&lSQu!q1LFYMM#E zNC=Fdmhcs}zxV26r$%TMD*DDjooeZSl$(%WZ(I8wchLxV+g6{M$iZ@_CE+8Pgj)xV zW z=xq4)U&u_7ui82M83559m8Y*Ek7-r>%ZHy#fR_wbTw4b%b}wv0JSASIXh0fUr{|EP zzwLMAIk=jeN=vWZd;0&ZxLkIOfQKzob-(EjO`hCCQXAA9N+I-^q3WqUUe+nouHEZa z!@<257%lrtC)8qZ{lueuri|#gD3U6jwLPy|O2O9KzRWUz>oeGXu?;l+&N>1MdFPxo ztBIbLxD&5OaVtO_7iuVR=sk&b>GN8HVhw-q*bTL)$qIBB(WxC)dP;p#eD(wvQPKR$ zx@g`5)=kge5^~HvSaa%U#=djmMSd||RQT4JCGX~7%VL-H^f@1!3L6S>fAGB2Aq{R& zQ=M`T;_~i~h>0c0XBa6n)a$eyWPit|t_6U8sQXTjPI#QM> z1KP0)j?{)P%{@OJoy)|Gc$WJokmpQ|?lo$*ylrE0i0fsDqR}I6x-8>WdbCk*$ZtZT z=T_8D$t-#bnI7P{ANPpwX8=r#31a6<+)9hA@968ai!UR~ORPaRWuhrLui8bf^3I** zHWcNVQ|RJjdL^m_4WvR2q3@kumK&JoM0%J=#9xp{h?pXB1fTI>qWQV}Rwg;xlUF}Q z-&l^$e&?h)6fyQw-cf;nX~!Pp(T2M_1T7zT)$%%z#Zu9+=*bQ&fu>3D87r;`bFkPK z>j5Hns{@&FMr{Qar=42sU36O4oL3*jES6(3x4MZs?P7ayAcz`{W3KcV1~E@05EvMQ^o=_V1MN<2X2n7v8nAPw|7wFzfmGLhgz8U0H8H`q#|4B8eR#f&2OC zy14f(Y&7~uM|OF&QAvZJnzC^jxOn*{0!>HLOPk#ggEmDNoj``DH%BBW7y9A7c+X(H zUM-sIzVE@Qg~_e;8DHlz`wW@Jtb7d4OBOj+;}U>D2Oz zneX>qOh-9FcbP*z5Lp1ac7^`RAW?fGS4Tb#7omBPk)}2FcR_)l-wrS}wfNO9$nCtC z?42pGYMfvWA-RYbb();X>|X=wOLs8U0C#+Alvj55f&4hKIbywQb>oM1gMfXn3hl=N z=2+!(r51*+=T7`L&+B-8KW1bZkAnN(yT#8`0~M056tZ=&oJ)8KH5yJ&Lbq^- z#*Xwh3WId9&ch!gGCi9i*w?>3CKioF%x5$hNeSXJK&!Yg?xN~= z=#z`KJ2jXRCd!D#v1FTt@Cx^ncUfqRdGN6qB)f%%Z8tym0Fu2 znrmSZ%(0o@0y!_y{=7W$|J_0>p{rAEnAvo`xK$jPuQ2~U}q zBd8v{Foc)?sDZ*`Z3R#kxyN(%{0C~#|H8#AtMDH)yVgokRECE5-ZN8q=2HECvU(YX zOKa*rIM=R1dixDm2`(8DM8DZ*S=3SNcH*l>+>i`&Xae@5mr3}4dlY(=wLLiIwMqIc{kFdBzDr@RgEz{J zH2$G!?}F|;P-{>JEtFbUr;5b3E?UAO4OGdat(aj z>Yuw1TC(+=J?tstEDI+z5+a;WZ@0zapT_=xS>_#~-b86@MRDLN{JNV(+&c1shM$ja z!2{VX32~)$suB&xd&=lu!+a&@ydpY=y2j)K2UB1v2LXo=%T~>Er=Q(;$X(Rxru5*d zkQ_Z=p^Y3zTexork%S&f$KJcWy?BGz8Yj}mz1R5uwD&;%*XHZD(G_BE)nkF;9tmF| z{lMf)(XIPA1r+(za{FdtWbl$^ghejJ`?k3kNA|bfV zeTY)noIg5W=5fSPhgl*}6JxL}mkH}_CWAM5uN0nF=m&l&Wuf1G4*2(C2t4jmS~I6M zzmA<~;t!`~2meDen;GOTRExhNuc)U8a;h$OTIB&U?KN)f9C)3#Qorqrj?41MIC74h zvNZ5pdLcw8d#nS(MMe$0xS0xO`D3wVfaOdQ818&}a2X@An4i}%Tnzq0qo?4?2puw) zlZ+^+;uc<1xSzUxLWd)~9bnKoYq<)$48_Py5c5l*fqCTcP}K|2wAIMk<5f_dOi+-G zpt3aI#2v<++tc1p=N%jJ*$IWi3D zzuYcfczw68Mmt>*g%OdsAk!-cnWp?Z+gW(eovUrSCeA)wEy%N&EcMs5z|LfT-}uSrg9nc~uwGRoH(!hVeFW_Mqal@_Qw*6q0IfAKFdQ&swDDx~ zazVX1Q8-`+vl-2#2i1c$By^QHYVP<2Q(FDWi7|hKpr6370fi9BJFF85#LB9H2xfYI z#7SqlYlc|e%Qs&34C!zFlG$!N29B;7btkG^egqDJ-uq0iLuJwUUuc2TK>S2_R8OzS zS{DFb#d}iqK#WBgF>#6wt)IJD4Ta0w#K&b=YyJqb?SL~fvef9dp&2NC05}snWKsK4 z!B)lAn zP(>GBFC6tHC#+O>W-%GCr<739qci&_%aYcm5FMkS>=*uqZJ+7~(==D+mN}5w$&YiX z*u17+>AN&I+p4^Lj{}?C8>b>(@M)X35Ieux#Y;yj6N?mxuSOgYuVSCHaBdYzK(TO- zAlM1W{M(X^qI|a@r!~#b2&7jy1IjE;_#x|A?DU$weN8D5e{fKw%uu9bbB3U|Ex0&AswWkL0h4#KX+~CiD*`7%pQt$U0wxl}JFtLY2n;4! zb6hdtaN$y2C2(?Dj12t&sXN}3d*9iaJ9Y*$o|2=(sylgi;t&Dv$yUzGI>QxSYuj4U zn*X%$<4(TE)3O^(VE7~>xNKN|rKFct+mPfs1brDWdPXd%x+`L6{ifloU~94@*gNTF zcIRHZiT%gX8;ol zIRFJCoByps_g%nk*+z+~{1)_JejVq^zFTYf#g;}MCghcMq_G)zKJ_hf6|;I{Pop3@ z`e(PFA_C&jaDQCHN*WN0CI)@)fl0qq_*)Q{<@V@?zNx4-)rm%KGC`b}_WQeq6V7EH567zn>IN=2&m5s4RSEKy!OLl$X8 zrJj6TbP*PiNXKu@cc2P~pNugiE$2>7IBSu`>d??9(O3w6cm$xd1MOhMPoaJ@XVI`m zzl6h(C-Fymx|prwatZyf$Qg2dnyc>GD*nUi#IIsB4KDsG5}`x&wx#lP=MOE^zx=2Q zRdiXw_fddQ!RWA7%S3{88PdJ6zV#1W6{>&6_4t(EM!_Cd!N1Z#@!vlozl9P4(S&z~ zsHYdwet`P>**mVEdC|f6q#!^4nTlNooMsC8%1=nK@%0DN&TG#F`B3+tSzi(=(gH_- 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 const SizedBox(height: 8), - itemBuilder: (context, index) { - final item = components[index]; - return ListTile( - tileColor: const Color(0xFF2A2D3A), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - side: const BorderSide(color: Color(0xFF3D4155), width: 2), - ), - title: Text( - item.name, - style: GoogleFonts.pressStart2p( - fontSize: 10, - color: Colors.white, - ), - ), - trailing: const Icon(Icons.chevron_right, color: Colors.cyanAccent), - onTap: () => Navigator.push( - context, - MaterialPageRoute(builder: (_) => item.page), + body: Stack( + children: [ + // Grit background + Positioned.fill( + child: PixelPanel( + width: double.infinity, + height: double.infinity, + style: PixelPanelStyle.grit, + borderThickness: 0, + child: const SizedBox.expand(), ), - ); + ), + // List content + ListView.separated( + padding: const EdgeInsets.all(16), + itemCount: components.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final item = components[index]; + return _PixelListTile( + title: item.name, + onTap: () => Navigator.push( + context, + MaterialPageRoute(builder: (_) => item.page), + ), + ); + }, + ), + ], + ), + ); + } +} + +/// Pixel-perfect stepped corner list tile +class _PixelListTile extends StatefulWidget { + final String title; + final VoidCallback? onTap; + + const _PixelListTile({ + required this.title, + this.onTap, + }); + + @override + State<_PixelListTile> createState() => _PixelListTileState(); +} + +class _PixelListTileState extends State<_PixelListTile> { + bool _isPressed = false; + bool _isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + cursor: SystemMouseCursors.click, + onEnter: (_) => setState(() => _isHovered = true), + onExit: (_) => setState(() => _isHovered = false), + child: GestureDetector( + onTapDown: (_) => setState(() => _isPressed = true), + onTapUp: (_) { + setState(() => _isPressed = false); + widget.onTap?.call(); }, + onTapCancel: () => setState(() => _isPressed = false), + child: CustomPaint( + painter: _PixelTilePainter( + backgroundColor: _isPressed + ? const Color(0xFF1A1C24) + : _isHovered + ? const Color(0xFF353848) + : const Color(0xFF2A2D3A), + borderColor: const Color(0xFF3D4155), + highlightColor: const Color(0xFF4A4E65), + shadowColor: const Color(0xFF1A1C24), + pixelSize: 2.0, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Expanded( + child: Text( + widget.title, + style: GoogleFonts.pressStart2p( + fontSize: 10, + color: Colors.white, + ), + ), + ), + const Icon(Icons.chevron_right, color: Colors.cyanAccent, size: 20), + ], + ), + ), + ), ), ); } } +/// Pixel-perfect stepped corner painter for list tiles +class _PixelTilePainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color highlightColor; + final Color shadowColor; + final double pixelSize; + + _PixelTilePainter({ + required this.backgroundColor, + required this.borderColor, + required this.highlightColor, + required this.shadowColor, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with 3D bevel === + drawHLine(p * 2, w - p * 2, p, highlightColor); + drawVLine(p, p * 2, h - p * 2, highlightColor); + drawHLine(p * 2, w - p * 2, h - p * 2, shadowColor); + drawVLine(w - p * 2, p * 2, h - p * 2, shadowColor); + + // Inner corner pixels for bevel + drawPixel(p, p, highlightColor); + drawPixel(w - p * 2, p, highlightColor); + drawPixel(p, h - p * 2, shadowColor); + drawPixel(w - p * 2, h - p * 2, shadowColor); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + } + + @override + bool shouldRepaint(covariant _PixelTilePainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.highlightColor != highlightColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.pixelSize != pixelSize; + } +} + class ComponentItem { final String name; final Widget page; @@ -139,65 +293,97 @@ class ComponentDemoPage extends StatelessWidget { // ============== Individual Component Pages ============== -class PixelButtonPage extends StatelessWidget { +class PixelButtonPage extends StatefulWidget { const PixelButtonPage({super.key}); + @override + State createState() => _PixelButtonPageState(); +} + +class _PixelButtonPageState extends State { + bool _useSteppedCorners = true; + @override Widget build(BuildContext context) { final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); return ComponentDemoPage( title: 'PixelButton', - child: Wrap( - spacing: 16, - runSpacing: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - PixelButton( - label: 'DEFAULT', - textStyle: pixelFont, - onPressed: () {}, - ), - PixelButton( - label: 'CYAN OUTLINE', - textStyle: pixelFont, - onPressed: () {}, - borderDark: Colors.cyanAccent, - ), - PixelButton( - label: 'GREEN BUTTON', - textStyle: pixelFont, - onPressed: () {}, - borderDark: Colors.greenAccent, - color: const Color(0xFF1A3A2A), - hoverColor: const Color(0xFF2A4A3A), - pressedColor: const Color(0xFF0A2A1A), - ), - PixelButton( - label: 'RED BUTTON', - textStyle: pixelFont, - onPressed: () {}, - borderDark: Colors.redAccent, - color: const Color(0xFF3A1A1A), - hoverColor: const Color(0xFF4A2A2A), - pressedColor: const Color(0xFF2A0A0A), - ), - PixelButton( - label: 'DISABLED', - textStyle: pixelFont, - enabled: false, - ), - PixelButton( - label: 'WITH GLOW', - textStyle: pixelFont, - onPressed: () {}, - enableGlowAnimation: true, - borderDark: Colors.purpleAccent, - ), - PixelButton( - label: 'WITH SCANLINES', - textStyle: pixelFont, - onPressed: () {}, - enableScanlineAnimation: true, - borderDark: Colors.amber, + Row( + children: [ + Text('STEPPED CORNERS:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _useSteppedCorners, + onChanged: (v) => setState(() => _useSteppedCorners = v), + onColor: Colors.cyanAccent, + ), + ], + ), + const SizedBox(height: 24), + Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelButton( + label: 'DEFAULT', + textStyle: pixelFont, + onPressed: () {}, + useSteppedCorners: _useSteppedCorners, + ), + PixelButton( + label: 'CYAN OUTLINE', + textStyle: pixelFont, + onPressed: () {}, + borderDark: Colors.cyanAccent, + useSteppedCorners: _useSteppedCorners, + ), + PixelButton( + label: 'GREEN BUTTON', + textStyle: pixelFont, + onPressed: () {}, + borderDark: Colors.greenAccent, + color: const Color(0xFF1A3A2A), + hoverColor: const Color(0xFF2A4A3A), + pressedColor: const Color(0xFF0A2A1A), + useSteppedCorners: _useSteppedCorners, + ), + PixelButton( + label: 'RED BUTTON', + textStyle: pixelFont, + onPressed: () {}, + borderDark: Colors.redAccent, + color: const Color(0xFF3A1A1A), + hoverColor: const Color(0xFF4A2A2A), + pressedColor: const Color(0xFF2A0A0A), + useSteppedCorners: _useSteppedCorners, + ), + PixelButton( + label: 'DISABLED', + textStyle: pixelFont, + enabled: false, + useSteppedCorners: _useSteppedCorners, + ), + PixelButton( + label: 'WITH GLOW', + textStyle: pixelFont, + onPressed: () {}, + enableGlowAnimation: true, + borderDark: Colors.purpleAccent, + glowColor: Colors.purpleAccent, + useSteppedCorners: _useSteppedCorners, + ), + PixelButton( + label: 'WITH SCANLINES', + textStyle: pixelFont, + onPressed: () {}, + enableScanlineAnimation: true, + borderDark: Colors.amber, + useSteppedCorners: _useSteppedCorners, + ), + ], ), ], ), @@ -281,60 +467,153 @@ class PixelTextPage extends StatelessWidget { } } -class PixelPanelPage extends StatelessWidget { +class PixelPanelPage extends StatefulWidget { const PixelPanelPage({super.key}); + @override + State createState() => _PixelPanelPageState(); +} + +class _PixelPanelPageState extends State { + bool _showImageInCRT = true; + @override Widget build(BuildContext context) { final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); return ComponentDemoPage( title: 'PixelPanel', - child: Wrap( - spacing: 16, - runSpacing: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - PixelPanel( - width: 180, - height: 100, - style: PixelPanelStyle.pixelOutline, - borderColor: Colors.cyanAccent, - child: Center( - child: Text('PIXEL\nOUTLINE', style: pixelFont, textAlign: TextAlign.center), - ), - ), - PixelPanel( - width: 180, - height: 100, - style: PixelPanelStyle.glowingBorder, - borderColor: Colors.greenAccent, - child: Center( - child: Text('GLOWING\nBORDER', style: pixelFont, textAlign: TextAlign.center), - ), + Text('PANEL STYLES', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.pixelOutline, + borderColor: Colors.cyanAccent, + child: Center( + child: Text('PIXEL\nOUTLINE', style: pixelFont, textAlign: TextAlign.center), + ), + ), + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.glowingBorder, + borderColor: Colors.greenAccent, + child: Center( + child: Text('GLOWING\nBORDER', style: pixelFont, textAlign: TextAlign.center), + ), + ), + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.oldScreenCRT, + borderColor: Colors.amber, + child: Center( + child: Text( + 'OLD CRT\nSCREEN', + style: pixelFont.copyWith(color: Colors.amber), + textAlign: TextAlign.center, + ), + ), + ), + PixelPanel( + width: 180, + height: 100, + style: PixelPanelStyle.paperGrain, + child: Center( + child: Text( + 'PAPER\nGRAIN', + style: pixelFont.copyWith(color: Colors.black87), + textAlign: TextAlign.center, + ), + ), + ), + ], ), - PixelPanel( - width: 180, - height: 100, - style: PixelPanelStyle.oldScreenCRT, - borderColor: Colors.amber, - child: Center( - child: Text( - 'OLD CRT\nSCREEN', - style: pixelFont.copyWith(color: Colors.amber), - textAlign: TextAlign.center, + const SizedBox(height: 32), + const Divider(color: Colors.white24), + const SizedBox(height: 16), + Text('CRT SCREEN WITH IMAGE', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + Row( + children: [ + Text('SHOW IMAGE:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _showImageInCRT, + onChanged: (v) => setState(() => _showImageInCRT = v), + onColor: Colors.amber, ), - ), + ], ), - PixelPanel( - width: 180, - height: 100, - style: PixelPanelStyle.paperGrain, - child: Center( - child: Text( - 'PAPER\nGRAIN', - style: pixelFont.copyWith(color: Colors.black87), - textAlign: TextAlign.center, + const SizedBox(height: 16), + Row( + children: [ + // Image without CRT overlay + Column( + children: [ + Container( + width: 160, + height: 140, + decoration: BoxDecoration( + border: Border.all(color: Colors.amber, width: 2), + ), + child: ClipRRect( + child: Image.asset( + 'assets/landscape.jpeg', + fit: BoxFit.cover, + ), + ), + ), + const SizedBox(height: 8), + Text('WITHOUT CRT', style: pixelFont.copyWith(fontSize: 8)), + ], ), - ), + const SizedBox(width: 24), + // Image with CRT overlay + Column( + children: [ + Container( + width: 160, + height: 140, + decoration: BoxDecoration( + border: Border.all(color: Colors.cyanAccent, width: 2), + ), + child: Stack( + children: [ + // Base image + Positioned.fill( + child: Image.asset( + 'assets/landscape.jpeg', + fit: BoxFit.cover, + ), + ), + // Animated CRT overlay with fuzzy noise during transition + Positioned.fill( + child: AnimatedCRTOverlay( + visible: _showImageInCRT, + duration: const Duration(milliseconds: 600), + scanlineOpacity: 0.25, + scanlineSpacing: 3.0, + noiseIntensity: 0.08, + noisePixelSize: 3.0, + noiseAnimationFps: 20, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + Text('WITH CRT', style: pixelFont.copyWith(fontSize: 8)), + ], + ), + ], ), ], ), @@ -353,6 +632,11 @@ class _PixelSliderPageState extends State { double _value1 = 0.5; double _value2 = 0.3; double _value3 = 0.7; + double _value4 = 0.6; + double _value5 = 0.4; + double _valueV1 = 0.7; + double _valueV2 = 0.5; + double _valueV3 = 0.3; @override Widget build(BuildContext context) { @@ -396,6 +680,93 @@ class _PixelSliderPageState extends State { inactiveColor: const Color(0xFF3D4155), ), ), + const SizedBox(height: 32), + const Divider(color: Colors.white24), + const SizedBox(height: 16), + Text('PIXEL BORDER SLIDER', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + Text('CYAN BORDER: ${(_value4 * 100).toInt()}%', style: pixelFont), + const SizedBox(height: 8), + SizedBox( + width: 300, + child: PixelBorderSlider( + value: _value4, + onChanged: (v) => setState(() => _value4 = v), + activeColor: Colors.cyanAccent, + borderColor: Colors.cyanAccent, + divisions: 10, + ), + ), + const SizedBox(height: 24), + Text('PURPLE BORDER: ${(_value5 * 100).toInt()}%', style: pixelFont), + const SizedBox(height: 8), + SizedBox( + width: 300, + child: PixelBorderSlider( + value: _value5, + onChanged: (v) => setState(() => _value5 = v), + activeColor: Colors.purpleAccent, + borderColor: Colors.purpleAccent, + ), + ), + const SizedBox(height: 32), + const Divider(color: Colors.white24), + const SizedBox(height: 16), + Text('VERTICAL SLIDERS', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + SizedBox( + height: 150, + child: PixelVerticalSlider( + value: _valueV1, + onChanged: (v) => setState(() => _valueV1 = v), + activeColor: Colors.cyanAccent, + borderColor: Colors.cyanAccent, + ), + ), + const SizedBox(height: 8), + Text('${(_valueV1 * 100).toInt()}%', style: pixelFont), + ], + ), + Column( + children: [ + SizedBox( + height: 150, + child: PixelVerticalSlider( + value: _valueV2, + onChanged: (v) => setState(() => _valueV2 = v), + activeColor: Colors.greenAccent, + borderColor: Colors.greenAccent, + divisions: 5, + ), + ), + const SizedBox(height: 8), + Text('${(_valueV2 * 100).toInt()}%', style: pixelFont), + ], + ), + Column( + children: [ + SizedBox( + height: 150, + child: PixelVerticalSlider( + value: _valueV3, + onChanged: (v) => setState(() => _valueV3 = v), + activeColor: Colors.purpleAccent, + borderColor: Colors.purpleAccent, + width: 32, + pixelSize: 3.0, + ), + ), + const SizedBox(height: 8), + Text('${(_valueV3 * 100).toInt()}%', style: pixelFont), + ], + ), + ], + ), ], ), ); @@ -540,56 +911,151 @@ class _PixelProgressBarPageState extends State { activeColor: Colors.cyanAccent, ), ), + const SizedBox(height: 32), + const Divider(color: Colors.white24), + const SizedBox(height: 16), + Text('VERTICAL PROGRESS BARS', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + PixelVerticalProgressBar( + progress: _progress, + fillColor: Colors.cyanAccent, + borderColor: Colors.cyanAccent, + height: 120, + ), + const SizedBox(height: 8), + Text('CYAN', style: pixelFont.copyWith(fontSize: 8)), + ], + ), + Column( + children: [ + PixelVerticalProgressBar( + progress: _progress, + fillColor: Colors.greenAccent, + borderColor: Colors.greenAccent, + height: 120, + segments: 5, + ), + const SizedBox(height: 8), + Text('SEGMENTED', style: pixelFont.copyWith(fontSize: 8)), + ], + ), + Column( + children: [ + PixelVerticalProgressBar( + progress: _progress, + fillColor: Colors.purpleAccent, + borderColor: Colors.purpleAccent, + height: 120, + showGlow: true, + ), + const SizedBox(height: 8), + Text('GLOW', style: pixelFont.copyWith(fontSize: 8)), + ], + ), + Column( + children: [ + PixelVerticalProgressBar( + progress: _progress, + fillColor: Colors.amber, + borderColor: Colors.amber, + height: 120, + width: 32, + pixelSize: 3.0, + ), + const SizedBox(height: 8), + Text('WIDE', style: pixelFont.copyWith(fontSize: 8)), + ], + ), + ], + ), ], ), ); } } -class PixelNotificationCardPage extends StatelessWidget { +class PixelNotificationCardPage extends StatefulWidget { const PixelNotificationCardPage({super.key}); + @override + State createState() => _PixelNotificationCardPageState(); +} + +class _PixelNotificationCardPageState extends State { + bool _useSteppedBorder = false; + @override Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); return ComponentDemoPage( title: 'PixelNotificationCard', - child: Wrap( - spacing: 16, - runSpacing: 16, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - PixelNotificationCard( - title: 'John Doe', - action: 'Liked your post', - avatar: const PixelAvatar(), - titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), - actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), - onTap: () {}, - ), - PixelNotificationCard( - title: 'Jane Smith', - action: 'Commented', - avatar: const PixelAvatar(backgroundColor: Color(0xFF4A4E65)), - titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), - actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), - onTap: () {}, - ), - PixelNotificationCard( - title: 'Alex Johnson', - action: 'Followed you', - avatar: const PixelAvatar(backgroundColor: Color(0xFF3D5A80)), - titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), - actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), - showScanlines: true, - onTap: () {}, - ), - PixelNotificationCard( - title: 'Sara Wilson', - action: 'Shared a post', - avatar: const PixelAvatar(backgroundColor: Color(0xFF5A3D80)), - titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), - actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), - width: 320, - onTap: () {}, + Row( + children: [ + Text('STEPPED BORDER:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _useSteppedBorder, + onChanged: (v) => setState(() => _useSteppedBorder = v), + onColor: Colors.cyanAccent, + ), + ], + ), + const SizedBox(height: 24), + Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelNotificationCard( + title: 'John Doe', + action: 'Liked your post', + avatar: const PixelAvatar(), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + useSteppedBorder: _useSteppedBorder, + borderColor: Colors.cyanAccent, + onTap: () {}, + ), + PixelNotificationCard( + title: 'Jane Smith', + action: 'Commented', + avatar: const PixelAvatar(backgroundColor: Color(0xFF4A4E65)), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + useSteppedBorder: _useSteppedBorder, + borderColor: Colors.greenAccent, + onTap: () {}, + ), + PixelNotificationCard( + title: 'Alex Johnson', + action: 'Followed you', + avatar: const PixelAvatar(backgroundColor: Color(0xFF3D5A80)), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + showScanlines: true, + useSteppedBorder: _useSteppedBorder, + borderColor: Colors.purpleAccent, + onTap: () {}, + ), + PixelNotificationCard( + title: 'Sara Wilson', + action: 'Shared a post', + avatar: const PixelAvatar(backgroundColor: Color(0xFF5A3D80)), + titleStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white), + actionStyle: GoogleFonts.pressStart2p(fontSize: 7, color: const Color(0xFF8A8E9E)), + width: 320, + useSteppedBorder: _useSteppedBorder, + borderColor: Colors.amber, + onTap: () {}, + ), + ], ), ], ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 996c45d..815dbf1 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -20,3 +20,5 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/landscape.jpeg diff --git a/lib/src/widgets/pixel_button.dart b/lib/src/widgets/pixel_button.dart index 3efeb2c..0001a4a 100644 --- a/lib/src/widgets/pixel_button.dart +++ b/lib/src/widgets/pixel_button.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; /// PixelButton with authentic retro stepped-corner border -/// Replicates classic game UI button style with pixel-perfect edges +/// Two borders: outer dark, inner with 3D bevel effect class PixelButton extends StatefulWidget { final VoidCallback? onPressed; final bool enabled; @@ -13,16 +13,17 @@ class PixelButton extends StatefulWidget { final Color pressedColor; final Color disabledColor; - // Border colors for 3D effect (outer to inner) - final Color borderDark; // Darkest outer edge - final Color borderMid; // Middle border - final Color borderLight; // Inner highlight - + // Border colors + final Color borderDark; // Outer edge color final double pixelSize; // Size of each "pixel" in the border final bool enableGlowAnimation; + final Color? glowColor; final bool enableScanlineAnimation; + /// Use stepped corners with corner pixels (true) or smooth edges (false) + final bool useSteppedCorners; + const PixelButton({ Key? key, required this.label, @@ -30,25 +31,28 @@ class PixelButton extends StatefulWidget { this.enabled = true, this.textStyle, this.color = const Color(0xFF4A4D5E), - this.hoverColor = const Color(0xFF6A6D7E), // Noticeably lighter on hover - this.pressedColor = const Color(0xFF2A2D3E), // Darker when pressed + this.hoverColor = const Color(0xFF6A6D7E), + this.pressedColor = const Color(0xFF2A2D3E), this.disabledColor = const Color(0xFF555555), - this.borderDark = const Color(0xFF000000), // Pure black outer edge - this.borderMid = const Color(0xFF1A1C24), // Very dark middle - this.borderLight = const Color(0xFF8A8D9E), // Light inner highlight + this.borderDark = const Color(0xFF000000), this.pixelSize = 2.0, this.enableGlowAnimation = false, + this.glowColor, this.enableScanlineAnimation = false, + this.useSteppedCorners = true, }) : super(key: key); @override State createState() => _PixelButtonState(); } -class _PixelButtonState extends State { +class _PixelButtonState extends State with SingleTickerProviderStateMixin { bool _hovering = false; bool _pressed = false; + AnimationController? _glowController; + Animation? _glowAnimation; + Color get _backgroundColor { if (!widget.enabled) return widget.disabledColor; if (_pressed) return widget.pressedColor; @@ -56,6 +60,38 @@ class _PixelButtonState extends State { return widget.color; } + Color get _borderLight { + // Lighter than background for top-left inner edge + final hsl = HSLColor.fromColor(_backgroundColor); + return hsl.withLightness((hsl.lightness + 0.2).clamp(0.0, 1.0)).toColor(); + } + + Color get _borderShadow { + // Darker than background for bottom-right inner edge + final hsl = HSLColor.fromColor(_backgroundColor); + return hsl.withLightness((hsl.lightness - 0.2).clamp(0.0, 1.0)).toColor(); + } + + @override + void initState() { + super.initState(); + if (widget.enableGlowAnimation) { + _glowController = AnimationController( + duration: const Duration(milliseconds: 1500), + vsync: this, + )..repeat(reverse: true); + _glowAnimation = Tween(begin: 0.3, end: 1.0).animate( + CurvedAnimation(parent: _glowController!, curve: Curves.easeInOut), + ); + } + } + + @override + void dispose() { + _glowController?.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final textStyle = widget.textStyle ?? @@ -65,6 +101,60 @@ class _PixelButtonState extends State { color: Colors.white, ); + Widget buttonContent = CustomPaint( + painter: _PixelBorderPainter( + backgroundColor: _backgroundColor, + borderDark: widget.borderDark, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + useSteppedCorners: widget.useSteppedCorners, + ), + child: Padding( + padding: EdgeInsets.all(widget.pixelSize * 3 + 8), + child: Text(widget.label, style: textStyle), + ), + ); + + // Add scanlines if enabled + if (widget.enableScanlineAnimation) { + buttonContent = Stack( + children: [ + buttonContent, + Positioned.fill( + child: ClipRRect( + child: CustomPaint( + painter: _ScanlinePainter(), + ), + ), + ), + ], + ); + } + + // Add glow if enabled + if (widget.enableGlowAnimation && _glowAnimation != null) { + final glowColor = widget.glowColor ?? widget.borderDark; + buttonContent = AnimatedBuilder( + animation: _glowAnimation!, + builder: (context, child) { + return Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: glowColor.withOpacity(_glowAnimation!.value * 0.6), + blurRadius: 12 * _glowAnimation!.value, + spreadRadius: 2 * _glowAnimation!.value, + ), + ], + ), + child: child, + ); + }, + child: buttonContent, + ); + } + return MouseRegion( cursor: widget.enabled ? SystemMouseCursors.click : SystemMouseCursors.basic, onEnter: (_) => setState(() => _hovering = true), @@ -78,47 +168,36 @@ class _PixelButtonState extends State { } : null, onTapCancel: () => setState(() => _pressed = false), - child: CustomPaint( - painter: _PixelBorderPainter( - backgroundColor: _backgroundColor, - borderDark: widget.borderDark, - borderMid: widget.borderMid, - borderLight: widget.borderLight, - pixelSize: widget.pixelSize, - ), - child: Padding( - padding: EdgeInsets.all(widget.pixelSize * 4 + 8), - child: Text(widget.label, style: textStyle), - ), - ), + child: buttonContent, ), ); } } -/// Draws pixel-perfect stepped corner borders +/// Draws pixel-perfect stepped corner borders - 2 borders only class _PixelBorderPainter extends CustomPainter { final Color backgroundColor; final Color borderDark; - final Color borderMid; final Color borderLight; + final Color borderShadow; final double pixelSize; + final bool useSteppedCorners; _PixelBorderPainter({ required this.backgroundColor, required this.borderDark, - required this.borderMid, required this.borderLight, + required this.borderShadow, required this.pixelSize, + this.useSteppedCorners = true, }); @override void paint(Canvas canvas, Size size) { - final double p = pixelSize; // Pixel unit size + final double p = pixelSize; final double w = size.width; final double h = size.height; - // Helper to draw a pixel void drawPixel(double x, double y, Color color) { canvas.drawRect( Rect.fromLTWH(x, y, p, p), @@ -126,7 +205,6 @@ class _PixelBorderPainter extends CustomPainter { ); } - // Helper to draw horizontal line of pixels void drawHLine(double x1, double x2, double y, Color color) { canvas.drawRect( Rect.fromLTWH(x1, y, x2 - x1, p), @@ -134,7 +212,6 @@ class _PixelBorderPainter extends CustomPainter { ); } - // Helper to draw vertical line of pixels void drawVLine(double x, double y1, double y2, Color color) { canvas.drawRect( Rect.fromLTWH(x, y1, p, y2 - y1), @@ -142,67 +219,78 @@ class _PixelBorderPainter extends CustomPainter { ); } - // === LAYER 1: Outermost dark border === - // Top edge (with corner cutouts) - drawHLine(p * 2, w - p * 2, 0, borderDark); - // Bottom edge - drawHLine(p * 2, w - p * 2, h - p, borderDark); - // Left edge - drawVLine(0, p * 2, h - p * 2, borderDark); - // Right edge - drawVLine(w - p, p * 2, h - p * 2, borderDark); - - // Corner pixels for stepped effect - drawPixel(p, p, borderDark); - drawPixel(w - p * 2, p, borderDark); - drawPixel(p, h - p * 2, borderDark); - drawPixel(w - p * 2, h - p * 2, borderDark); - - // === LAYER 2: Middle border === - // Top edge - drawHLine(p * 3, w - p * 3, p, borderMid); - // Bottom edge - drawHLine(p * 3, w - p * 3, h - p * 2, borderMid); - // Left edge - drawVLine(p, p * 3, h - p * 3, borderMid); - // Right edge - drawVLine(w - p * 2, p * 3, h - p * 3, borderMid); - - // Corner pixels - drawPixel(p * 2, p * 2, borderMid); - drawPixel(w - p * 3, p * 2, borderMid); - drawPixel(p * 2, h - p * 3, borderMid); - drawPixel(w - p * 3, h - p * 3, borderMid); - - // === LAYER 3: Inner highlight border === - // Top edge - drawHLine(p * 4, w - p * 4, p * 2, borderLight); - // Left edge - drawVLine(p * 2, p * 4, h - p * 4, borderLight); - - // Corner pixels - drawPixel(p * 3, p * 3, borderLight); - - // === LAYER 4: Background fill === - final bgPaint = Paint()..color = backgroundColor; + if (useSteppedCorners) { + // === BORDER 1: Outer border with stepped corners === + // Top edge (with corner cutouts) + drawHLine(p * 2, w - p * 2, 0, borderDark); + // Bottom edge + drawHLine(p * 2, w - p * 2, h - p, borderDark); + // Left edge + drawVLine(0, p * 2, h - p * 2, borderDark); + // Right edge + drawVLine(w - p, p * 2, h - p * 2, borderDark); + + // Corner pixels for stepped effect + drawPixel(p, p, borderDark); + drawPixel(w - p * 2, p, borderDark); + drawPixel(p, h - p * 2, borderDark); + drawPixel(w - p * 2, h - p * 2, borderDark); + } else { + // === BORDER 1: Outer border without stepped corners (smooth) === + drawHLine(p, w - p, 0, borderDark); + drawHLine(p, w - p, h - p, borderDark); + drawVLine(0, p, h - p, borderDark); + drawVLine(w - p, p, h - p, borderDark); + } + + // === BORDER 2: Inner border with 3D bevel === + // Top edge (light - highlight) + drawHLine(p * 2, w - p * 2, p, borderLight); + // Left edge (light - highlight) + drawVLine(p, p * 2, h - p * 2, borderLight); + // Bottom edge (shadow) + drawHLine(p * 2, w - p * 2, h - p * 2, borderShadow); + // Right edge (shadow) + drawVLine(w - p * 2, p * 2, h - p * 2, borderShadow); + + // Inner corner pixels for bevel + drawPixel(p, p, borderLight); // top-left + drawPixel(w - p * 2, p, borderLight); // top-right (light side) + drawPixel(p, h - p * 2, borderShadow); // bottom-left (shadow side) + drawPixel(w - p * 2, h - p * 2, borderShadow); // bottom-right - // Main body (inside the borders) + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; canvas.drawRect( - Rect.fromLTWH(p * 3, p * 3, w - p * 6, h - p * 6), + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), bgPaint, ); - - // Fill corner areas - canvas.drawRect(Rect.fromLTWH(p * 4, p * 2, w - p * 8, p), bgPaint); - canvas.drawRect(Rect.fromLTWH(p * 2, p * 4, p, h - p * 8), bgPaint); } @override bool shouldRepaint(covariant _PixelBorderPainter oldDelegate) { return oldDelegate.backgroundColor != backgroundColor || oldDelegate.borderDark != borderDark || - oldDelegate.borderMid != borderMid || oldDelegate.borderLight != borderLight || + oldDelegate.useSteppedCorners != useSteppedCorners || + oldDelegate.borderShadow != borderShadow || oldDelegate.pixelSize != pixelSize; } } + +/// Scanline overlay painter +class _ScanlinePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.black.withOpacity(0.15) + ..strokeWidth = 1; + + for (double y = 0; y < size.height; y += 3) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/src/widgets/pixel_notification_card.dart b/lib/src/widgets/pixel_notification_card.dart index 25fd0cd..dd9ef9a 100644 --- a/lib/src/widgets/pixel_notification_card.dart +++ b/lib/src/widgets/pixel_notification_card.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; /// /// Features: /// - Pixel-perfect raised border style +/// - Optional stepped pixel border (like PixelButton) /// - Optional avatar with pixel border /// - Title and action text in pixel font /// - Customizable colors and sizes @@ -31,6 +32,12 @@ class PixelNotificationCard extends StatefulWidget { final double avatarSize; final bool enablePressAnimation; + /// Use stepped pixel border style (like PixelButton) instead of raised border + final bool useSteppedBorder; + + /// Pixel size for stepped border (only used when useSteppedBorder is true) + final double pixelSize; + const PixelNotificationCard({ super.key, required this.title, @@ -52,6 +59,8 @@ class PixelNotificationCard extends StatefulWidget { this.showScanlines = false, this.avatarSize = 44, this.enablePressAnimation = true, + this.useSteppedBorder = false, + this.pixelSize = 2.0, }); @override @@ -61,6 +70,16 @@ class PixelNotificationCard extends StatefulWidget { class _PixelNotificationCardState extends State { bool _isPressed = false; + Color get _borderLight { + final hsl = HSLColor.fromColor(widget.backgroundColor); + return hsl.withLightness((hsl.lightness + 0.2).clamp(0.0, 1.0)).toColor(); + } + + Color get _borderShadow { + final hsl = HSLColor.fromColor(widget.backgroundColor); + return hsl.withLightness((hsl.lightness - 0.2).clamp(0.0, 1.0)).toColor(); + } + @override Widget build(BuildContext context) { final defaultTitleStyle = widget.titleStyle ?? @@ -81,6 +100,10 @@ class _PixelNotificationCardState extends State { height: 1.2, ); + final contentPadding = widget.useSteppedBorder + ? widget.padding.add(EdgeInsets.all(widget.pixelSize * 3)) + : widget.padding.add(EdgeInsets.all(widget.borderWidth)); + return GestureDetector( onTapDown: widget.onTap != null && widget.enablePressAnimation ? (_) => setState(() => _isPressed = true) @@ -100,18 +123,26 @@ class _PixelNotificationCardState extends State { height: widget.height, transform: _isPressed ? Matrix4.translationValues(1, 1, 0) : Matrix4.identity(), child: CustomPaint( - painter: _PixelBorderPainter( - backgroundColor: widget.backgroundColor, - borderColor: widget.borderColor, - highlightColor: widget.highlightColor, - shadowColor: widget.shadowColor, - borderWidth: widget.borderWidth, - isPressed: _isPressed, - ), + painter: widget.useSteppedBorder + ? _SteppedPixelBorderPainter( + backgroundColor: widget.backgroundColor, + borderColor: widget.borderColor, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + ) + : _PixelBorderPainter( + backgroundColor: widget.backgroundColor, + borderColor: widget.borderColor, + highlightColor: widget.highlightColor, + shadowColor: widget.shadowColor, + borderWidth: widget.borderWidth, + isPressed: _isPressed, + ), child: Stack( children: [ Padding( - padding: widget.padding.add(EdgeInsets.all(widget.borderWidth)), + padding: contentPadding, child: Row( children: [ if (widget.avatar != null) ...[ @@ -285,6 +316,91 @@ class _PixelBorderPainter extends CustomPainter { } } +/// Custom painter for stepped pixel border (like PixelButton) +class _SteppedPixelBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color borderLight; + final Color borderShadow; + final double pixelSize; + + _SteppedPixelBorderPainter({ + required this.backgroundColor, + required this.borderColor, + required this.borderLight, + required this.borderShadow, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with 3D bevel === + drawHLine(p * 2, w - p * 2, p, borderLight); + drawVLine(p, p * 2, h - p * 2, borderLight); + drawHLine(p * 2, w - p * 2, h - p * 2, borderShadow); + drawVLine(w - p * 2, p * 2, h - p * 2, borderShadow); + + // Inner corner pixels for bevel + drawPixel(p, p, borderLight); + drawPixel(w - p * 2, p, borderLight); + drawPixel(p, h - p * 2, borderShadow); + drawPixel(w - p * 2, h - p * 2, borderShadow); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + } + + @override + bool shouldRepaint(covariant _SteppedPixelBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.borderLight != borderLight || + oldDelegate.borderShadow != borderShadow || + oldDelegate.pixelSize != pixelSize; + } +} + /// Custom painter for circular avatar with pixel border class _PixelAvatarBorderPainter extends CustomPainter { final Color borderColor; diff --git a/lib/src/widgets/pixel_panel.dart b/lib/src/widgets/pixel_panel.dart index c106c38..f2984d5 100644 --- a/lib/src/widgets/pixel_panel.dart +++ b/lib/src/widgets/pixel_panel.dart @@ -1,7 +1,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; -enum PixelPanelStyle { pixelOutline, glowingBorder, paperGrain, oldScreenCRT } +enum PixelPanelStyle { pixelOutline, glowingBorder, paperGrain, oldScreenCRT, grit } class PixelPanel extends StatelessWidget { final Widget child; @@ -15,6 +15,11 @@ class PixelPanel extends StatelessWidget { final Color borderColor; final List? gradientColors; + /// Background color for the panel. When set to Colors.transparent, + /// only the overlay effects (scanlines, noise) are shown without + /// a solid background - useful for overlaying on images. + final Color? backgroundColor; + const PixelPanel({ super.key, required this.child, @@ -27,6 +32,7 @@ class PixelPanel extends StatelessWidget { this.showPixelation = true, this.borderColor = Colors.white, this.gradientColors, + this.backgroundColor, }); Widget _buildBackground(BuildContext context) { @@ -53,26 +59,68 @@ class PixelPanel extends StatelessWidget { ); case PixelPanelStyle.oldScreenCRT: + final isTransparent = backgroundColor == Colors.transparent; return Stack( children: [ - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: - gradientColors ?? [Colors.black87, Colors.black, Colors.black87], - begin: Alignment.topLeft, - end: Alignment.bottomRight, + if (!isTransparent) + Container( + decoration: BoxDecoration( + color: backgroundColor, + gradient: backgroundColor == null + ? LinearGradient( + colors: gradientColors ?? + [Colors.black87, Colors.black, Colors.black87], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ) + : null, ), ), - ), Positioned.fill( child: _ScanlineOverlay( - opacity: 0.12, + opacity: isTransparent ? 0.25 : 0.12, spacing: 3, ), ), if (showPixelation) - Positioned.fill(child: _NoiseOverlay(intensity: 0.06, pixelSize: 4)), + Positioned.fill( + child: _NoiseOverlay( + intensity: isTransparent ? 0.08 : 0.06, pixelSize: 4)), + ], + ); + + case PixelPanelStyle.grit: + return Stack( + children: [ + Container( + color: backgroundColor ?? const Color(0xFF1A1C24), + ), + // Fine JPEG-like compression artifacts + Positioned.fill( + child: _GritOverlay( + intensity: 0.03, + pixelSize: 1, + color: Colors.white, + density: 0.08, + ), + ), + Positioned.fill( + child: _GritOverlay( + intensity: 0.025, + pixelSize: 1, + color: Colors.black, + density: 0.06, + ), + ), + // Slightly larger scattered artifacts + Positioned.fill( + child: _GritOverlay( + intensity: 0.02, + pixelSize: 2, + color: const Color(0xFF252830), + density: 0.03, + ), + ), ], ); @@ -80,7 +128,7 @@ class PixelPanel extends StatelessWidget { case PixelPanelStyle.pixelOutline: default: return Container( - color: Colors.black, + color: backgroundColor ?? Colors.black, ); } } @@ -137,14 +185,23 @@ class PixelPanel extends StatelessWidget { class _NoiseOverlay extends StatelessWidget { final double intensity; final double pixelSize; + final int? seed; - const _NoiseOverlay({Key? key, required this.intensity, required this.pixelSize}) - : super(key: key); + const _NoiseOverlay({ + Key? key, + required this.intensity, + required this.pixelSize, + this.seed, + }) : super(key: key); @override Widget build(BuildContext context) { return CustomPaint( - painter: _NoisePainter(intensity: intensity, pixelSize: pixelSize), + painter: _NoisePainter( + intensity: intensity, + pixelSize: pixelSize, + seed: seed, + ), ); } } @@ -152,9 +209,14 @@ class _NoiseOverlay extends StatelessWidget { class _NoisePainter extends CustomPainter { final double intensity; final double pixelSize; - final Random _random = Random(); + final int? seed; + late final Random _random; - _NoisePainter({required this.intensity, required this.pixelSize}); + _NoisePainter({ + required this.intensity, + required this.pixelSize, + this.seed, + }) : _random = Random(seed); @override void paint(Canvas canvas, Size size) { @@ -172,23 +234,32 @@ class _NoisePainter extends CustomPainter { } @override - bool shouldRepaint(covariant _NoisePainter oldDelegate) => false; + bool shouldRepaint(covariant _NoisePainter oldDelegate) => + oldDelegate.seed != seed || + oldDelegate.intensity != intensity || + oldDelegate.pixelSize != pixelSize; } class _ScanlineOverlay extends StatelessWidget { final double opacity; final double spacing; + final double offset; const _ScanlineOverlay({ Key? key, this.opacity = 0.1, this.spacing = 4.0, + this.offset = 0.0, }) : super(key: key); @override Widget build(BuildContext context) { return CustomPaint( - painter: _ScanlinePainter(opacity: opacity, spacing: spacing), + painter: _ScanlinePainter( + opacity: opacity, + spacing: spacing, + offset: offset, + ), ); } } @@ -196,8 +267,13 @@ class _ScanlineOverlay extends StatelessWidget { class _ScanlinePainter extends CustomPainter { final double opacity; final double spacing; + final double offset; - _ScanlinePainter({required this.opacity, required this.spacing}); + _ScanlinePainter({ + required this.opacity, + required this.spacing, + this.offset = 0.0, + }); @override void paint(Canvas canvas, Size size) { @@ -205,17 +281,256 @@ class _ScanlinePainter extends CustomPainter { ..color = Colors.white.withOpacity(opacity) ..strokeWidth = 1; - for (double y = 0; y < size.height; y += spacing) { + for (double y = offset % spacing; y < size.height; y += spacing) { canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } } @override - bool shouldRepaint(covariant _ScanlinePainter oldDelegate) => false; + bool shouldRepaint(covariant _ScanlinePainter oldDelegate) => + oldDelegate.offset != offset || + oldDelegate.opacity != opacity || + oldDelegate.spacing != spacing; +} + +/// Grit overlay for creating subtle JPEG-like compression artifacts +class _GritOverlay extends StatelessWidget { + final double intensity; + final double pixelSize; + final Color color; + final double density; + + const _GritOverlay({ + Key? key, + required this.intensity, + required this.pixelSize, + required this.color, + this.density = 0.1, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: _GritPainter( + intensity: intensity, + pixelSize: pixelSize, + color: color, + density: density, + ), + ); + } +} + +class _GritPainter extends CustomPainter { + final double intensity; + final double pixelSize; + final Color color; + final double density; + + _GritPainter({ + required this.intensity, + required this.pixelSize, + required this.color, + required this.density, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color.withOpacity(intensity) + ..style = PaintingStyle.fill; + + // Create JPEG-like compression artifact pattern + // Use prime numbers for better distribution + final step = pixelSize.clamp(1.0, 4.0); + + for (double y = 0; y < size.height; y += step) { + for (double x = 0; x < size.width; x += step) { + // Hash function for pseudo-random but deterministic pattern + final hash = ((x * 17 + y * 31 + (x * y * 0.1).toInt()) % 1000) / 1000.0; + + if (hash < density) { + // Vary the opacity slightly for more natural look + final opacityVar = 0.5 + (hash * 1.0); + paint.color = color.withOpacity(intensity * opacityVar); + + canvas.drawRect( + Rect.fromLTWH(x, y, pixelSize, pixelSize), + paint, + ); + } + } + } + } + + @override + bool shouldRepaint(covariant _GritPainter oldDelegate) => false; +} + + +/// Animated CRT overlay widget that creates fuzzy TV static during transitions. +/// Use this for creating animated CRT effects on images or other content. +class AnimatedCRTOverlay extends StatefulWidget { + /// Whether the CRT effect is visible + final bool visible; + + /// Duration of the fade animation + final Duration duration; + + /// Opacity when fully visible + final double opacity; + + /// Scanline opacity + final double scanlineOpacity; + + /// Scanline spacing + final double scanlineSpacing; + + /// Noise intensity + final double noiseIntensity; + + /// Noise pixel size + final double noisePixelSize; + + /// Whether to animate the noise during transition + final bool animateNoise; + + /// Animation speed for the noise (frames per second) + final int noiseAnimationFps; + + const AnimatedCRTOverlay({ + super.key, + required this.visible, + this.duration = const Duration(milliseconds: 400), + this.opacity = 1.0, + this.scanlineOpacity = 0.25, + this.scanlineSpacing = 3.0, + this.noiseIntensity = 0.08, + this.noisePixelSize = 4.0, + this.animateNoise = true, + this.noiseAnimationFps = 15, + }); + + @override + State createState() => _AnimatedCRTOverlayState(); } +class _AnimatedCRTOverlayState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + int _noiseSeed = 0; + double _scanlineOffset = 0.0; + bool _isAnimatingNoise = false; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: widget.duration, + vsync: this, + ); + + _fadeAnimation = CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + ); + + _controller.addListener(_onAnimationTick); + + if (widget.visible) { + _controller.value = 1.0; + } + } + + void _onAnimationTick() { + if (_controller.isAnimating && widget.animateNoise) { + if (!_isAnimatingNoise) { + _isAnimatingNoise = true; + _startNoiseAnimation(); + } + } else { + _isAnimatingNoise = false; + } + } + + void _startNoiseAnimation() { + if (!_isAnimatingNoise || !mounted) return; + + final frameDuration = Duration(milliseconds: 1000 ~/ widget.noiseAnimationFps); + + Future.delayed(frameDuration, () { + if (!mounted || !_isAnimatingNoise) return; + setState(() { + _noiseSeed = DateTime.now().microsecondsSinceEpoch; + _scanlineOffset += 1.5; + }); + _startNoiseAnimation(); + }); + } + + @override + void didUpdateWidget(AnimatedCRTOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.visible != oldWidget.visible) { + if (widget.visible) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + if (widget.duration != oldWidget.duration) { + _controller.duration = widget.duration; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _fadeAnimation, + builder: (context, child) { + if (_fadeAnimation.value == 0) return const SizedBox.shrink(); + + // Increase noise intensity during transition for "fuzzy" effect + final isTransitioning = _controller.isAnimating; + final noiseMultiplier = isTransitioning ? 2.0 : 1.0; + final scanlineMultiplier = isTransitioning ? 1.5 : 1.0; + + return Opacity( + opacity: _fadeAnimation.value * widget.opacity, + child: Stack( + children: [ + // Scanlines with animated offset + Positioned.fill( + child: _ScanlineOverlay( + opacity: widget.scanlineOpacity * scanlineMultiplier, + spacing: widget.scanlineSpacing, + offset: _scanlineOffset, + ), + ), + // Noise with animated seed + Positioned.fill( + child: _NoiseOverlay( + intensity: widget.noiseIntensity * noiseMultiplier, + pixelSize: widget.noisePixelSize, + seed: _noiseSeed, + ), + ), + ], + ), + ); + }, + ); + } +} -// usage +// usage // PixelPanel( // width: 320, diff --git a/lib/src/widgets/pixel_progress_bar.dart b/lib/src/widgets/pixel_progress_bar.dart index faf7d8a..aeb2c14 100644 --- a/lib/src/widgets/pixel_progress_bar.dart +++ b/lib/src/widgets/pixel_progress_bar.dart @@ -256,6 +256,241 @@ class _ScanlinePainter extends CustomPainter { bool shouldRepaint(covariant _ScanlinePainter oldDelegate) => false; } +/// Vertical pixel progress bar with stepped pixel border style +class PixelVerticalProgressBar extends StatefulWidget { + final double progress; // 0.0 to 1.0 + final double width; + final double height; + final Color fillColor; + final Color backgroundColor; + final Color borderColor; + final double pixelSize; + final bool showGlow; + final Duration fillAnimationDuration; + final int? segments; + + const PixelVerticalProgressBar({ + Key? key, + required this.progress, + this.width = 24, + this.height = 150, + this.fillColor = Colors.cyanAccent, + this.backgroundColor = const Color(0xFF2A2D3A), + this.borderColor = Colors.cyanAccent, + this.pixelSize = 2.0, + this.showGlow = false, + this.fillAnimationDuration = const Duration(milliseconds: 500), + this.segments, + }) : super(key: key); + + @override + _PixelVerticalProgressBarState createState() => _PixelVerticalProgressBarState(); +} + +class _PixelVerticalProgressBarState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late Animation _animation; + double _oldProgress = 0.0; + + Color get _borderLight { + final hsl = HSLColor.fromColor(widget.backgroundColor); + return hsl.withLightness((hsl.lightness + 0.15).clamp(0.0, 1.0)).toColor(); + } + + Color get _borderShadow { + final hsl = HSLColor.fromColor(widget.backgroundColor); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } + + @override + void initState() { + super.initState(); + _oldProgress = widget.progress; + _animationController = AnimationController( + vsync: this, + duration: widget.fillAnimationDuration, + ); + _animation = Tween(begin: _oldProgress, end: widget.progress) + .animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)) + ..addListener(() { + setState(() {}); + }); + _animationController.forward(); + } + + @override + void didUpdateWidget(PixelVerticalProgressBar oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.progress != oldWidget.progress) { + _oldProgress = _animation.value; + _animationController.duration = widget.fillAnimationDuration; + _animation = Tween(begin: _oldProgress, end: widget.progress) + .animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut)) + ..addListener(() { + setState(() {}); + }); + _animationController.forward(from: 0); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget bar = SizedBox( + width: widget.width, + height: widget.height, + child: CustomPaint( + painter: _PixelVerticalProgressBarPainter( + progress: _animation.value, + fillColor: widget.fillColor, + backgroundColor: widget.backgroundColor, + borderColor: widget.borderColor, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + segments: widget.segments, + ), + ), + ); + + if (widget.showGlow) { + bar = Container( + decoration: BoxDecoration( + boxShadow: [ + BoxShadow( + color: widget.fillColor.withOpacity(0.4), + blurRadius: 8, + spreadRadius: 1, + ), + ], + ), + child: bar, + ); + } + + return bar; + } +} + +class _PixelVerticalProgressBarPainter extends CustomPainter { + final double progress; + final Color fillColor; + final Color backgroundColor; + final Color borderColor; + final Color borderLight; + final Color borderShadow; + final double pixelSize; + final int? segments; + + _PixelVerticalProgressBarPainter({ + required this.progress, + required this.fillColor, + required this.backgroundColor, + required this.borderColor, + required this.borderLight, + required this.borderShadow, + required this.pixelSize, + this.segments, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with 3D bevel === + drawHLine(p * 2, w - p * 2, p, borderLight); + drawVLine(p, p * 2, h - p * 2, borderLight); + drawHLine(p * 2, w - p * 2, h - p * 2, borderShadow); + drawVLine(w - p * 2, p * 2, h - p * 2, borderShadow); + + // Inner corner pixels for bevel + drawPixel(p, p, borderLight); + drawPixel(w - p * 2, p, borderLight); + drawPixel(p, h - p * 2, borderShadow); + drawPixel(w - p * 2, h - p * 2, borderShadow); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + + // === Progress fill (from bottom) === + final trackInnerHeight = h - p * 4; + final fillHeight = trackInnerHeight * progress.clamp(0.0, 1.0); + if (fillHeight > 0) { + final fillPaint = Paint()..color = fillColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, h - p * 2 - fillHeight, w - p * 4, fillHeight), + fillPaint, + ); + } + + // === Draw segment lines if segments set === + if (segments != null && segments! > 1) { + final segmentPaint = Paint()..color = borderShadow; + final segmentHeight = trackInnerHeight / segments!; + for (int i = 1; i < segments!; i++) { + final y = p * 2 + i * segmentHeight; + canvas.drawRect( + Rect.fromLTWH(p * 2, y - 1, w - p * 4, 2), + segmentPaint, + ); + } + } + } + + @override + bool shouldRepaint(covariant _PixelVerticalProgressBarPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.fillColor != fillColor || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor; + } +} + // usage diff --git a/lib/src/widgets/pixel_slider.dart b/lib/src/widgets/pixel_slider.dart index 95b2137..684d1fc 100644 --- a/lib/src/widgets/pixel_slider.dart +++ b/lib/src/widgets/pixel_slider.dart @@ -181,6 +181,454 @@ class _PixelHandle extends StatelessWidget { +/// PixelBorderSlider: slider with stepped pixel border style like PixelButton +/// Has outer dark border with stepped corners and inner 3D bevel effect +class PixelBorderSlider extends StatefulWidget { + final double value; + final ValueChanged onChanged; + final Color activeColor; + final Color inactiveColor; + final Color borderColor; + final int? divisions; + final double height; + final double pixelSize; + + const PixelBorderSlider({ + Key? key, + required this.value, + required this.onChanged, + this.activeColor = Colors.cyanAccent, + this.inactiveColor = const Color(0xFF2A2D3A), + this.borderColor = Colors.cyanAccent, + this.divisions, + this.height = 24, + this.pixelSize = 2.0, + }) : super(key: key); + + @override + _PixelBorderSliderState createState() => _PixelBorderSliderState(); +} + +class _PixelBorderSliderState extends State { + late double _localValue; + + @override + void initState() { + super.initState(); + _localValue = widget.value; + } + + @override + void didUpdateWidget(PixelBorderSlider oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != _localValue) { + _localValue = widget.value; + } + } + + Color get _borderLight { + final hsl = HSLColor.fromColor(widget.inactiveColor); + return hsl.withLightness((hsl.lightness + 0.15).clamp(0.0, 1.0)).toColor(); + } + + Color get _borderShadow { + final hsl = HSLColor.fromColor(widget.inactiveColor); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onHorizontalDragUpdate: (details) { + final box = context.findRenderObject() as RenderBox; + final localPosition = box.globalToLocal(details.globalPosition); + final p = widget.pixelSize; + final trackStart = p * 2; + final trackEnd = box.size.width - p * 2; + final trackWidth = trackEnd - trackStart; + double percent = ((localPosition.dx - trackStart) / trackWidth).clamp(0.0, 1.0); + if (widget.divisions != null) { + int div = widget.divisions!; + percent = (percent * div).round() / div; + } + setState(() { + _localValue = percent; + }); + widget.onChanged(_localValue); + }, + onTapDown: (details) { + final box = context.findRenderObject() as RenderBox; + final localPosition = box.globalToLocal(details.globalPosition); + final p = widget.pixelSize; + final trackStart = p * 2; + final trackEnd = box.size.width - p * 2; + final trackWidth = trackEnd - trackStart; + double percent = ((localPosition.dx - trackStart) / trackWidth).clamp(0.0, 1.0); + if (widget.divisions != null) { + int div = widget.divisions!; + percent = (percent * div).round() / div; + } + setState(() { + _localValue = percent; + }); + widget.onChanged(_localValue); + }, + child: SizedBox( + height: widget.height, + child: CustomPaint( + painter: _PixelBorderSliderPainter( + value: _localValue, + activeColor: widget.activeColor, + inactiveColor: widget.inactiveColor, + borderColor: widget.borderColor, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + divisions: widget.divisions, + ), + ), + ), + ); + } +} + +class _PixelBorderSliderPainter extends CustomPainter { + final double value; + final Color activeColor; + final Color inactiveColor; + final Color borderColor; + final Color borderLight; + final Color borderShadow; + final double pixelSize; + final int? divisions; + + _PixelBorderSliderPainter({ + required this.value, + required this.activeColor, + required this.inactiveColor, + required this.borderColor, + required this.borderLight, + required this.borderShadow, + required this.pixelSize, + this.divisions, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with 3D bevel === + drawHLine(p * 2, w - p * 2, p, borderLight); + drawVLine(p, p * 2, h - p * 2, borderLight); + drawHLine(p * 2, w - p * 2, h - p * 2, borderShadow); + drawVLine(w - p * 2, p * 2, h - p * 2, borderShadow); + + // Inner corner pixels for bevel + drawPixel(p, p, borderLight); + drawPixel(w - p * 2, p, borderLight); + drawPixel(p, h - p * 2, borderShadow); + drawPixel(w - p * 2, h - p * 2, borderShadow); + + // === Background fill === + final bgPaint = Paint()..color = inactiveColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + + // === Active fill (progress) === + final trackInnerWidth = w - p * 4; + final activeWidth = trackInnerWidth * value; + if (activeWidth > 0) { + final activePaint = Paint()..color = activeColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, activeWidth, h - p * 4), + activePaint, + ); + } + + // === Draw segments if divisions set === + if (divisions != null && divisions! > 1) { + final segmentPaint = Paint()..color = borderShadow; + final segmentWidth = trackInnerWidth / divisions!; + for (int i = 1; i < divisions!; i++) { + final x = p * 2 + i * segmentWidth; + canvas.drawRect( + Rect.fromLTWH(x - 1, p * 2, 2, h - p * 4), + segmentPaint, + ); + } + } + } + + @override + bool shouldRepaint(covariant _PixelBorderSliderPainter oldDelegate) { + return oldDelegate.value != value || + oldDelegate.activeColor != activeColor || + oldDelegate.inactiveColor != inactiveColor || + oldDelegate.borderColor != borderColor; + } +} + +/// PixelVerticalSlider: vertical slider with pixel border style +class PixelVerticalSlider extends StatefulWidget { + final double value; + final ValueChanged onChanged; + final Color activeColor; + final Color inactiveColor; + final Color borderColor; + final int? divisions; + final double width; + final double pixelSize; + + const PixelVerticalSlider({ + Key? key, + required this.value, + required this.onChanged, + this.activeColor = Colors.cyanAccent, + this.inactiveColor = const Color(0xFF2A2D3A), + this.borderColor = Colors.cyanAccent, + this.divisions, + this.width = 24, + this.pixelSize = 2.0, + }) : super(key: key); + + @override + _PixelVerticalSliderState createState() => _PixelVerticalSliderState(); +} + +class _PixelVerticalSliderState extends State { + late double _localValue; + + @override + void initState() { + super.initState(); + _localValue = widget.value; + } + + @override + void didUpdateWidget(PixelVerticalSlider oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.value != _localValue) { + _localValue = widget.value; + } + } + + Color get _borderLight { + final hsl = HSLColor.fromColor(widget.inactiveColor); + return hsl.withLightness((hsl.lightness + 0.15).clamp(0.0, 1.0)).toColor(); + } + + Color get _borderShadow { + final hsl = HSLColor.fromColor(widget.inactiveColor); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onVerticalDragUpdate: (details) { + final box = context.findRenderObject() as RenderBox; + final localPosition = box.globalToLocal(details.globalPosition); + final p = widget.pixelSize; + final trackStart = p * 2; + final trackEnd = box.size.height - p * 2; + final trackHeight = trackEnd - trackStart; + // Invert: top = 1.0, bottom = 0.0 + double percent = 1.0 - ((localPosition.dy - trackStart) / trackHeight).clamp(0.0, 1.0); + if (widget.divisions != null) { + int div = widget.divisions!; + percent = (percent * div).round() / div; + } + setState(() { + _localValue = percent; + }); + widget.onChanged(_localValue); + }, + onTapDown: (details) { + final box = context.findRenderObject() as RenderBox; + final localPosition = box.globalToLocal(details.globalPosition); + final p = widget.pixelSize; + final trackStart = p * 2; + final trackEnd = box.size.height - p * 2; + final trackHeight = trackEnd - trackStart; + double percent = 1.0 - ((localPosition.dy - trackStart) / trackHeight).clamp(0.0, 1.0); + if (widget.divisions != null) { + int div = widget.divisions!; + percent = (percent * div).round() / div; + } + setState(() { + _localValue = percent; + }); + widget.onChanged(_localValue); + }, + child: SizedBox( + width: widget.width, + child: CustomPaint( + painter: _PixelVerticalSliderPainter( + value: _localValue, + activeColor: widget.activeColor, + inactiveColor: widget.inactiveColor, + borderColor: widget.borderColor, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + divisions: widget.divisions, + ), + ), + ), + ); + } +} + +class _PixelVerticalSliderPainter extends CustomPainter { + final double value; + final Color activeColor; + final Color inactiveColor; + final Color borderColor; + final Color borderLight; + final Color borderShadow; + final double pixelSize; + final int? divisions; + + _PixelVerticalSliderPainter({ + required this.value, + required this.activeColor, + required this.inactiveColor, + required this.borderColor, + required this.borderLight, + required this.borderShadow, + required this.pixelSize, + this.divisions, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with 3D bevel === + drawHLine(p * 2, w - p * 2, p, borderLight); + drawVLine(p, p * 2, h - p * 2, borderLight); + drawHLine(p * 2, w - p * 2, h - p * 2, borderShadow); + drawVLine(w - p * 2, p * 2, h - p * 2, borderShadow); + + // Inner corner pixels for bevel + drawPixel(p, p, borderLight); + drawPixel(w - p * 2, p, borderLight); + drawPixel(p, h - p * 2, borderShadow); + drawPixel(w - p * 2, h - p * 2, borderShadow); + + // === Background fill === + final bgPaint = Paint()..color = inactiveColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + + // === Active fill (progress from bottom) === + final trackInnerHeight = h - p * 4; + final activeHeight = trackInnerHeight * value; + if (activeHeight > 0) { + final activePaint = Paint()..color = activeColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, h - p * 2 - activeHeight, w - p * 4, activeHeight), + activePaint, + ); + } + + // === Draw segments if divisions set === + if (divisions != null && divisions! > 1) { + final segmentPaint = Paint()..color = borderShadow; + final segmentHeight = trackInnerHeight / divisions!; + for (int i = 1; i < divisions!; i++) { + final y = p * 2 + i * segmentHeight; + canvas.drawRect( + Rect.fromLTWH(p * 2, y - 1, w - p * 4, 2), + segmentPaint, + ); + } + } + } + + @override + bool shouldRepaint(covariant _PixelVerticalSliderPainter oldDelegate) { + return oldDelegate.value != value || + oldDelegate.activeColor != activeColor || + oldDelegate.inactiveColor != inactiveColor || + oldDelegate.borderColor != borderColor; + } +} + // usage // class MyRetroUI extends StatefulWidget { diff --git a/test/pixelify_flutter_test.dart b/test/pixelify_flutter_test.dart index 9b8d0a7..926e818 100644 --- a/test/pixelify_flutter_test.dart +++ b/test/pixelify_flutter_test.dart @@ -490,6 +490,147 @@ void main() { }); }); + group('PixelBorderSlider', () { + testWidgets('renders with initial value', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelBorderSlider( + value: 0.5, + onChanged: (_) {}, + ), + ), + ), + ); + + expect(find.byType(PixelBorderSlider), findsOneWidget); + }); + + testWidgets('calls onChanged on drag', (tester) async { + double value = 0.5; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: PixelBorderSlider( + value: value, + onChanged: (v) => value = v, + ), + ), + ), + ), + ), + ); + + await tester.drag(find.byType(PixelBorderSlider), const Offset(50, 0)); + await tester.pumpAndSettle(); + + expect(value, isNot(0.5)); + }); + + testWidgets('applies custom colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: PixelBorderSlider( + value: 0.5, + onChanged: (_) {}, + activeColor: Colors.red, + borderColor: Colors.blue, + ), + ), + ), + ); + + expect(find.byType(PixelBorderSlider), findsOneWidget); + }); + }); + + group('PixelVerticalSlider', () { + testWidgets('renders with initial value', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 200, + child: PixelVerticalSlider( + value: 0.5, + onChanged: (_) {}, + ), + ), + ), + ), + ); + + expect(find.byType(PixelVerticalSlider), findsOneWidget); + }); + + testWidgets('calls onChanged on vertical drag', (tester) async { + double value = 0.5; + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox( + height: 200, + child: PixelVerticalSlider( + value: value, + onChanged: (v) => value = v, + ), + ), + ), + ), + ), + ); + + await tester.drag(find.byType(PixelVerticalSlider), const Offset(0, -50)); + await tester.pumpAndSettle(); + + expect(value, isNot(0.5)); + }); + + testWidgets('applies custom colors', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 200, + child: PixelVerticalSlider( + value: 0.5, + onChanged: (_) {}, + activeColor: Colors.green, + borderColor: Colors.purple, + ), + ), + ), + ), + ); + + expect(find.byType(PixelVerticalSlider), findsOneWidget); + }); + + testWidgets('supports divisions', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 200, + child: PixelVerticalSlider( + value: 0.5, + onChanged: (_) {}, + divisions: 5, + ), + ), + ), + ), + ); + + expect(find.byType(PixelVerticalSlider), findsOneWidget); + }); + }); + group('PixelToggle', () { testWidgets('renders with initial value', (tester) async { await tester.pumpWidget( From d65e7db3e0eb4616866582438855439a586ce22b Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 06:52:05 +1100 Subject: [PATCH 05/15] ok --- lib/src/effects/glitch_effect.dart | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/src/effects/glitch_effect.dart b/lib/src/effects/glitch_effect.dart index 3ea82c6..41a17fc 100644 --- a/lib/src/effects/glitch_effect.dart +++ b/lib/src/effects/glitch_effect.dart @@ -82,9 +82,32 @@ class _GlitchEffectState extends State with SingleTickerProviderSt @override Widget build(BuildContext context) { return LayoutBuilder(builder: (context, constraints) { + // Handle unbounded constraints by using child's intrinsic size + final hasBoundedHeight = constraints.hasBoundedHeight; + final hasBoundedWidth = constraints.hasBoundedWidth; + + // If constraints are unbounded, we need to wrap in IntrinsicWidth/Height + if (!hasBoundedHeight || !hasBoundedWidth) { + return IntrinsicHeight( + child: IntrinsicWidth( + child: _buildGlitchContent( + context, + hasBoundedWidth ? constraints.maxWidth : 200, + hasBoundedHeight ? constraints.maxHeight : 200, + ), + ), + ); + } + final width = constraints.maxWidth; final height = constraints.maxHeight; + return _buildGlitchContent(context, width, height); + }); + } + + Widget _buildGlitchContent(BuildContext context, double width, double height) { + Widget scanLines = const SizedBox.shrink(); if (widget.enableScanLines) { scanLines = IgnorePointer( @@ -142,7 +165,6 @@ class _GlitchEffectState extends State with SingleTickerProviderSt ), ], ); - }); } Positioned _displacedSlice(_Slice s, double width, double height) { From 4427327596c7ca18c2eb322329b4b5608bdfb803 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 07:08:32 +1100 Subject: [PATCH 06/15] Add PixelBottomBar widget with stepped corners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Create PixelBottomBar with pixelated stepped corner effect - Add PixelBottomBarAdvanced variant with labels support - Include customizable colors, gradients, and glow effects - Add example page with toggle controls for stepped corners and glow 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- example/lib/main.dart | 148 +++++++++ lib/pixelify_flutter.dart | 1 + lib/src/widgets/pixel_bottom_bar.dart | 414 ++++++++++++++++++++++++++ 3 files changed, 563 insertions(+) create mode 100644 lib/src/widgets/pixel_bottom_bar.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index cd28b70..ad1d09a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -49,6 +49,7 @@ class ComponentListPage extends StatelessWidget { ComponentItem('WaveAnimation', const WaveAnimationPage()), ComponentItem('GlitchEffect', const GlitchEffectPage()), ComponentItem('NoiseEffect', const NoiseEffectPage()), + ComponentItem('PixelBottomBar', const PixelBottomBarPage()), ]; return Scaffold( @@ -1319,3 +1320,150 @@ class NoiseEffectPage extends StatelessWidget { ); } } + +class PixelBottomBarPage extends StatefulWidget { + const PixelBottomBarPage({super.key}); + + @override + State createState() => _PixelBottomBarPageState(); +} + +class _PixelBottomBarPageState extends State { + int _currentIndex = 0; + bool _useSteppedCorners = true; + bool _showGlow = false; + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + return Scaffold( + appBar: AppBar( + backgroundColor: const Color(0xFF2A2D3A), + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.cyanAccent), + onPressed: () => Navigator.of(context).pop(), + ), + title: Text( + 'PixelBottomBar', + style: GoogleFonts.pressStart2p( + fontSize: 10, + color: Colors.cyanAccent, + ), + ), + ), + body: Stack( + children: [ + // Content area + Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('STEPPED CORNERS:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _useSteppedCorners, + onChanged: (v) => setState(() => _useSteppedCorners = v), + onColor: Colors.cyanAccent, + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Text('ACTIVE GLOW:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _showGlow, + onChanged: (v) => setState(() => _showGlow = v), + onColor: Colors.cyanAccent, + ), + ], + ), + const SizedBox(height: 32), + Text('SELECTED: ${_getPageName(_currentIndex)}', style: pixelFont), + const SizedBox(height: 32), + // Content based on selected tab + Expanded( + child: Center( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: _buildContent(_currentIndex), + ), + ), + ), + ], + ), + ), + // Bottom bar + Positioned( + left: 0, + right: 0, + bottom: 24, + child: PixelBottomBar( + currentIndex: _currentIndex, + icons: const [ + Icons.home_outlined, + Icons.search, + Icons.grid_view, + Icons.favorite_outline, + Icons.person_outline, + ], + onTap: (index) => setState(() => _currentIndex = index), + useSteppedCorners: _useSteppedCorners, + showActiveGlow: _showGlow, + activeIconColor: Colors.cyanAccent, + activeCircleColor: const Color(0xFF3D4155), + borderColor: const Color(0xFF4A4E65), + ), + ), + ], + ), + ); + } + + String _getPageName(int index) { + switch (index) { + case 0: + return 'HOME'; + case 1: + return 'SEARCH'; + case 2: + return 'GRID'; + case 3: + return 'FAVORITES'; + case 4: + return 'PROFILE'; + default: + return 'UNKNOWN'; + } + } + + Widget _buildContent(int index) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 14, color: Colors.cyanAccent); + return Container( + key: ValueKey(index), + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + [ + Icons.home_outlined, + Icons.search, + Icons.grid_view, + Icons.favorite_outline, + Icons.person_outline, + ][index], + size: 64, + color: Colors.cyanAccent, + ), + const SizedBox(height: 16), + Text(_getPageName(index), style: pixelFont), + ], + ), + ); + } +} diff --git a/lib/pixelify_flutter.dart b/lib/pixelify_flutter.dart index 47f2568..4ddc700 100644 --- a/lib/pixelify_flutter.dart +++ b/lib/pixelify_flutter.dart @@ -17,6 +17,7 @@ export 'src/widgets/pixel_slider.dart'; export 'src/widgets/pixel_text_field.dart'; export 'src/widgets/pixel_text.dart'; export 'src/widgets/pixel_toggle.dart'; +export 'src/widgets/pixel_bottom_bar.dart'; //Animations export 'src/animations/fade_animation.dart'; diff --git a/lib/src/widgets/pixel_bottom_bar.dart b/lib/src/widgets/pixel_bottom_bar.dart new file mode 100644 index 0000000..8a23802 --- /dev/null +++ b/lib/src/widgets/pixel_bottom_bar.dart @@ -0,0 +1,414 @@ +import 'package:flutter/material.dart'; + +/// A pixelated bottom navigation bar with stepped corners and retro styling. +class PixelBottomBar extends StatelessWidget { + /// The currently selected icon index. + final int currentIndex; + + /// List of icons to display in the bar. + final List icons; + + /// Callback when an icon is tapped. + final ValueChanged? onTap; + + /// Background gradient for the bar. + final LinearGradient? gradient; + + /// Background color (used if gradient is null). + final Color backgroundColor; + + /// Color of the circle behind the active icon. + final Color activeCircleColor; + + /// Color of the active icon. + final Color activeIconColor; + + /// Color of inactive icons. + final Color inactiveIconColor; + + /// Border color. + final Color borderColor; + + /// Pixel size for the stepped corners. + final double pixelSize; + + /// Height of the bar. + final double height; + + /// Horizontal margin around the bar. + final double horizontalMargin; + + /// Whether to show the stepped corner pixel effect. + final bool useSteppedCorners; + + /// Whether to show a glow effect on the active icon. + final bool showActiveGlow; + + /// Icon size. + final double iconSize; + + const PixelBottomBar({ + super.key, + required this.currentIndex, + required this.icons, + this.onTap, + this.gradient, + this.backgroundColor = const Color(0xFF2A2D3A), + this.activeCircleColor = const Color(0xFF3D4155), + this.activeIconColor = Colors.white, + this.inactiveIconColor = const Color(0xFF6B7280), + this.borderColor = const Color(0xFF3D4155), + this.pixelSize = 2.0, + this.height = 60, + this.horizontalMargin = 24, + this.useSteppedCorners = true, + this.showActiveGlow = false, + this.iconSize = 24, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: horizontalMargin), + height: height, + child: CustomPaint( + painter: _PixelBottomBarPainter( + backgroundColor: backgroundColor, + gradient: gradient, + borderColor: borderColor, + pixelSize: pixelSize, + useSteppedCorners: useSteppedCorners, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: pixelSize * 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: icons.asMap().entries.map((entry) { + final index = entry.key; + final icon = entry.value; + final isActive = currentIndex == index; + + return GestureDetector( + onTap: () => onTap?.call(index), + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: isActive ? activeCircleColor : Colors.transparent, + borderRadius: BorderRadius.circular(pixelSize * 4), + boxShadow: isActive && showActiveGlow + ? [ + BoxShadow( + color: activeIconColor.withValues(alpha: 0.3), + blurRadius: 8, + spreadRadius: 2, + ), + ] + : null, + ), + child: Icon( + icon, + size: iconSize, + color: isActive ? activeIconColor : inactiveIconColor, + ), + ), + ); + }).toList(), + ), + ), + ), + ); + } +} + +/// Painter for the pixel bottom bar with stepped corners. +class _PixelBottomBarPainter extends CustomPainter { + final Color backgroundColor; + final LinearGradient? gradient; + final Color borderColor; + final double pixelSize; + final bool useSteppedCorners; + + _PixelBottomBarPainter({ + required this.backgroundColor, + this.gradient, + required this.borderColor, + required this.pixelSize, + required this.useSteppedCorners, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + // Background paint + final bgPaint = Paint(); + if (gradient != null) { + bgPaint.shader = gradient!.createShader(Rect.fromLTWH(0, 0, w, h)); + } else { + bgPaint.color = backgroundColor; + } + + // Border paint + final borderPaint = Paint()..color = borderColor; + + if (useSteppedCorners) { + // Draw stepped corner background + _drawSteppedBackground(canvas, size, bgPaint, borderPaint, p); + } else { + // Draw simple rounded rectangle + final rrect = RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, w, h), + Radius.circular(h / 2), + ); + canvas.drawRRect(rrect, bgPaint); + canvas.drawRRect( + rrect, + borderPaint + ..style = PaintingStyle.stroke + ..strokeWidth = p, + ); + } + } + + void _drawSteppedBackground( + Canvas canvas, + Size size, + Paint bgPaint, + Paint borderPaint, + double p, + ) { + final double w = size.width; + final double h = size.height; + + // Number of stepped pixels on each corner + final int steps = (h / (2 * p)).floor().clamp(2, 8); + + // Create the stepped path + final path = Path(); + + // Start from top-left, after stepped corner + path.moveTo(p * steps, 0); + + // Top edge + path.lineTo(w - p * steps, 0); + + // Top-right stepped corner + for (int i = steps - 1; i >= 0; i--) { + path.lineTo(w - p * i, p * (steps - i)); + } + + // Right edge + path.lineTo(w, h - p * steps); + + // Bottom-right stepped corner + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (i + 1), h - p * (steps - i - 1)); + } + + // Bottom edge + path.lineTo(p * steps, h); + + // Bottom-left stepped corner + for (int i = steps - 1; i >= 0; i--) { + path.lineTo(p * i, h - p * (steps - i)); + } + + // Left edge + path.lineTo(0, p * steps); + + // Top-left stepped corner + for (int i = 0; i < steps; i++) { + path.lineTo(p * (i + 1), p * (steps - i - 1)); + } + + path.close(); + + // Draw background + canvas.drawPath(path, bgPaint); + + // Draw border + canvas.drawPath( + path, + borderPaint + ..style = PaintingStyle.stroke + ..strokeWidth = p, + ); + + // Draw highlight on top edge + final highlightPaint = Paint() + ..color = Colors.white.withValues(alpha: 0.1) + ..strokeWidth = p; + + canvas.drawLine( + Offset(p * steps, p), + Offset(w - p * steps, p), + highlightPaint, + ); + } + + @override + bool shouldRepaint(covariant _PixelBottomBarPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.gradient != gradient || + oldDelegate.borderColor != borderColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.useSteppedCorners != useSteppedCorners; + } +} + +/// A pixelated bottom navigation bar item model. +class PixelBottomBarItem { + /// The icon to display. + final IconData icon; + + /// Optional label for the item. + final String? label; + + /// Optional custom widget for the icon. + final Widget? customIcon; + + const PixelBottomBarItem({ + required this.icon, + this.label, + this.customIcon, + }); +} + +/// A more customizable version of PixelBottomBar with item models. +class PixelBottomBarAdvanced extends StatelessWidget { + /// The currently selected item index. + final int currentIndex; + + /// List of items to display in the bar. + final List items; + + /// Callback when an item is tapped. + final ValueChanged? onTap; + + /// Background gradient for the bar. + final LinearGradient? gradient; + + /// Background color (used if gradient is null). + final Color backgroundColor; + + /// Color of the circle behind the active item. + final Color activeCircleColor; + + /// Color of the active icon. + final Color activeIconColor; + + /// Color of inactive icons. + final Color inactiveIconColor; + + /// Border color. + final Color borderColor; + + /// Pixel size for the stepped corners. + final double pixelSize; + + /// Height of the bar. + final double height; + + /// Horizontal margin around the bar. + final double horizontalMargin; + + /// Whether to show the stepped corner pixel effect. + final bool useSteppedCorners; + + /// Whether to show labels. + final bool showLabels; + + /// Label style. + final TextStyle? labelStyle; + + /// Icon size. + final double iconSize; + + const PixelBottomBarAdvanced({ + super.key, + required this.currentIndex, + required this.items, + this.onTap, + this.gradient, + this.backgroundColor = const Color(0xFF2A2D3A), + this.activeCircleColor = const Color(0xFF3D4155), + this.activeIconColor = Colors.white, + this.inactiveIconColor = const Color(0xFF6B7280), + this.borderColor = const Color(0xFF3D4155), + this.pixelSize = 2.0, + this.height = 60, + this.horizontalMargin = 24, + this.useSteppedCorners = true, + this.showLabels = false, + this.labelStyle, + this.iconSize = 24, + }); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: horizontalMargin), + height: showLabels ? height + 16 : height, + child: CustomPaint( + painter: _PixelBottomBarPainter( + backgroundColor: backgroundColor, + gradient: gradient, + borderColor: borderColor, + pixelSize: pixelSize, + useSteppedCorners: useSteppedCorners, + ), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: pixelSize * 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: items.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final isActive = currentIndex == index; + + return GestureDetector( + onTap: () => onTap?.call(index), + behavior: HitTestBehavior.opaque, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isActive ? activeCircleColor : Colors.transparent, + borderRadius: BorderRadius.circular(pixelSize * 4), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + item.customIcon ?? + Icon( + item.icon, + size: iconSize, + color: isActive ? activeIconColor : inactiveIconColor, + ), + if (showLabels && item.label != null) ...[ + const SizedBox(height: 4), + Text( + item.label!, + style: labelStyle ?? + TextStyle( + fontSize: 10, + color: isActive ? activeIconColor : inactiveIconColor, + ), + ), + ], + ], + ), + ), + ); + }).toList(), + ), + ), + ), + ); + } +} From 217d65ac2f20eae9d920407b072e226cc3fd10e6 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 07:16:51 +1100 Subject: [PATCH 07/15] Enhance PixelBottomBar with badges, blur, and better pixel corners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add notification badge support with pixelated badge shape - Add translucent blur effect option (useBlur, blurSigma) - Improve stepped corner algorithm for more pixel-art style - Add pixelated highlight painter for active icon - Add ClipPath for proper blur clipping - Update example with badge demo and blur toggle 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- example/lib/main.dart | 35 +- lib/src/widgets/pixel_bottom_bar.dart | 615 +++++++++++++++++++++----- 2 files changed, 530 insertions(+), 120 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ad1d09a..16e90d5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1332,6 +1332,7 @@ class _PixelBottomBarPageState extends State { int _currentIndex = 0; bool _useSteppedCorners = true; bool _showGlow = false; + bool _useBlur = false; @override Widget build(BuildContext context) { @@ -1353,6 +1354,20 @@ class _PixelBottomBarPageState extends State { ), body: Stack( children: [ + // Background gradient for blur effect demo + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF1A1D2E), + Color(0xFF2D3250), + Color(0xFF424769), + ], + ), + ), + ), // Content area Padding( padding: const EdgeInsets.all(16), @@ -1382,6 +1397,18 @@ class _PixelBottomBarPageState extends State { ), ], ), + const SizedBox(height: 16), + Row( + children: [ + Text('BLUR EFFECT:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _useBlur, + onChanged: (v) => setState(() => _useBlur = v), + onColor: Colors.cyanAccent, + ), + ], + ), const SizedBox(height: 32), Text('SELECTED: ${_getPageName(_currentIndex)}', style: pixelFont), const SizedBox(height: 32), @@ -1407,15 +1434,17 @@ class _PixelBottomBarPageState extends State { icons: const [ Icons.home_outlined, Icons.search, - Icons.grid_view, + Icons.notifications_outlined, Icons.favorite_outline, Icons.person_outline, ], + badges: const [null, null, 3, 12, null], onTap: (index) => setState(() => _currentIndex = index), useSteppedCorners: _useSteppedCorners, showActiveGlow: _showGlow, + useBlur: _useBlur, activeIconColor: Colors.cyanAccent, - activeCircleColor: const Color(0xFF3D4155), + activeHighlightColor: const Color(0xFF3D4155), borderColor: const Color(0xFF4A4E65), ), ), @@ -1431,7 +1460,7 @@ class _PixelBottomBarPageState extends State { case 1: return 'SEARCH'; case 2: - return 'GRID'; + return 'ALERTS'; case 3: return 'FAVORITES'; case 4: diff --git a/lib/src/widgets/pixel_bottom_bar.dart b/lib/src/widgets/pixel_bottom_bar.dart index 8a23802..e2fac75 100644 --- a/lib/src/widgets/pixel_bottom_bar.dart +++ b/lib/src/widgets/pixel_bottom_bar.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; /// A pixelated bottom navigation bar with stepped corners and retro styling. @@ -17,8 +19,8 @@ class PixelBottomBar extends StatelessWidget { /// Background color (used if gradient is null). final Color backgroundColor; - /// Color of the circle behind the active icon. - final Color activeCircleColor; + /// Color of the highlight behind the active icon. + final Color activeHighlightColor; /// Color of the active icon. final Color activeIconColor; @@ -47,6 +49,27 @@ class PixelBottomBar extends StatelessWidget { /// Icon size. final double iconSize; + /// Optional badge counts for each icon (use null or 0 for no badge). + final List? badges; + + /// Badge background color. + final Color badgeColor; + + /// Badge text color. + final Color badgeTextColor; + + /// Whether to show indicator line under active icon. + final bool showActiveIndicator; + + /// Active indicator color. + final Color activeIndicatorColor; + + /// Whether to use translucent blur effect. + final bool useBlur; + + /// Blur intensity (sigma) when useBlur is true. + final double blurSigma; + const PixelBottomBar({ super.key, required this.currentIndex, @@ -54,67 +77,222 @@ class PixelBottomBar extends StatelessWidget { this.onTap, this.gradient, this.backgroundColor = const Color(0xFF2A2D3A), - this.activeCircleColor = const Color(0xFF3D4155), + this.activeHighlightColor = const Color(0xFF3D4155), this.activeIconColor = Colors.white, this.inactiveIconColor = const Color(0xFF6B7280), this.borderColor = const Color(0xFF3D4155), - this.pixelSize = 2.0, - this.height = 60, + this.pixelSize = 3.0, + this.height = 64, this.horizontalMargin = 24, this.useSteppedCorners = true, this.showActiveGlow = false, - this.iconSize = 24, + this.iconSize = 26, + this.badges, + this.badgeColor = const Color(0xFFE53935), + this.badgeTextColor = Colors.white, + this.showActiveIndicator = false, + this.activeIndicatorColor = const Color(0xFFE53935), + this.useBlur = false, + this.blurSigma = 10.0, }); @override Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.symmetric(horizontal: horizontalMargin), - height: height, - child: CustomPaint( - painter: _PixelBottomBarPainter( - backgroundColor: backgroundColor, - gradient: gradient, - borderColor: borderColor, - pixelSize: pixelSize, - useSteppedCorners: useSteppedCorners, - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: pixelSize * 4), - child: Row( + final content = Padding( + padding: EdgeInsets.symmetric(horizontal: pixelSize * 6), + child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: icons.asMap().entries.map((entry) { final index = entry.key; final icon = entry.value; final isActive = currentIndex == index; + final badgeCount = badges != null && index < badges!.length + ? badges![index] + : null; return GestureDetector( onTap: () => onTap?.call(index), behavior: HitTestBehavior.opaque, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: isActive ? activeCircleColor : Colors.transparent, - borderRadius: BorderRadius.circular(pixelSize * 4), - boxShadow: isActive && showActiveGlow - ? [ - BoxShadow( - color: activeIconColor.withValues(alpha: 0.3), - blurRadius: 8, - spreadRadius: 2, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + // Active highlight with pixelated corners + if (isActive) + CustomPaint( + painter: _PixelHighlightPainter( + color: activeHighlightColor, + pixelSize: pixelSize, + glowColor: showActiveGlow + ? activeIconColor.withValues(alpha: 0.3) + : null, ), - ] - : null, - ), - child: Icon( - icon, - size: iconSize, - color: isActive ? activeIconColor : inactiveIconColor, - ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: pixelSize * 5, + vertical: pixelSize * 3, + ), + child: Icon( + icon, + size: iconSize, + color: activeIconColor, + ), + ), + ) + else + Padding( + padding: EdgeInsets.symmetric( + horizontal: pixelSize * 5, + vertical: pixelSize * 3, + ), + child: Icon( + icon, + size: iconSize, + color: inactiveIconColor, + ), + ), + // Badge + if (badgeCount != null && badgeCount > 0) + Positioned( + right: 0, + top: -4, + child: _PixelBadge( + count: badgeCount, + backgroundColor: badgeColor, + textColor: badgeTextColor, + pixelSize: pixelSize, + ), + ), + ], + ), + // Active indicator line + if (showActiveIndicator && isActive) + Container( + margin: const EdgeInsets.only(top: 4), + width: iconSize, + height: pixelSize, + color: activeIndicatorColor, + ), + ], ), ); }).toList(), + ), + ); + + return Container( + margin: EdgeInsets.symmetric(horizontal: horizontalMargin), + height: height, + child: ClipPath( + clipper: useSteppedCorners + ? _PixelBarClipper(pixelSize: pixelSize) + : null, + child: useBlur + ? BackdropFilter( + filter: ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma), + child: CustomPaint( + painter: _PixelBottomBarPainter( + backgroundColor: backgroundColor.withValues(alpha: 0.7), + gradient: gradient, + borderColor: borderColor, + pixelSize: pixelSize, + useSteppedCorners: useSteppedCorners, + ), + child: content, + ), + ) + : CustomPaint( + painter: _PixelBottomBarPainter( + backgroundColor: backgroundColor, + gradient: gradient, + borderColor: borderColor, + pixelSize: pixelSize, + useSteppedCorners: useSteppedCorners, + ), + child: content, + ), + ), + ); + } +} + +/// Custom clipper for stepped corner path. +class _PixelBarClipper extends CustomClipper { + final double pixelSize; + + _PixelBarClipper({required this.pixelSize}); + + @override + Path getClip(Size size) { + final p = pixelSize; + final w = size.width; + final h = size.height; + const int steps = 4; + + final path = Path(); + path.moveTo(p * steps, 0); + path.lineTo(w - p * steps, 0); + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (steps - i - 1), p * (i + 1)); + } + path.lineTo(w, h - p * steps); + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (i + 1), h - p * (steps - i - 1)); + } + path.lineTo(p * steps, h); + for (int i = 0; i < steps; i++) { + path.lineTo(p * (steps - i - 1), h - p * (i + 1)); + } + path.lineTo(0, p * steps); + for (int i = 0; i < steps; i++) { + path.lineTo(p * (i + 1), p * (steps - i - 1)); + } + path.close(); + return path; + } + + @override + bool shouldReclip(_PixelBarClipper oldClipper) => + oldClipper.pixelSize != pixelSize; +} + +/// Pixelated badge widget for notifications. +class _PixelBadge extends StatelessWidget { + final int count; + final Color backgroundColor; + final Color textColor; + final double pixelSize; + + const _PixelBadge({ + required this.count, + required this.backgroundColor, + required this.textColor, + required this.pixelSize, + }); + + @override + Widget build(BuildContext context) { + final displayText = count > 99 ? '99+' : count.toString(); + + return CustomPaint( + painter: _PixelBadgePainter( + color: backgroundColor, + pixelSize: pixelSize, + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: pixelSize * 2.5, + vertical: pixelSize, + ), + child: Text( + displayText, + style: TextStyle( + color: textColor, + fontSize: 10, + fontWeight: FontWeight.bold, + height: 1, ), ), ), @@ -122,6 +300,132 @@ class PixelBottomBar extends StatelessWidget { } } +/// Painter for pixelated badge. +class _PixelBadgePainter extends CustomPainter { + final Color color; + final double pixelSize; + + _PixelBadgePainter({ + required this.color, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final p = pixelSize; + + // Draw pixelated circle/pill shape + final path = Path(); + + // Simple pixelated rectangle with cut corners + path.moveTo(p, 0); + path.lineTo(size.width - p, 0); + path.lineTo(size.width, p); + path.lineTo(size.width, size.height - p); + path.lineTo(size.width - p, size.height); + path.lineTo(p, size.height); + path.lineTo(0, size.height - p); + path.lineTo(0, p); + path.close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant _PixelBadgePainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.pixelSize != pixelSize; + } +} + +/// Painter for the pixelated highlight behind active icon. +class _PixelHighlightPainter extends CustomPainter { + final Color color; + final double pixelSize; + final Color? glowColor; + + _PixelHighlightPainter({ + required this.color, + required this.pixelSize, + this.glowColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final p = pixelSize; + final w = size.width; + final h = size.height; + + // Draw glow if enabled + if (glowColor != null) { + final glowPaint = Paint() + ..color = glowColor! + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 8); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(-4, -4, w + 8, h + 8), + Radius.circular(p * 3), + ), + glowPaint, + ); + } + + final paint = Paint()..color = color; + + // Create stepped corner path for highlight + final path = Path(); + + // Start top-left after corner steps + path.moveTo(p * 2, 0); + + // Top edge + path.lineTo(w - p * 2, 0); + + // Top-right corner (2 steps) + path.lineTo(w - p, p); + path.lineTo(w, p * 2); + + // Right edge + path.lineTo(w, h - p * 2); + + // Bottom-right corner (2 steps) + path.lineTo(w - p, h - p); + path.lineTo(w - p * 2, h); + + // Bottom edge + path.lineTo(p * 2, h); + + // Bottom-left corner (2 steps) + path.lineTo(p, h - p); + path.lineTo(0, h - p * 2); + + // Left edge + path.lineTo(0, p * 2); + + // Top-left corner (2 steps) + path.lineTo(p, p); + path.lineTo(p * 2, 0); + + path.close(); + + canvas.drawPath(path, paint); + + // Draw subtle border + final borderPaint = Paint() + ..color = Colors.white.withValues(alpha: 0.1) + ..style = PaintingStyle.stroke + ..strokeWidth = 1; + canvas.drawPath(path, borderPaint); + } + + @override + bool shouldRepaint(covariant _PixelHighlightPainter oldDelegate) { + return oldDelegate.color != color || + oldDelegate.pixelSize != pixelSize || + oldDelegate.glowColor != glowColor; + } +} + /// Painter for the pixel bottom bar with stepped corners. class _PixelBottomBarPainter extends CustomPainter { final Color backgroundColor; @@ -153,7 +457,10 @@ class _PixelBottomBarPainter extends CustomPainter { } // Border paint - final borderPaint = Paint()..color = borderColor; + final borderPaint = Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = p; if (useSteppedCorners) { // Draw stepped corner background @@ -165,12 +472,7 @@ class _PixelBottomBarPainter extends CustomPainter { Radius.circular(h / 2), ); canvas.drawRRect(rrect, bgPaint); - canvas.drawRRect( - rrect, - borderPaint - ..style = PaintingStyle.stroke - ..strokeWidth = p, - ); + canvas.drawRRect(rrect, borderPaint); } } @@ -184,10 +486,10 @@ class _PixelBottomBarPainter extends CustomPainter { final double w = size.width; final double h = size.height; - // Number of stepped pixels on each corner - final int steps = (h / (2 * p)).floor().clamp(2, 8); + // Fixed number of steps for consistent pixel look + const int steps = 4; - // Create the stepped path + // Create the stepped path - true pixel art style final path = Path(); // Start from top-left, after stepped corner @@ -196,15 +498,15 @@ class _PixelBottomBarPainter extends CustomPainter { // Top edge path.lineTo(w - p * steps, 0); - // Top-right stepped corner - for (int i = steps - 1; i >= 0; i--) { - path.lineTo(w - p * i, p * (steps - i)); + // Top-right corner - diagonal pixel steps + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (steps - i - 1), p * (i + 1)); } // Right edge path.lineTo(w, h - p * steps); - // Bottom-right stepped corner + // Bottom-right corner - diagonal pixel steps for (int i = 0; i < steps; i++) { path.lineTo(w - p * (i + 1), h - p * (steps - i - 1)); } @@ -212,15 +514,15 @@ class _PixelBottomBarPainter extends CustomPainter { // Bottom edge path.lineTo(p * steps, h); - // Bottom-left stepped corner - for (int i = steps - 1; i >= 0; i--) { - path.lineTo(p * i, h - p * (steps - i)); + // Bottom-left corner - diagonal pixel steps + for (int i = 0; i < steps; i++) { + path.lineTo(p * (steps - i - 1), h - p * (i + 1)); } // Left edge path.lineTo(0, p * steps); - // Top-left stepped corner + // Top-left corner - diagonal pixel steps for (int i = 0; i < steps; i++) { path.lineTo(p * (i + 1), p * (steps - i - 1)); } @@ -231,16 +533,11 @@ class _PixelBottomBarPainter extends CustomPainter { canvas.drawPath(path, bgPaint); // Draw border - canvas.drawPath( - path, - borderPaint - ..style = PaintingStyle.stroke - ..strokeWidth = p, - ); + canvas.drawPath(path, borderPaint); // Draw highlight on top edge final highlightPaint = Paint() - ..color = Colors.white.withValues(alpha: 0.1) + ..color = Colors.white.withValues(alpha: 0.08) ..strokeWidth = p; canvas.drawLine( @@ -260,7 +557,7 @@ class _PixelBottomBarPainter extends CustomPainter { } } -/// A pixelated bottom navigation bar item model. +/// A pixelated bottom navigation bar item model with badge support. class PixelBottomBarItem { /// The icon to display. final IconData icon; @@ -271,10 +568,14 @@ class PixelBottomBarItem { /// Optional custom widget for the icon. final Widget? customIcon; + /// Optional badge count (0 or null = no badge). + final int? badgeCount; + const PixelBottomBarItem({ required this.icon, this.label, this.customIcon, + this.badgeCount, }); } @@ -295,8 +596,8 @@ class PixelBottomBarAdvanced extends StatelessWidget { /// Background color (used if gradient is null). final Color backgroundColor; - /// Color of the circle behind the active item. - final Color activeCircleColor; + /// Color of the highlight behind the active item. + final Color activeHighlightColor; /// Color of the active icon. final Color activeIconColor; @@ -328,6 +629,21 @@ class PixelBottomBarAdvanced extends StatelessWidget { /// Icon size. final double iconSize; + /// Badge background color. + final Color badgeColor; + + /// Badge text color. + final Color badgeTextColor; + + /// Whether to show a glow effect on the active icon. + final bool showActiveGlow; + + /// Whether to use translucent blur effect. + final bool useBlur; + + /// Blur intensity (sigma) when useBlur is true. + final double blurSigma; + const PixelBottomBarAdvanced({ super.key, required this.currentIndex, @@ -335,37 +651,31 @@ class PixelBottomBarAdvanced extends StatelessWidget { this.onTap, this.gradient, this.backgroundColor = const Color(0xFF2A2D3A), - this.activeCircleColor = const Color(0xFF3D4155), + this.activeHighlightColor = const Color(0xFF3D4155), this.activeIconColor = Colors.white, this.inactiveIconColor = const Color(0xFF6B7280), this.borderColor = const Color(0xFF3D4155), - this.pixelSize = 2.0, - this.height = 60, + this.pixelSize = 3.0, + this.height = 64, this.horizontalMargin = 24, this.useSteppedCorners = true, this.showLabels = false, this.labelStyle, - this.iconSize = 24, + this.iconSize = 26, + this.badgeColor = const Color(0xFFE53935), + this.badgeTextColor = Colors.white, + this.showActiveGlow = false, + this.useBlur = false, + this.blurSigma = 10.0, }); @override Widget build(BuildContext context) { - return Container( - margin: EdgeInsets.symmetric(horizontal: horizontalMargin), - height: showLabels ? height + 16 : height, - child: CustomPaint( - painter: _PixelBottomBarPainter( - backgroundColor: backgroundColor, - gradient: gradient, - borderColor: borderColor, - pixelSize: pixelSize, - useSteppedCorners: useSteppedCorners, - ), - child: Padding( - padding: EdgeInsets.symmetric(horizontal: pixelSize * 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: items.asMap().entries.map((entry) { + final content = Padding( + padding: EdgeInsets.symmetric(horizontal: pixelSize * 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: items.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; final isActive = currentIndex == index; @@ -373,41 +683,112 @@ class PixelBottomBarAdvanced extends StatelessWidget { return GestureDetector( onTap: () => onTap?.call(index), behavior: HitTestBehavior.opaque, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - decoration: BoxDecoration( - color: isActive ? activeCircleColor : Colors.transparent, - borderRadius: BorderRadius.circular(pixelSize * 4), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - item.customIcon ?? - Icon( - item.icon, - size: iconSize, - color: isActive ? activeIconColor : inactiveIconColor, - ), - if (showLabels && item.label != null) ...[ - const SizedBox(height: 4), - Text( - item.label!, - style: labelStyle ?? - TextStyle( - fontSize: 10, - color: isActive ? activeIconColor : inactiveIconColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + // Active highlight with pixelated corners + if (isActive) + CustomPaint( + painter: _PixelHighlightPainter( + color: activeHighlightColor, + pixelSize: pixelSize, + glowColor: showActiveGlow + ? activeIconColor.withValues(alpha: 0.3) + : null, + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: pixelSize * 5, + vertical: pixelSize * 3, ), - ), + child: item.customIcon ?? + Icon( + item.icon, + size: iconSize, + color: activeIconColor, + ), + ), + ) + else + Padding( + padding: EdgeInsets.symmetric( + horizontal: pixelSize * 5, + vertical: pixelSize * 3, + ), + child: item.customIcon ?? + Icon( + item.icon, + size: iconSize, + color: inactiveIconColor, + ), + ), + // Badge + if (item.badgeCount != null && item.badgeCount! > 0) + Positioned( + right: 0, + top: -4, + child: _PixelBadge( + count: item.badgeCount!, + backgroundColor: badgeColor, + textColor: badgeTextColor, + pixelSize: pixelSize, + ), + ), ], + ), + if (showLabels && item.label != null) ...[ + const SizedBox(height: 4), + Text( + item.label!, + style: labelStyle ?? + TextStyle( + fontSize: 10, + color: isActive ? activeIconColor : inactiveIconColor, + ), + ), ], - ), + ], ), ); }).toList(), - ), - ), + ), + ); + + return Container( + margin: EdgeInsets.symmetric(horizontal: horizontalMargin), + height: showLabels ? height + 20 : height, + child: ClipPath( + clipper: useSteppedCorners + ? _PixelBarClipper(pixelSize: pixelSize) + : null, + child: useBlur + ? BackdropFilter( + filter: ImageFilter.blur(sigmaX: blurSigma, sigmaY: blurSigma), + child: CustomPaint( + painter: _PixelBottomBarPainter( + backgroundColor: backgroundColor.withValues(alpha: 0.7), + gradient: gradient, + borderColor: borderColor, + pixelSize: pixelSize, + useSteppedCorners: useSteppedCorners, + ), + child: content, + ), + ) + : CustomPaint( + painter: _PixelBottomBarPainter( + backgroundColor: backgroundColor, + gradient: gradient, + borderColor: borderColor, + pixelSize: pixelSize, + useSteppedCorners: useSteppedCorners, + ), + child: content, + ), ), ); } From 38b4652a7474b1c68f0e8e51cd0ea64d7f4718a6 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 07:23:38 +1100 Subject: [PATCH 08/15] Add indicator style option to PixelBottomBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PixelBottomBarIndicatorStyle enum (highlight, underline) - Add indicatorStyle and activeIndicatorColor properties - Update both PixelBottomBar and PixelBottomBarAdvanced - Fix pixel art staircase corners (true horizontal/vertical steps) - Add RED UNDERLINE toggle in example app 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- example/lib/main.dart | 19 +++- lib/src/widgets/pixel_bottom_bar.dart | 152 +++++++++++++++++++------- 2 files changed, 129 insertions(+), 42 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 16e90d5..75bbd0c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1333,6 +1333,7 @@ class _PixelBottomBarPageState extends State { bool _useSteppedCorners = true; bool _showGlow = false; bool _useBlur = false; + bool _useUnderline = false; @override Widget build(BuildContext context) { @@ -1409,6 +1410,18 @@ class _PixelBottomBarPageState extends State { ), ], ), + const SizedBox(height: 16), + Row( + children: [ + Text('RED UNDERLINE:', style: pixelFont), + const SizedBox(width: 16), + PixelToggle( + value: _useUnderline, + onChanged: (v) => setState(() => _useUnderline = v), + onColor: Colors.redAccent, + ), + ], + ), const SizedBox(height: 32), Text('SELECTED: ${_getPageName(_currentIndex)}', style: pixelFont), const SizedBox(height: 32), @@ -1443,9 +1456,13 @@ class _PixelBottomBarPageState extends State { useSteppedCorners: _useSteppedCorners, showActiveGlow: _showGlow, useBlur: _useBlur, - activeIconColor: Colors.cyanAccent, + activeIconColor: _useUnderline ? Colors.redAccent : Colors.cyanAccent, activeHighlightColor: const Color(0xFF3D4155), borderColor: const Color(0xFF4A4E65), + indicatorStyle: _useUnderline + ? PixelBottomBarIndicatorStyle.underline + : PixelBottomBarIndicatorStyle.highlight, + activeIndicatorColor: Colors.redAccent, ), ), ], diff --git a/lib/src/widgets/pixel_bottom_bar.dart b/lib/src/widgets/pixel_bottom_bar.dart index e2fac75..a568c60 100644 --- a/lib/src/widgets/pixel_bottom_bar.dart +++ b/lib/src/widgets/pixel_bottom_bar.dart @@ -2,6 +2,14 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +/// Indicator style for the active icon in PixelBottomBar. +enum PixelBottomBarIndicatorStyle { + /// Shows a highlighted background behind the active icon. + highlight, + /// Shows a line under the active icon instead of highlight. + underline, +} + /// A pixelated bottom navigation bar with stepped corners and retro styling. class PixelBottomBar extends StatelessWidget { /// The currently selected icon index. @@ -58,12 +66,16 @@ class PixelBottomBar extends StatelessWidget { /// Badge text color. final Color badgeTextColor; - /// Whether to show indicator line under active icon. + /// Whether to show indicator line under active icon (in addition to highlight). final bool showActiveIndicator; /// Active indicator color. final Color activeIndicatorColor; + /// Style of active indicator: 'highlight' shows background behind icon, + /// 'underline' shows line under icon instead of highlight. + final PixelBottomBarIndicatorStyle indicatorStyle; + /// Whether to use translucent blur effect. final bool useBlur; @@ -92,6 +104,7 @@ class PixelBottomBar extends StatelessWidget { this.badgeTextColor = Colors.white, this.showActiveIndicator = false, this.activeIndicatorColor = const Color(0xFFE53935), + this.indicatorStyle = PixelBottomBarIndicatorStyle.highlight, this.useBlur = false, this.blurSigma = 10.0, }); @@ -119,8 +132,8 @@ class PixelBottomBar extends StatelessWidget { Stack( clipBehavior: Clip.none, children: [ - // Active highlight with pixelated corners - if (isActive) + // Active highlight with pixelated corners (only for highlight style) + if (isActive && indicatorStyle == PixelBottomBarIndicatorStyle.highlight) CustomPaint( painter: _PixelHighlightPainter( color: activeHighlightColor, @@ -150,7 +163,7 @@ class PixelBottomBar extends StatelessWidget { child: Icon( icon, size: iconSize, - color: inactiveIconColor, + color: isActive ? activeIconColor : inactiveIconColor, ), ), // Badge @@ -167,11 +180,11 @@ class PixelBottomBar extends StatelessWidget { ), ], ), - // Active indicator line - if (showActiveIndicator && isActive) + // Active indicator line (for underline style or showActiveIndicator) + if ((indicatorStyle == PixelBottomBarIndicatorStyle.underline || showActiveIndicator) && isActive) Container( margin: const EdgeInsets.only(top: 4), - width: iconSize, + width: iconSize + pixelSize * 4, height: pixelSize, color: activeIndicatorColor, ), @@ -231,24 +244,41 @@ class _PixelBarClipper extends CustomClipper { final h = size.height; const int steps = 4; + // TRUE pixel art staircase - horizontal then vertical for each step final path = Path(); path.moveTo(p * steps, 0); path.lineTo(w - p * steps, 0); + + // Top-right corner for (int i = 0; i < steps; i++) { - path.lineTo(w - p * (steps - i - 1), p * (i + 1)); + path.lineTo(w - p * (steps - 1 - i), p * i); + path.lineTo(w - p * (steps - 1 - i), p * (i + 1)); } + path.lineTo(w, h - p * steps); + + // Bottom-right corner for (int i = 0; i < steps; i++) { - path.lineTo(w - p * (i + 1), h - p * (steps - i - 1)); + path.lineTo(w - p * i, h - p * (steps - 1 - i)); + path.lineTo(w - p * (i + 1), h - p * (steps - 1 - i)); } + path.lineTo(p * steps, h); + + // Bottom-left corner for (int i = 0; i < steps; i++) { - path.lineTo(p * (steps - i - 1), h - p * (i + 1)); + path.lineTo(p * (steps - 1 - i), h - p * i); + path.lineTo(p * (steps - 1 - i), h - p * (i + 1)); } + path.lineTo(0, p * steps); + + // Top-left corner for (int i = 0; i < steps; i++) { - path.lineTo(p * (i + 1), p * (steps - i - 1)); + path.lineTo(p * i, p * (steps - 1 - i)); + path.lineTo(p * (i + 1), p * (steps - 1 - i)); } + path.close(); return path; } @@ -372,39 +402,49 @@ class _PixelHighlightPainter extends CustomPainter { final paint = Paint()..color = color; - // Create stepped corner path for highlight + // Create stepped corner path for highlight - TRUE pixel art staircase style + // Each step is horizontal then vertical (or vice versa) - no diagonals! final path = Path(); + const int steps = 2; // Smaller steps for highlight // Start top-left after corner steps - path.moveTo(p * 2, 0); + path.moveTo(p * steps, 0); // Top edge - path.lineTo(w - p * 2, 0); + path.lineTo(w - p * steps, 0); - // Top-right corner (2 steps) - path.lineTo(w - p, p); - path.lineTo(w, p * 2); + // Top-right corner - staircase down + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (steps - 1 - i), p * i); + path.lineTo(w - p * (steps - 1 - i), p * (i + 1)); + } // Right edge - path.lineTo(w, h - p * 2); + path.lineTo(w, h - p * steps); - // Bottom-right corner (2 steps) - path.lineTo(w - p, h - p); - path.lineTo(w - p * 2, h); + // Bottom-right corner - staircase left + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * i, h - p * (steps - 1 - i)); + path.lineTo(w - p * (i + 1), h - p * (steps - 1 - i)); + } // Bottom edge - path.lineTo(p * 2, h); + path.lineTo(p * steps, h); - // Bottom-left corner (2 steps) - path.lineTo(p, h - p); - path.lineTo(0, h - p * 2); + // Bottom-left corner - staircase up + for (int i = 0; i < steps; i++) { + path.lineTo(p * (steps - 1 - i), h - p * i); + path.lineTo(p * (steps - 1 - i), h - p * (i + 1)); + } // Left edge - path.lineTo(0, p * 2); + path.lineTo(0, p * steps); - // Top-left corner (2 steps) - path.lineTo(p, p); - path.lineTo(p * 2, 0); + // Top-left corner - staircase right + for (int i = 0; i < steps; i++) { + path.lineTo(p * i, p * (steps - 1 - i)); + path.lineTo(p * (i + 1), p * (steps - 1 - i)); + } path.close(); @@ -489,7 +529,8 @@ class _PixelBottomBarPainter extends CustomPainter { // Fixed number of steps for consistent pixel look const int steps = 4; - // Create the stepped path - true pixel art style + // Create the stepped path - TRUE pixel art staircase style + // Each step is horizontal then vertical (or vice versa) - no diagonals! final path = Path(); // Start from top-left, after stepped corner @@ -498,33 +539,45 @@ class _PixelBottomBarPainter extends CustomPainter { // Top edge path.lineTo(w - p * steps, 0); - // Top-right corner - diagonal pixel steps + // Top-right corner - staircase down (horizontal right, then vertical down) for (int i = 0; i < steps; i++) { - path.lineTo(w - p * (steps - i - 1), p * (i + 1)); + // Horizontal step right + path.lineTo(w - p * (steps - 1 - i), p * i); + // Vertical step down + path.lineTo(w - p * (steps - 1 - i), p * (i + 1)); } // Right edge path.lineTo(w, h - p * steps); - // Bottom-right corner - diagonal pixel steps + // Bottom-right corner - staircase left (vertical down, then horizontal left) for (int i = 0; i < steps; i++) { - path.lineTo(w - p * (i + 1), h - p * (steps - i - 1)); + // Vertical step down + path.lineTo(w - p * i, h - p * (steps - 1 - i)); + // Horizontal step left + path.lineTo(w - p * (i + 1), h - p * (steps - 1 - i)); } // Bottom edge path.lineTo(p * steps, h); - // Bottom-left corner - diagonal pixel steps + // Bottom-left corner - staircase up (horizontal left, then vertical up) for (int i = 0; i < steps; i++) { - path.lineTo(p * (steps - i - 1), h - p * (i + 1)); + // Horizontal step left + path.lineTo(p * (steps - 1 - i), h - p * i); + // Vertical step up + path.lineTo(p * (steps - 1 - i), h - p * (i + 1)); } // Left edge path.lineTo(0, p * steps); - // Top-left corner - diagonal pixel steps + // Top-left corner - staircase right (vertical up, then horizontal right) for (int i = 0; i < steps; i++) { - path.lineTo(p * (i + 1), p * (steps - i - 1)); + // Vertical step up + path.lineTo(p * i, p * (steps - 1 - i)); + // Horizontal step right + path.lineTo(p * (i + 1), p * (steps - 1 - i)); } path.close(); @@ -644,6 +697,13 @@ class PixelBottomBarAdvanced extends StatelessWidget { /// Blur intensity (sigma) when useBlur is true. final double blurSigma; + /// Style of active indicator: 'highlight' shows background behind icon, + /// 'underline' shows line under icon instead of highlight. + final PixelBottomBarIndicatorStyle indicatorStyle; + + /// Active indicator color for underline style. + final Color activeIndicatorColor; + const PixelBottomBarAdvanced({ super.key, required this.currentIndex, @@ -667,6 +727,8 @@ class PixelBottomBarAdvanced extends StatelessWidget { this.showActiveGlow = false, this.useBlur = false, this.blurSigma = 10.0, + this.indicatorStyle = PixelBottomBarIndicatorStyle.highlight, + this.activeIndicatorColor = const Color(0xFFE53935), }); @override @@ -690,8 +752,8 @@ class PixelBottomBarAdvanced extends StatelessWidget { Stack( clipBehavior: Clip.none, children: [ - // Active highlight with pixelated corners - if (isActive) + // Active highlight with pixelated corners (only for highlight style) + if (isActive && indicatorStyle == PixelBottomBarIndicatorStyle.highlight) CustomPaint( painter: _PixelHighlightPainter( color: activeHighlightColor, @@ -723,7 +785,7 @@ class PixelBottomBarAdvanced extends StatelessWidget { Icon( item.icon, size: iconSize, - color: inactiveIconColor, + color: isActive ? activeIconColor : inactiveIconColor, ), ), // Badge @@ -740,6 +802,14 @@ class PixelBottomBarAdvanced extends StatelessWidget { ), ], ), + // Active indicator line (for underline style) + if (indicatorStyle == PixelBottomBarIndicatorStyle.underline && isActive) + Container( + margin: const EdgeInsets.only(top: 4), + width: iconSize + pixelSize * 4, + height: pixelSize, + color: activeIndicatorColor, + ), if (showLabels && item.label != null) ...[ const SizedBox(height: 4), Text( From be3f9f67fbf5c50f7423ae87cc6e978dc1ca1909 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 10:49:30 +1100 Subject: [PATCH 09/15] Add PixelSteppedPanel component with stepped corner toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PixelSteppedPanel: basic panel with stepped pixel-art corners - Add PixelPillPanel: pill-shaped panel with multi-step staircase corners - Add PixelInsetPanel: inset panel with 3D bevel effect - All panels support useSteppedCorners toggle for smooth/stepped corners - Add demo page with examples for Presidential Dilemma game UI 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- example/lib/main.dart | 545 ++++++++++++++++++++++- lib/pixelify_flutter.dart | 1 + lib/src/widgets/pixel_stepped_panel.dart | 544 ++++++++++++++++++++++ 3 files changed, 1078 insertions(+), 12 deletions(-) create mode 100644 lib/src/widgets/pixel_stepped_panel.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 75bbd0c..4db683c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,6 +29,198 @@ class PixelifyExampleApp extends StatelessWidget { } } +/// Pixel-perfect hamburger menu icon with rounded stepped background +class PixelHamburgerIcon extends StatelessWidget { + final Color barColor; + final Color backgroundColor; + final Color borderColor; + final Color shadowColor; + final double size; + + const PixelHamburgerIcon({ + super.key, + this.barColor = Colors.white, + this.backgroundColor = const Color(0xFF5A6178), + this.borderColor = const Color(0xFF3D4155), + this.shadowColor = const Color(0xFF2A2D3E), + this.size = 32, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + size: Size(size, size), + painter: _PixelHamburgerPainter( + barColor: barColor, + backgroundColor: backgroundColor, + borderColor: borderColor, + shadowColor: shadowColor, + ), + ); + } +} + +class _PixelHamburgerPainter extends CustomPainter { + final Color barColor; + final Color backgroundColor; + final Color borderColor; + final Color shadowColor; + + _PixelHamburgerPainter({ + required this.barColor, + required this.backgroundColor, + required this.borderColor, + required this.shadowColor, + }); + + @override + void paint(Canvas canvas, Size size) { + // 16x16 pixel grid + final p = size.width / 16; + + void drawPixel(int x, int y, Color color) { + canvas.drawRect(Rect.fromLTWH(x * p, y * p, p, p), Paint()..color = color); + } + + // Draw shadow first (offset by 1 pixel bottom-right) + // Stepped rounded shape for shadow + for (int x = 4; x <= 12; x++) { + drawPixel(x, 15, shadowColor); + } + for (int y = 4; y <= 14; y++) { + drawPixel(15, y, shadowColor); + } + drawPixel(14, 14, shadowColor); + drawPixel(13, 14, shadowColor); + drawPixel(14, 13, shadowColor); + + // Draw border (dark outline) + // Top edge + for (int x = 4; x <= 11; x++) drawPixel(x, 0, borderColor); + // Bottom edge + for (int x = 4; x <= 11; x++) drawPixel(x, 14, borderColor); + // Left edge + for (int y = 4; y <= 11; y++) drawPixel(0, y, borderColor); + // Right edge + for (int y = 4; y <= 11; y++) drawPixel(14, y, borderColor); + + // Stepped corners (border) + // Top-left + drawPixel(1, 3, borderColor); drawPixel(2, 2, borderColor); drawPixel(3, 1, borderColor); + drawPixel(1, 2, borderColor); drawPixel(2, 1, borderColor); + // Top-right + drawPixel(12, 1, borderColor); drawPixel(13, 2, borderColor); drawPixel(14, 3, borderColor); + drawPixel(13, 1, borderColor); drawPixel(14, 2, borderColor); + // Bottom-left + drawPixel(1, 11, borderColor); drawPixel(2, 12, borderColor); drawPixel(3, 13, borderColor); + drawPixel(1, 12, borderColor); drawPixel(2, 13, borderColor); + // Bottom-right + drawPixel(12, 13, borderColor); drawPixel(13, 12, borderColor); drawPixel(14, 11, borderColor); + drawPixel(13, 13, borderColor); drawPixel(14, 12, borderColor); + + // Fill background + for (int y = 1; y <= 13; y++) { + for (int x = 1; x <= 13; x++) { + // Skip corners + if (y <= 2 && x <= 2) continue; + if (y <= 2 && x >= 12) continue; + if (y >= 12 && x <= 2) continue; + if (y >= 12 && x >= 12) continue; + if (y == 1 && (x <= 3 || x >= 11)) continue; + if (y == 13 && (x <= 3 || x >= 11)) continue; + if (x == 1 && (y <= 3 || y >= 11)) continue; + if (x == 13 && (y <= 3 || y >= 11)) continue; + drawPixel(x, y, backgroundColor); + } + } + + // Draw three white bars + // Top bar (y=4) + for (int x = 4; x <= 10; x++) { + drawPixel(x, 4, barColor); + drawPixel(x, 5, barColor); + } + // Middle bar (y=7) + for (int x = 4; x <= 10; x++) { + drawPixel(x, 7, barColor); + drawPixel(x, 8, barColor); + } + // Bottom bar (y=10) + for (int x = 4; x <= 10; x++) { + drawPixel(x, 10, barColor); + drawPixel(x, 11, barColor); + } + } + + @override + bool shouldRepaint(covariant _PixelHamburgerPainter oldDelegate) { + return oldDelegate.barColor != barColor || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.shadowColor != shadowColor; + } +} + +/// Circuit board pattern background painter +class _CircuitPatternPainter extends CustomPainter { + final Color lineColor; + final Color dotColor; + final double spacing; + + _CircuitPatternPainter({ + required this.lineColor, + required this.dotColor, + this.spacing = 40, + }); + + @override + void paint(Canvas canvas, Size size) { + final linePaint = Paint() + ..color = lineColor + ..strokeWidth = 1; + + final dotPaint = Paint()..color = dotColor; + + // Draw grid lines + for (double x = 0; x < size.width; x += spacing) { + canvas.drawLine(Offset(x, 0), Offset(x, size.height), linePaint); + } + for (double y = 0; y < size.height; y += spacing) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), linePaint); + } + + // Draw junction dots at intersections + for (double x = 0; x < size.width; x += spacing) { + for (double y = 0; y < size.height; y += spacing) { + canvas.drawRect(Rect.fromCenter(center: Offset(x, y), width: 4, height: 4), dotPaint); + } + } + + // Draw some circuit traces (decorative lines) + final tracePaint = Paint() + ..color = lineColor.withValues(alpha: 0.8) + ..strokeWidth = 2; + + // Horizontal traces at random intervals + for (double y = spacing / 2; y < size.height; y += spacing * 2) { + final startX = (y.toInt() % 3) * spacing; + final endX = startX + spacing * 3; + if (endX < size.width) { + canvas.drawLine(Offset(startX, y), Offset(endX, y), tracePaint); + // Add corner turn + canvas.drawLine(Offset(endX, y), Offset(endX, y + spacing / 2), tracePaint); + } + } + } + + @override + bool shouldRepaint(covariant _CircuitPatternPainter oldDelegate) { + return oldDelegate.lineColor != lineColor || + oldDelegate.dotColor != dotColor || + oldDelegate.spacing != spacing; + } +} + /// List of all component demos with individual pages class ComponentListPage extends StatelessWidget { const ComponentListPage({super.key}); @@ -36,6 +228,8 @@ class ComponentListPage extends StatelessWidget { @override Widget build(BuildContext context) { final components = [ + ComponentItem('PixelBottomBar', const PixelBottomBarPage()), + ComponentItem('PixelSteppedPanel', const PixelSteppedPanelPage()), ComponentItem('PixelButton', const PixelButtonPage()), ComponentItem('PixelText', const PixelTextPage()), ComponentItem('PixelPanel', const PixelPanelPage()), @@ -49,16 +243,30 @@ class ComponentListPage extends StatelessWidget { ComponentItem('WaveAnimation', const WaveAnimationPage()), ComponentItem('GlitchEffect', const GlitchEffectPage()), ComponentItem('NoiseEffect', const NoiseEffectPage()), - ComponentItem('PixelBottomBar', const PixelBottomBarPage()), ]; return Scaffold( appBar: AppBar( - backgroundColor: const Color(0xFF2A2D3A), + backgroundColor: const Color(0xFF1A1D2E), + leading: Padding( + padding: const EdgeInsets.all(8), + child: GestureDetector( + onTap: () { + // TODO: Open drawer/menu + }, + child: const PixelHamburgerIcon( + barColor: Colors.white, + backgroundColor: Color(0xFF5A6178), + borderColor: Color(0xFF3D4155), + shadowColor: Color(0xFF2A2D3E), + size: 40, + ), + ), + ), title: Text( - 'PIXELIFY COMPONENTS', + 'PIXELIFY', style: GoogleFonts.pressStart2p( - fontSize: 12, + fontSize: 14, color: Colors.cyanAccent, ), ), @@ -66,14 +274,34 @@ class ComponentListPage extends StatelessWidget { ), body: Stack( children: [ - // Grit background + // Dark gradient background + Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xFF0A0D14), + Color(0xFF151A28), + Color(0xFF1A1D2E), + ], + ), + ), + ), + // Circuit pattern overlay Positioned.fill( - child: PixelPanel( - width: double.infinity, - height: double.infinity, - style: PixelPanelStyle.grit, - borderThickness: 0, - child: const SizedBox.expand(), + child: CustomPaint( + painter: _CircuitPatternPainter( + lineColor: const Color(0xFF1E2540), + dotColor: const Color(0xFF2A3555), + spacing: 50, + ), + ), + ), + // Subtle scanline overlay + Positioned.fill( + child: CustomPaint( + painter: _ScanlineOverlayPainter(), ), ), // List content @@ -98,6 +326,21 @@ class ComponentListPage extends StatelessWidget { } } +/// Scanline overlay for CRT effect +class _ScanlineOverlayPainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = Colors.black.withValues(alpha: 0.1); + + for (double y = 0; y < size.height; y += 3) { + canvas.drawRect(Rect.fromLTWH(0, y, size.width, 1), paint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} + /// Pixel-perfect stepped corner list tile class _PixelListTile extends StatefulWidget { final String title; @@ -386,6 +629,50 @@ class _PixelButtonPageState extends State { ), ], ), + const SizedBox(height: 32), + const Divider(color: Colors.white24), + const SizedBox(height: 16), + Text('PILL STYLE BUTTONS', style: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white54)), + const SizedBox(height: 16), + Wrap( + spacing: 16, + runSpacing: 16, + children: [ + PixelButton( + label: 'BEGIN GAME', + textStyle: GoogleFonts.pressStart2p(fontSize: 12, color: Colors.white), + onPressed: () {}, + style: PixelButtonStyle.pill, + borderDark: Colors.amber, + color: const Color(0xFF2A2D3E), + hoverColor: const Color(0xFF3A3D4E), + pixelSize: 3.0, + cornerSteps: 5, + ), + PixelButton( + label: 'CONTINUE', + textStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.cyanAccent), + onPressed: () {}, + style: PixelButtonStyle.pill, + borderDark: Colors.cyanAccent, + color: const Color(0xFF1A2A3E), + hoverColor: const Color(0xFF2A3A4E), + pixelSize: 2.0, + cornerSteps: 4, + ), + PixelButton( + label: 'EXIT', + textStyle: GoogleFonts.pressStart2p(fontSize: 10, color: Colors.redAccent), + onPressed: () {}, + style: PixelButtonStyle.pill, + borderDark: Colors.redAccent, + color: const Color(0xFF3A1A1A), + hoverColor: const Color(0xFF4A2A2A), + pixelSize: 2.0, + cornerSteps: 3, + ), + ], + ), ], ), ); @@ -1456,7 +1743,7 @@ class _PixelBottomBarPageState extends State { useSteppedCorners: _useSteppedCorners, showActiveGlow: _showGlow, useBlur: _useBlur, - activeIconColor: _useUnderline ? Colors.redAccent : Colors.cyanAccent, + activeIconColor: _useUnderline ? Colors.white : Colors.cyanAccent, activeHighlightColor: const Color(0xFF3D4155), borderColor: const Color(0xFF4A4E65), indicatorStyle: _useUnderline @@ -1513,3 +1800,237 @@ class _PixelBottomBarPageState extends State { ); } } + +// ============================================================================= +// PIXEL STEPPED PANEL PAGE +// ============================================================================= +class PixelSteppedPanelPage extends StatefulWidget { + const PixelSteppedPanelPage({super.key}); + + @override + State createState() => _PixelSteppedPanelPageState(); +} + +class _PixelSteppedPanelPageState extends State { + bool _useSteppedCorners = true; + + @override + Widget build(BuildContext context) { + final pixelFont = GoogleFonts.pressStart2p(fontSize: 10, color: Colors.white); + final smallFont = GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70); + + return Scaffold( + backgroundColor: const Color(0xFF1A1C2C), + appBar: AppBar( + backgroundColor: const Color(0xFF2A2D3A), + title: Text('PixelSteppedPanel', style: pixelFont), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 8), + child: Row( + children: [ + Text('Stepped', style: smallFont), + Switch( + value: _useSteppedCorners, + onChanged: (v) => setState(() => _useSteppedCorners = v), + activeColor: Colors.cyanAccent, + ), + ], + ), + ), + ], + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Section: Basic Stepped Panel + Text('BASIC PANEL', style: pixelFont.copyWith(color: Colors.cyanAccent)), + const SizedBox(height: 12), + PixelSteppedPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF2A2D3A), + borderColor: const Color(0xFF4A4E65), + child: Text( + 'PRESIDENTIAL BRIEFING', + style: pixelFont, + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 24), + + // Section: Panel with content + Text('CONTENT PANEL', style: pixelFont.copyWith(color: Colors.cyanAccent)), + const SizedBox(height: 12), + PixelSteppedPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF252836), + borderColor: const Color(0xFF5A5E75), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Mission Report', style: pixelFont.copyWith(color: Colors.amber)), + const SizedBox(height: 8), + Text( + 'The diplomatic situation requires immediate attention. ' + 'Your advisors have prepared several options for your consideration.', + style: smallFont.copyWith(height: 1.5), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Section: Pill Panel + Text('PILL PANEL', style: pixelFont.copyWith(color: Colors.cyanAccent)), + const SizedBox(height: 12), + PixelPillPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF3D4155), + borderColor: const Color(0xFF6A6E85), + child: Text('SELECT YOUR RESPONSE', style: pixelFont, textAlign: TextAlign.center), + ), + + const SizedBox(height: 16), + + // Multiple pill panels as choice buttons + PixelPillPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF2E4A3E), + borderColor: const Color(0xFF4A7A5A), + cornerSteps: 3, + child: Text('A) Approve the treaty', style: smallFont), + ), + const SizedBox(height: 8), + PixelPillPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF4A3E2E), + borderColor: const Color(0xFF7A6A4A), + cornerSteps: 3, + child: Text('B) Request more time', style: smallFont), + ), + const SizedBox(height: 8), + PixelPillPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF4A2E3E), + borderColor: const Color(0xFF7A4A5A), + cornerSteps: 3, + child: Text('C) Reject the proposal', style: smallFont), + ), + + const SizedBox(height: 24), + + // Section: Inset Panel + Text('INSET PANEL', style: pixelFont.copyWith(color: Colors.cyanAccent)), + const SizedBox(height: 12), + PixelInsetPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF1A1C24), + borderColor: const Color(0xFF3D4155), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('STATISTICS', style: pixelFont.copyWith(color: Colors.greenAccent)), + const SizedBox(height: 12), + _buildStatRow('Approval Rating', '67%', Colors.greenAccent), + _buildStatRow('Economy', '52%', Colors.amber), + _buildStatRow('Military', '78%', Colors.cyanAccent), + _buildStatRow('Diplomacy', '45%', Colors.redAccent), + ], + ), + ), + + const SizedBox(height: 24), + + // Section: Nested Panels + Text('NESTED PANELS', style: pixelFont.copyWith(color: Colors.cyanAccent)), + const SizedBox(height: 12), + PixelSteppedPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF2A2D3A), + borderColor: const Color(0xFF5A5E75), + padding: const EdgeInsets.all(12), + child: Column( + children: [ + Text('GAME BOARD', style: pixelFont.copyWith(color: Colors.amber)), + const SizedBox(height: 12), + PixelInsetPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF1A1C24), + borderColor: const Color(0xFF3D4155), + padding: const EdgeInsets.all(8), + child: Text( + 'The inner inset panel provides a recessed area for content.', + style: smallFont.copyWith(height: 1.5), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Section: Custom Colors + Text('CUSTOM COLORS', style: pixelFont.copyWith(color: Colors.cyanAccent)), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: PixelSteppedPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF1E3A5F), + borderColor: const Color(0xFF4A90D9), + pixelSize: 2, + child: Text('BLUE', style: smallFont, textAlign: TextAlign.center), + ), + ), + const SizedBox(width: 8), + Expanded( + child: PixelSteppedPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF3A1E3A), + borderColor: const Color(0xFFD94AD9), + pixelSize: 2, + child: Text('PURPLE', style: smallFont, textAlign: TextAlign.center), + ), + ), + const SizedBox(width: 8), + Expanded( + child: PixelSteppedPanel( + useSteppedCorners: _useSteppedCorners, + backgroundColor: const Color(0xFF1E3A2A), + borderColor: const Color(0xFF4AD94A), + pixelSize: 2, + child: Text('GREEN', style: smallFont, textAlign: TextAlign.center), + ), + ), + ], + ), + + const SizedBox(height: 32), + ], + ), + ), + ); + } + + Widget _buildStatRow(String label, String value, Color color) { + final smallFont = GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(label, style: smallFont), + Text(value, style: smallFont.copyWith(color: color)), + ], + ), + ); + } +} diff --git a/lib/pixelify_flutter.dart b/lib/pixelify_flutter.dart index 4ddc700..0276f51 100644 --- a/lib/pixelify_flutter.dart +++ b/lib/pixelify_flutter.dart @@ -18,6 +18,7 @@ export 'src/widgets/pixel_text_field.dart'; export 'src/widgets/pixel_text.dart'; export 'src/widgets/pixel_toggle.dart'; export 'src/widgets/pixel_bottom_bar.dart'; +export 'src/widgets/pixel_stepped_panel.dart'; //Animations export 'src/animations/fade_animation.dart'; diff --git a/lib/src/widgets/pixel_stepped_panel.dart b/lib/src/widgets/pixel_stepped_panel.dart new file mode 100644 index 0000000..2b5f78f --- /dev/null +++ b/lib/src/widgets/pixel_stepped_panel.dart @@ -0,0 +1,544 @@ +import 'package:flutter/material.dart'; + +/// A panel widget with optional pixel-art stepped corners +/// Can toggle between smooth rounded corners and stepped/pixelated corners +class PixelSteppedPanel extends StatelessWidget { + final Widget child; + final EdgeInsets padding; + final Color backgroundColor; + final Color borderColor; + final Color? shadowColor; + final double pixelSize; + final bool useSteppedCorners; + final double borderRadius; + final double borderWidth; + final bool showShadow; + final Offset shadowOffset; + + const PixelSteppedPanel({ + super.key, + required this.child, + this.padding = const EdgeInsets.all(12), + this.backgroundColor = const Color(0xFF2A2D3A), + this.borderColor = const Color(0xFF4A4E65), + this.shadowColor, + this.pixelSize = 2.0, + this.useSteppedCorners = true, + this.borderRadius = 8.0, + this.borderWidth = 2.0, + this.showShadow = true, + this.shadowOffset = const Offset(3, 3), + }); + + @override + Widget build(BuildContext context) { + if (useSteppedCorners) { + return CustomPaint( + painter: _SteppedPanelPainter( + backgroundColor: backgroundColor, + borderColor: borderColor, + shadowColor: shadowColor ?? borderColor.withValues(alpha: 0.5), + pixelSize: pixelSize, + showShadow: showShadow, + shadowOffset: shadowOffset, + ), + child: Padding( + padding: padding.add(EdgeInsets.all(pixelSize * 2)), + child: child, + ), + ); + } + + // Smooth rounded corners fallback + return Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all(color: borderColor, width: borderWidth), + boxShadow: showShadow + ? [ + BoxShadow( + color: shadowColor ?? borderColor.withValues(alpha: 0.5), + offset: shadowOffset, + blurRadius: 0, + ), + ] + : null, + ), + child: Padding( + padding: padding, + child: child, + ), + ); + } +} + +/// Painter for pixel-perfect stepped corner panels +class _SteppedPanelPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color shadowColor; + final double pixelSize; + final bool showShadow; + final Offset shadowOffset; + + _SteppedPanelPainter({ + required this.backgroundColor, + required this.borderColor, + required this.shadowColor, + required this.pixelSize, + required this.showShadow, + required this.shadowOffset, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // Draw shadow first (offset) + if (showShadow) { + final sx = shadowOffset.dx; + final sy = shadowOffset.dy; + + // Shadow - same shape but offset + // Top edge (with corner cutouts) + drawHLine(p * 2 + sx, w - p * 2 + sx, sy, shadowColor); + // Bottom edge + drawHLine(p * 2 + sx, w - p * 2 + sx, h - p + sy, shadowColor); + // Left edge + drawVLine(sx, p * 2 + sy, h - p * 2 + sy, shadowColor); + // Right edge + drawVLine(w - p + sx, p * 2 + sy, h - p * 2 + sy, shadowColor); + + // Shadow corner pixels + drawPixel(p + sx, p + sy, shadowColor); + drawPixel(w - p * 2 + sx, p + sy, shadowColor); + drawPixel(p + sx, h - p * 2 + sy, shadowColor); + drawPixel(w - p * 2 + sx, h - p * 2 + sy, shadowColor); + + // Fill shadow interior + final shadowPaint = Paint()..color = shadowColor; + canvas.drawRect( + Rect.fromLTWH(p * 2 + sx, p * 2 + sy, w - p * 4, h - p * 4), + shadowPaint, + ); + } + + // === BORDER: Outer border with stepped corners === + // Top edge (with corner cutouts) + drawHLine(p * 2, w - p * 2, 0, borderColor); + // Bottom edge + drawHLine(p * 2, w - p * 2, h - p, borderColor); + // Left edge + drawVLine(0, p * 2, h - p * 2, borderColor); + // Right edge + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p, p * 2, w - p * 2, h - p * 4), + bgPaint, + ); + canvas.drawRect( + Rect.fromLTWH(p * 2, p, w - p * 4, p), + bgPaint, + ); + canvas.drawRect( + Rect.fromLTWH(p * 2, h - p * 2, w - p * 4, p), + bgPaint, + ); + } + + @override + bool shouldRepaint(covariant _SteppedPanelPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.showShadow != showShadow || + oldDelegate.shadowOffset != shadowOffset; + } +} + +/// Pill-shaped panel with multi-stepped rounded corners +/// Similar to PixelButton's pill style but for content panels +class PixelPillPanel extends StatelessWidget { + final Widget child; + final EdgeInsets padding; + final Color backgroundColor; + final Color borderColor; + final Color? shadowColor; + final double pixelSize; + final int cornerSteps; + final bool useSteppedCorners; + final double borderRadius; + final bool showShadow; + final Offset shadowOffset; + + const PixelPillPanel({ + super.key, + required this.child, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + this.backgroundColor = const Color(0xFF2A2D3A), + this.borderColor = const Color(0xFF4A4E65), + this.shadowColor, + this.pixelSize = 2.0, + this.cornerSteps = 4, + this.useSteppedCorners = true, + this.borderRadius = 20.0, + this.showShadow = true, + this.shadowOffset = const Offset(2, 2), + }); + + @override + Widget build(BuildContext context) { + if (useSteppedCorners) { + return CustomPaint( + painter: _PillPanelPainter( + backgroundColor: backgroundColor, + borderColor: borderColor, + shadowColor: shadowColor ?? borderColor.withValues(alpha: 0.5), + pixelSize: pixelSize, + steps: cornerSteps, + showShadow: showShadow, + shadowOffset: shadowOffset, + ), + child: Padding( + padding: padding.add(EdgeInsets.symmetric( + horizontal: pixelSize * (cornerSteps + 1), + vertical: pixelSize * 2, + )), + child: child, + ), + ); + } + + // Smooth rounded corners fallback + return Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all(color: borderColor, width: pixelSize), + boxShadow: showShadow + ? [ + BoxShadow( + color: shadowColor ?? borderColor.withValues(alpha: 0.5), + offset: shadowOffset, + blurRadius: 0, + ), + ] + : null, + ), + child: Padding( + padding: padding, + child: child, + ), + ); + } +} + +/// Painter for pill-shaped panels with stepped corners +class _PillPanelPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color shadowColor; + final double pixelSize; + final int steps; + final bool showShadow; + final Offset shadowOffset; + + _PillPanelPainter({ + required this.backgroundColor, + required this.borderColor, + required this.shadowColor, + required this.pixelSize, + required this.steps, + required this.showShadow, + required this.shadowOffset, + }); + + Path _buildSteppedPath(double w, double h, double p, double offsetX, double offsetY) { + final path = Path(); + + // Start at top-left after the stepped corner area + path.moveTo(p * steps + offsetX, offsetY); + + // Top edge + path.lineTo(w - p * steps + offsetX, offsetY); + + // Top-right corner - staircase down + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (steps - 1 - i) + offsetX, p * i + offsetY); + path.lineTo(w - p * (steps - 1 - i) + offsetX, p * (i + 1) + offsetY); + } + + // Right edge + path.lineTo(w + offsetX, h - p * steps + offsetY); + + // Bottom-right corner - staircase + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * i + offsetX, h - p * (steps - 1 - i) + offsetY); + path.lineTo(w - p * (i + 1) + offsetX, h - p * (steps - 1 - i) + offsetY); + } + + // Bottom edge + path.lineTo(p * steps + offsetX, h + offsetY); + + // Bottom-left corner - staircase up + for (int i = 0; i < steps; i++) { + path.lineTo(p * (steps - 1 - i) + offsetX, h - p * i + offsetY); + path.lineTo(p * (steps - 1 - i) + offsetX, h - p * (i + 1) + offsetY); + } + + // Left edge + path.lineTo(offsetX, p * steps + offsetY); + + // Top-left corner - staircase + for (int i = 0; i < steps; i++) { + path.lineTo(p * i + offsetX, p * (steps - 1 - i) + offsetY); + path.lineTo(p * (i + 1) + offsetX, p * (steps - 1 - i) + offsetY); + } + + path.close(); + return path; + } + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + // Draw shadow first + if (showShadow) { + final shadowPath = _buildSteppedPath(w, h, p, shadowOffset.dx, shadowOffset.dy); + canvas.drawPath(shadowPath, Paint()..color = shadowColor); + } + + // Build main path + final path = _buildSteppedPath(w, h, p, 0, 0); + + // Fill background + canvas.drawPath(path, Paint()..color = backgroundColor); + + // Draw border + canvas.drawPath( + path, + Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = p, + ); + } + + @override + bool shouldRepaint(covariant _PillPanelPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.steps != steps || + oldDelegate.showShadow != showShadow || + oldDelegate.shadowOffset != shadowOffset; + } +} + +/// Inset panel with beveled 3D effect (pressed in appearance) +class PixelInsetPanel extends StatelessWidget { + final Widget child; + final EdgeInsets padding; + final Color backgroundColor; + final Color borderColor; + final Color? highlightColor; + final Color? shadowColor; + final double pixelSize; + final bool useSteppedCorners; + final double borderRadius; + + const PixelInsetPanel({ + super.key, + required this.child, + this.padding = const EdgeInsets.all(12), + this.backgroundColor = const Color(0xFF1A1C24), + this.borderColor = const Color(0xFF3D4155), + this.highlightColor, + this.shadowColor, + this.pixelSize = 2.0, + this.useSteppedCorners = true, + this.borderRadius = 8.0, + }); + + @override + Widget build(BuildContext context) { + final highlight = highlightColor ?? + HSLColor.fromColor(backgroundColor) + .withLightness((HSLColor.fromColor(backgroundColor).lightness + 0.15).clamp(0.0, 1.0)) + .toColor(); + final shadow = shadowColor ?? + HSLColor.fromColor(backgroundColor) + .withLightness((HSLColor.fromColor(backgroundColor).lightness - 0.1).clamp(0.0, 1.0)) + .toColor(); + + if (useSteppedCorners) { + return CustomPaint( + painter: _InsetPanelPainter( + backgroundColor: backgroundColor, + borderColor: borderColor, + highlightColor: highlight, + shadowColor: shadow, + pixelSize: pixelSize, + ), + child: Padding( + padding: padding.add(EdgeInsets.all(pixelSize * 2)), + child: child, + ), + ); + } + + // Smooth rounded corners fallback with inset shadow effect + return Container( + decoration: BoxDecoration( + color: backgroundColor, + borderRadius: BorderRadius.circular(borderRadius), + border: Border.all(color: borderColor, width: pixelSize), + boxShadow: [ + BoxShadow( + color: shadow, + offset: const Offset(2, 2), + blurRadius: 0, + spreadRadius: -1, + ), + BoxShadow( + color: highlight, + offset: const Offset(-1, -1), + blurRadius: 0, + spreadRadius: -1, + ), + ], + ), + child: Padding( + padding: padding, + child: child, + ), + ); + } +} + +/// Painter for inset panels with 3D bevel effect +class _InsetPanelPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color highlightColor; + final Color shadowColor; + final double pixelSize; + + _InsetPanelPainter({ + required this.backgroundColor, + required this.borderColor, + required this.highlightColor, + required this.shadowColor, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with inverted 3D bevel (inset) === + // Top edge (shadow - darker for inset) + drawHLine(p * 2, w - p * 2, p, shadowColor); + // Left edge (shadow - darker for inset) + drawVLine(p, p * 2, h - p * 2, shadowColor); + // Bottom edge (highlight for inset) + drawHLine(p * 2, w - p * 2, h - p * 2, highlightColor); + // Right edge (highlight for inset) + drawVLine(w - p * 2, p * 2, h - p * 2, highlightColor); + + // Inner corner pixels for bevel + drawPixel(p, p, shadowColor); + drawPixel(w - p * 2, p, shadowColor); + drawPixel(p, h - p * 2, highlightColor); + drawPixel(w - p * 2, h - p * 2, highlightColor); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + } + + @override + bool shouldRepaint(covariant _InsetPanelPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.highlightColor != highlightColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.pixelSize != pixelSize; + } +} From ad20dfc6903879a0e53f98102988283a143d0ea0 Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 10:51:14 +1100 Subject: [PATCH 10/15] Update docs and default PixelBottomBar to red underline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive dartdoc comments for pub.dev documentation - Change PixelBottomBar indicatorStyle default from highlight to underline - Bump version to 0.0.2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/src/widgets/pixel_bottom_bar.dart | 40 +++--- lib/src/widgets/pixel_button.dart | 152 ++++++++++++++++++++--- lib/src/widgets/pixel_stepped_panel.dart | 133 +++++++++++++++++++- pubspec.yaml | 2 +- 4 files changed, 287 insertions(+), 40 deletions(-) diff --git a/lib/src/widgets/pixel_bottom_bar.dart b/lib/src/widgets/pixel_bottom_bar.dart index a568c60..3d810a3 100644 --- a/lib/src/widgets/pixel_bottom_bar.dart +++ b/lib/src/widgets/pixel_bottom_bar.dart @@ -88,24 +88,24 @@ class PixelBottomBar extends StatelessWidget { required this.icons, this.onTap, this.gradient, - this.backgroundColor = const Color(0xFF2A2D3A), - this.activeHighlightColor = const Color(0xFF3D4155), + this.backgroundColor = const Color(0xFF1F2937), // Dark neutral gray (no blue) + this.activeHighlightColor = const Color(0xFF374151), // Neutral gray highlight this.activeIconColor = Colors.white, - this.inactiveIconColor = const Color(0xFF6B7280), - this.borderColor = const Color(0xFF3D4155), + this.inactiveIconColor = const Color(0xFF9CA3AF), // Lighter gray for better contrast + this.borderColor = const Color(0xFF374151), // Neutral gray border this.pixelSize = 3.0, - this.height = 64, - this.horizontalMargin = 24, + this.height = 56, + this.horizontalMargin = 16, this.useSteppedCorners = true, this.showActiveGlow = false, - this.iconSize = 26, + this.iconSize = 24, this.badges, this.badgeColor = const Color(0xFFE53935), this.badgeTextColor = Colors.white, this.showActiveIndicator = false, - this.activeIndicatorColor = const Color(0xFFE53935), - this.indicatorStyle = PixelBottomBarIndicatorStyle.highlight, - this.useBlur = false, + this.activeIndicatorColor = const Color(0xFFE53935), // Red underline + this.indicatorStyle = PixelBottomBarIndicatorStyle.underline, + this.useBlur = true, // Enable blur by default for translucent effect this.blurSigma = 10.0, }); @@ -710,25 +710,25 @@ class PixelBottomBarAdvanced extends StatelessWidget { required this.items, this.onTap, this.gradient, - this.backgroundColor = const Color(0xFF2A2D3A), - this.activeHighlightColor = const Color(0xFF3D4155), + this.backgroundColor = const Color(0xFF1F2937), // Dark neutral gray (no blue) + this.activeHighlightColor = const Color(0xFF374151), // Neutral gray highlight this.activeIconColor = Colors.white, - this.inactiveIconColor = const Color(0xFF6B7280), - this.borderColor = const Color(0xFF3D4155), + this.inactiveIconColor = const Color(0xFF9CA3AF), // Lighter gray for better contrast + this.borderColor = const Color(0xFF374151), // Neutral gray border this.pixelSize = 3.0, - this.height = 64, - this.horizontalMargin = 24, + this.height = 56, + this.horizontalMargin = 16, this.useSteppedCorners = true, this.showLabels = false, this.labelStyle, - this.iconSize = 26, + this.iconSize = 24, this.badgeColor = const Color(0xFFE53935), this.badgeTextColor = Colors.white, this.showActiveGlow = false, - this.useBlur = false, + this.useBlur = true, // Enable blur by default for translucent effect this.blurSigma = 10.0, - this.indicatorStyle = PixelBottomBarIndicatorStyle.highlight, - this.activeIndicatorColor = const Color(0xFFE53935), + this.indicatorStyle = PixelBottomBarIndicatorStyle.underline, + this.activeIndicatorColor = const Color(0xFFE53935), // Red underline }); @override diff --git a/lib/src/widgets/pixel_button.dart b/lib/src/widgets/pixel_button.dart index 0001a4a..a973765 100644 --- a/lib/src/widgets/pixel_button.dart +++ b/lib/src/widgets/pixel_button.dart @@ -1,5 +1,13 @@ import 'package:flutter/material.dart'; +/// Button shape style +enum PixelButtonStyle { + /// Rectangular button with stepped corners (default) + rectangular, + /// Rounded pill button with multi-stepped pixelated corners + pill, +} + /// PixelButton with authentic retro stepped-corner border /// Two borders: outer dark, inner with 3D bevel effect class PixelButton extends StatefulWidget { @@ -24,6 +32,12 @@ class PixelButton extends StatefulWidget { /// Use stepped corners with corner pixels (true) or smooth edges (false) final bool useSteppedCorners; + /// Button style: rectangular (default) or pill (rounded) + final PixelButtonStyle style; + + /// Number of corner steps for pill style (default 4) + final int cornerSteps; + const PixelButton({ Key? key, required this.label, @@ -40,6 +54,8 @@ class PixelButton extends StatefulWidget { this.glowColor, this.enableScanlineAnimation = false, this.useSteppedCorners = true, + this.style = PixelButtonStyle.rectangular, + this.cornerSteps = 4, }) : super(key: key); @override @@ -101,20 +117,42 @@ class _PixelButtonState extends State with SingleTickerProviderStat color: Colors.white, ); - Widget buttonContent = CustomPaint( - painter: _PixelBorderPainter( - backgroundColor: _backgroundColor, - borderDark: widget.borderDark, - borderLight: _borderLight, - borderShadow: _borderShadow, - pixelSize: widget.pixelSize, - useSteppedCorners: widget.useSteppedCorners, - ), - child: Padding( - padding: EdgeInsets.all(widget.pixelSize * 3 + 8), - child: Text(widget.label, style: textStyle), - ), - ); + Widget buttonContent; + + if (widget.style == PixelButtonStyle.pill) { + // Pill style with multi-stepped rounded corners + buttonContent = CustomPaint( + painter: _PixelPillPainter( + backgroundColor: _backgroundColor, + borderColor: widget.borderDark, + pixelSize: widget.pixelSize, + steps: widget.cornerSteps, + ), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: widget.pixelSize * (widget.cornerSteps + 2) + 16, + vertical: widget.pixelSize * 3 + 8, + ), + child: Text(widget.label, style: textStyle), + ), + ); + } else { + // Rectangular style (default) + buttonContent = CustomPaint( + painter: _PixelBorderPainter( + backgroundColor: _backgroundColor, + borderDark: widget.borderDark, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + useSteppedCorners: widget.useSteppedCorners, + ), + child: Padding( + padding: EdgeInsets.all(widget.pixelSize * 3 + 8), + child: Text(widget.label, style: textStyle), + ), + ); + } // Add scanlines if enabled if (widget.enableScanlineAnimation) { @@ -294,3 +332,89 @@ class _ScanlinePainter extends CustomPainter { @override bool shouldRepaint(covariant CustomPainter oldDelegate) => false; } + +/// Draws pixel-perfect pill/rounded button with multi-stepped corners +class _PixelPillPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final double pixelSize; + final int steps; + + _PixelPillPainter({ + required this.backgroundColor, + required this.borderColor, + required this.pixelSize, + this.steps = 4, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + // Build stepped pill path + final path = Path(); + + // Start at top-left after the stepped corner area + path.moveTo(p * steps, 0); + + // Top edge + path.lineTo(w - p * steps, 0); + + // Top-right corner - staircase down (horizontal then vertical for each step) + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * (steps - 1 - i), p * i); + path.lineTo(w - p * (steps - 1 - i), p * (i + 1)); + } + + // Right edge + path.lineTo(w, h - p * steps); + + // Bottom-right corner - staircase (horizontal then vertical for each step) + for (int i = 0; i < steps; i++) { + path.lineTo(w - p * i, h - p * (steps - 1 - i)); + path.lineTo(w - p * (i + 1), h - p * (steps - 1 - i)); + } + + // Bottom edge + path.lineTo(p * steps, h); + + // Bottom-left corner - staircase up (horizontal then vertical for each step) + for (int i = 0; i < steps; i++) { + path.lineTo(p * (steps - 1 - i), h - p * i); + path.lineTo(p * (steps - 1 - i), h - p * (i + 1)); + } + + // Left edge + path.lineTo(0, p * steps); + + // Top-left corner - staircase (horizontal then vertical for each step) + for (int i = 0; i < steps; i++) { + path.lineTo(p * i, p * (steps - 1 - i)); + path.lineTo(p * (i + 1), p * (steps - 1 - i)); + } + + path.close(); + + // Fill background + canvas.drawPath(path, Paint()..color = backgroundColor); + + // Draw border + canvas.drawPath( + path, + Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = p, + ); + } + + @override + bool shouldRepaint(covariant _PixelPillPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.steps != steps; + } +} diff --git a/lib/src/widgets/pixel_stepped_panel.dart b/lib/src/widgets/pixel_stepped_panel.dart index 2b5f78f..784f6ea 100644 --- a/lib/src/widgets/pixel_stepped_panel.dart +++ b/lib/src/widgets/pixel_stepped_panel.dart @@ -1,20 +1,61 @@ import 'package:flutter/material.dart'; -/// A panel widget with optional pixel-art stepped corners -/// Can toggle between smooth rounded corners and stepped/pixelated corners +/// A panel widget with optional pixel-art stepped corners for retro game UIs. +/// +/// [PixelSteppedPanel] renders a container with pixel-perfect stepped corners +/// that give a classic 8-bit/16-bit aesthetic. It can toggle between stepped +/// pixel-art corners and smooth rounded corners via [useSteppedCorners]. +/// +/// {@tool snippet} +/// Basic usage: +/// ```dart +/// PixelSteppedPanel( +/// useSteppedCorners: true, +/// backgroundColor: Color(0xFF2A2D3A), +/// borderColor: Color(0xFF4A4E65), +/// child: Text('GAME PANEL'), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// * [PixelPillPanel], for pill-shaped panels with multi-step staircase corners +/// * [PixelInsetPanel], for inset panels with 3D bevel effect class PixelSteppedPanel extends StatelessWidget { + /// The widget to display inside the panel. final Widget child; + + /// Padding around the [child] widget. final EdgeInsets padding; + + /// Background color of the panel interior. final Color backgroundColor; + + /// Color of the panel border. final Color borderColor; + + /// Color of the drop shadow. Defaults to [borderColor] with 50% opacity. final Color? shadowColor; + + /// Size of each pixel unit for the stepped corners. final double pixelSize; + + /// Whether to use stepped pixel-art corners (true) or smooth rounded corners (false). final bool useSteppedCorners; + + /// Border radius when [useSteppedCorners] is false. final double borderRadius; + + /// Border width when [useSteppedCorners] is false. final double borderWidth; + + /// Whether to show a drop shadow behind the panel. final bool showShadow; + + /// Offset of the drop shadow. final Offset shadowOffset; + /// Creates a pixel-art stepped panel. const PixelSteppedPanel({ super.key, required this.child, @@ -190,21 +231,61 @@ class _SteppedPanelPainter extends CustomPainter { } } -/// Pill-shaped panel with multi-stepped rounded corners -/// Similar to PixelButton's pill style but for content panels +/// A pill-shaped panel with multi-stepped staircase corners for retro game UIs. +/// +/// [PixelPillPanel] creates a horizontally elongated panel with stepped +/// staircase corners on all four sides, giving a more rounded pixel-art look. +/// The number of steps in each corner is controlled by [cornerSteps]. +/// +/// {@tool snippet} +/// Basic usage: +/// ```dart +/// PixelPillPanel( +/// useSteppedCorners: true, +/// cornerSteps: 4, +/// child: Text('SELECT YOUR RESPONSE'), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// * [PixelSteppedPanel], for basic panels with simple stepped corners +/// * [PixelInsetPanel], for inset panels with 3D bevel effect class PixelPillPanel extends StatelessWidget { + /// The widget to display inside the panel. final Widget child; + + /// Padding around the [child] widget. final EdgeInsets padding; + + /// Background color of the panel interior. final Color backgroundColor; + + /// Color of the panel border. final Color borderColor; + + /// Color of the drop shadow. Defaults to [borderColor] with 50% opacity. final Color? shadowColor; + + /// Size of each pixel unit for the stepped corners. final double pixelSize; + + /// Number of staircase steps in each corner. Higher values create rounder corners. final int cornerSteps; + + /// Whether to use stepped pixel-art corners (true) or smooth rounded corners (false). final bool useSteppedCorners; + + /// Border radius when [useSteppedCorners] is false. final double borderRadius; + + /// Whether to show a drop shadow behind the panel. final bool showShadow; + + /// Offset of the drop shadow. final Offset shadowOffset; + /// Creates a pill-shaped pixel-art panel. const PixelPillPanel({ super.key, required this.child, @@ -373,18 +454,60 @@ class _PillPanelPainter extends CustomPainter { } } -/// Inset panel with beveled 3D effect (pressed in appearance) +/// An inset panel with beveled 3D effect for a "pressed in" appearance. +/// +/// [PixelInsetPanel] creates a recessed panel with inner shadows that give +/// the appearance of being pressed into the surface. The top-left edges are +/// darker (shadow) and bottom-right edges are lighter (highlight), creating +/// a classic inset bevel effect common in retro game UIs. +/// +/// {@tool snippet} +/// Basic usage: +/// ```dart +/// PixelInsetPanel( +/// useSteppedCorners: true, +/// child: Column( +/// children: [ +/// Text('STATISTICS'), +/// Text('Health: 100%'), +/// ], +/// ), +/// ) +/// ``` +/// {@end-tool} +/// +/// See also: +/// * [PixelSteppedPanel], for basic panels with simple stepped corners +/// * [PixelPillPanel], for pill-shaped panels with multi-step staircase corners class PixelInsetPanel extends StatelessWidget { + /// The widget to display inside the panel. final Widget child; + + /// Padding around the [child] widget. final EdgeInsets padding; + + /// Background color of the panel interior. final Color backgroundColor; + + /// Color of the panel border. final Color borderColor; + + /// Color for the highlight edge (bottom-right). Auto-calculated if null. final Color? highlightColor; + + /// Color for the shadow edge (top-left). Auto-calculated if null. final Color? shadowColor; + + /// Size of each pixel unit for the stepped corners. final double pixelSize; + + /// Whether to use stepped pixel-art corners (true) or smooth rounded corners (false). final bool useSteppedCorners; + + /// Border radius when [useSteppedCorners] is false. final double borderRadius; + /// Creates an inset pixel-art panel with 3D bevel effect. const PixelInsetPanel({ super.key, required this.child, diff --git a/pubspec.yaml b/pubspec.yaml index 87e58c6..ab66080 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: pixelify_flutter description: "A Flutter extension for retro Pixelated looking ui widgets and animations and effects." -version: 0.0.1 +version: 0.0.2 homepage: https://jyothish-ram.me/projects/pixelify_flutter repository: https://github.com/jyothish-ram/pixelify_flutter From d1235edda9110d9139be81834c0d3fb6ee2b3ffd Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 10:53:34 +1100 Subject: [PATCH 11/15] Update README and CHANGELOG with all new components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Document PixelBottomBar, PixelSteppedPanel, PixelPillPanel, PixelInsetPanel - Document PixelHamburgerIcon - Add code examples for all new widgets - Update CHANGELOG with 0.0.2 release notes - Update installation version to 0.0.2 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.md | 44 ++++++++++++++++++++++++++++- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41cc7d8..756ea83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,45 @@ +## 0.0.2 + +### New Widgets +* **PixelBottomBar** - Navigation bar with pixel-art stepped corners + - Stepped corner styling with customizable pixel size + - Badge support for notification indicators + - Blur effect for translucent backgrounds + - Two indicator styles: `underline` (default) and `highlight` + - Customizable active indicator color (defaults to red) + +* **PixelSteppedPanel** - Panel with toggleable stepped corners + - `useSteppedCorners` toggle between pixel-art and smooth rounded corners + - Customizable shadow, border, and background colors + - Pixel-perfect stepped corner rendering via CustomPainter + +* **PixelPillPanel** - Pill-shaped panel with multi-step staircase corners + - Configurable `cornerSteps` for rounder or sharper corners + - Same toggle support as PixelSteppedPanel + +* **PixelInsetPanel** - Inset panel with 3D bevel effect + - "Pressed in" appearance with inner shadows + - Auto-calculated highlight/shadow colors + - Classic retro game UI style + +* **PixelHamburgerIcon** - Animated hamburger menu icon + - Smooth animation between open/closed states + - Pixel-perfect line rendering + +### Enhancements +* **PixelButton** - Added `pill` style option for rounded pill-shaped buttons +* **PixelBottomBar** - Default indicator style changed to `underline` with red color + +### Documentation +* Added comprehensive dartdoc comments for pub.dev documentation +* Updated README with new widget examples +* Added code examples for all new components + ## 0.0.1 -* TODO: Describe initial release. +* Initial release +* Core pixel widgets: PixelButton, PixelText, PixelPanel, PixelSlider, PixelToggle, PixelProgressBar +* Visual effects: Pixelate, Bloom, Scanline, Glitch, Noise +* Animations: Fade, Flicker, Jitter, Wave +* PixelTheme for global styling +* Audio integration support diff --git a/README.md b/README.md index 524f614..890ed9b 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,19 @@ A comprehensive Flutter package for creating retro pixel-art style UI components ## ✨ Features ### 🎨 Pixel Widgets -- **PixelButton** - Interactive buttons with hover effects, sound support, and scanline animations +- **PixelButton** - Interactive buttons with hover effects, sound support, pill style, and scanline animations - **PixelText** - Text with multiple retro effects (flicker, glitch, scanline, pixelate) - **PixelPanel** - Containers with various retro styles (CRT, paper grain, glowing borders) +- **PixelSteppedPanel** - Panels with pixel-art stepped corners (toggleable) +- **PixelPillPanel** - Pill-shaped panels with multi-step staircase corners +- **PixelInsetPanel** - Inset panels with 3D bevel effect +- **PixelBottomBar** - Navigation bar with stepped corners, badges, blur, and indicator styles - **PixelSlider** - Segmented sliders with pixel-perfect styling - **PixelToggle** - Toggle switches with blinking and flip animations - **PixelProgressBar** - Progress indicators with segmented, smooth, and icon-filled styles - **PixelTextField** - Input fields with retro styling - **PixelShimmer** - Loading shimmer effects with pixelated overlays +- **PixelHamburgerIcon** - Animated hamburger menu icon with pixel styling ### 🎭 Visual Effects - **Pixelate Effect** - Shader-based pixelation with customizable pixel size @@ -42,7 +47,7 @@ Add this to your package's `pubspec.yaml` file: ```yaml dependencies: - pixelify_flutter: ^0.0.1 + pixelify_flutter: ^0.0.2 audioplayers: ^6.5.0 # For sound effects ``` @@ -241,6 +246,76 @@ PixelPanel( ) ``` +### 📦 PixelSteppedPanel + +Panels with pixel-art stepped corners that can toggle between stepped and smooth rounded corners. + +```dart +// Basic stepped panel +PixelSteppedPanel( + useSteppedCorners: true, + backgroundColor: Color(0xFF2A2D3A), + borderColor: Color(0xFF4A4E65), + child: Text('PRESIDENTIAL BRIEFING'), +) + +// Pill-shaped panel with staircase corners +PixelPillPanel( + useSteppedCorners: true, + cornerSteps: 4, + backgroundColor: Color(0xFF3D4155), + borderColor: Color(0xFF6A6E85), + child: Text('SELECT YOUR RESPONSE'), +) + +// Inset panel with 3D bevel effect +PixelInsetPanel( + useSteppedCorners: true, + backgroundColor: Color(0xFF1A1C24), + borderColor: Color(0xFF3D4155), + child: Text('STATISTICS'), +) +``` + +### 🧭 PixelBottomBar + +Navigation bar with pixel-art stepped corners, badge support, blur effect, and customizable indicator styles. + +```dart +PixelBottomBar( + icons: [ + Icons.home, + Icons.search, + Icons.notifications, + Icons.person, + ], + labels: ['Home', 'Search', 'Alerts', 'Profile'], + currentIndex: selectedIndex, + onTap: (index) => setState(() => selectedIndex = index), + backgroundColor: Color(0xFF1A1C2C), + activeColor: Colors.cyanAccent, + inactiveColor: Colors.grey, + indicatorStyle: PixelBottomBarIndicatorStyle.underline, + activeIndicatorColor: Colors.redAccent, + badges: {2: '5'}, // Show badge on Alerts tab + useBlur: true, +) +``` + +### ☰ PixelHamburgerIcon + +Animated hamburger menu icon with pixel styling. + +```dart +PixelHamburgerIcon( + isOpen: isMenuOpen, + onTap: () => setState(() => isMenuOpen = !isMenuOpen), + color: Colors.cyanAccent, + size: 32, + pixelSize: 3, +) +``` + ## 🎨 Effects & Animations ### Wrapper Widgets From b104dbcc8354c60ef41b2d47ab77448f7f3ca3bc Mon Sep 17 00:00:00 2001 From: John Pope Date: Tue, 9 Dec 2025 13:28:46 +1100 Subject: [PATCH 12/15] Add noiseColor parameter to AnimatedCRTOverlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allows customizing the noise/static color in CRT effects. Default is white (existing behavior), can be set to Colors.black for dark static effects. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- lib/src/widgets/pixel_panel.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/widgets/pixel_panel.dart b/lib/src/widgets/pixel_panel.dart index f2984d5..42d7fd1 100644 --- a/lib/src/widgets/pixel_panel.dart +++ b/lib/src/widgets/pixel_panel.dart @@ -186,12 +186,14 @@ class _NoiseOverlay extends StatelessWidget { final double intensity; final double pixelSize; final int? seed; + final Color noiseColor; const _NoiseOverlay({ Key? key, required this.intensity, required this.pixelSize, this.seed, + this.noiseColor = Colors.white, }) : super(key: key); @override @@ -201,6 +203,7 @@ class _NoiseOverlay extends StatelessWidget { intensity: intensity, pixelSize: pixelSize, seed: seed, + noiseColor: noiseColor, ), ); } @@ -210,18 +213,20 @@ class _NoisePainter extends CustomPainter { final double intensity; final double pixelSize; final int? seed; + final Color noiseColor; late final Random _random; _NoisePainter({ required this.intensity, required this.pixelSize, this.seed, + this.noiseColor = Colors.white, }) : _random = Random(seed); @override void paint(Canvas canvas, Size size) { final paint = Paint() - ..color = Colors.white.withOpacity(intensity) + ..color = noiseColor.withOpacity(intensity) ..style = PaintingStyle.fill; for (double y = 0; y < size.height; y += pixelSize) { @@ -398,6 +403,9 @@ class AnimatedCRTOverlay extends StatefulWidget { /// Animation speed for the noise (frames per second) final int noiseAnimationFps; + /// Color of the noise effect (default white, use Colors.black for dark noise) + final Color noiseColor; + const AnimatedCRTOverlay({ super.key, required this.visible, @@ -409,6 +417,7 @@ class AnimatedCRTOverlay extends StatefulWidget { this.noisePixelSize = 4.0, this.animateNoise = true, this.noiseAnimationFps = 15, + this.noiseColor = Colors.white, }); @override @@ -520,6 +529,7 @@ class _AnimatedCRTOverlayState extends State intensity: widget.noiseIntensity * noiseMultiplier, pixelSize: widget.noisePixelSize, seed: _noiseSeed, + noiseColor: widget.noiseColor, ), ), ], From 54148d29e6453e883a247052a8418bd44d4aa8c5 Mon Sep 17 00:00:00 2001 From: John Pope Date: Sat, 20 Dec 2025 16:31:51 +0800 Subject: [PATCH 13/15] updates --- example/pubspec.lock | 14 +-- lib/src/core/pixel_theme.dart | 91 +++++++++++++++++++- lib/src/widgets/pixel_button.dart | 4 + lib/src/widgets/pixel_notification_card.dart | 8 +- lib/src/widgets/pixel_text.dart | 6 ++ lib/src/widgets/pixel_text_field.dart | 18 ++-- 6 files changed, 122 insertions(+), 19 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index c608396..f56858b 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -220,18 +220,18 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -302,7 +302,7 @@ packages: path: ".." relative: true source: path - version: "0.0.1" + version: "0.0.2" platform: dependency: transitive description: @@ -376,10 +376,10 @@ packages: dependency: transitive description: name: test_api - sha256: "19a78f63e83d3a61f00826d09bc2f60e191bf3504183c001262be6ac75589fb8" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.7" typed_data: dependency: transitive description: diff --git a/lib/src/core/pixel_theme.dart b/lib/src/core/pixel_theme.dart index 35cce85..c7211ea 100644 --- a/lib/src/core/pixel_theme.dart +++ b/lib/src/core/pixel_theme.dart @@ -2,29 +2,112 @@ import 'package:flutter/material.dart'; /// Global PixelTheme for consistent styling across widgets and effects. /// Wrap your root widget with PixelTheme to customize accent color, scale, -/// or shader usage. -/// +/// fonts, or shader usage. +/// +/// {@tool snippet} +/// Basic usage with custom font: +/// ```dart +/// PixelTheme( +/// fontFamily: 'Retro86', +/// textColor: Colors.white, +/// child: MaterialApp(...), +/// ) +/// ``` +/// {@end-tool} class PixelTheme extends InheritedWidget { final Color accentColor; final double pixelScale; final bool enableShaders; + /// The default font family for pixel widgets. + /// Common pixel fonts: 'PressStart2P', 'VT323', 'Retro86', 'PixelMplus10' + final String? fontFamily; + + /// Default text color for pixel widgets. + final Color textColor; + + /// Default font size for pixel widgets. + final double fontSize; + + /// Font weight for pixel text. + final FontWeight fontWeight; + + /// Text shadows for pixel text (retro shadow effect). + final List? textShadows; + const PixelTheme({ super.key, required Widget child, this.accentColor = Colors.cyan, this.pixelScale = 1.0, this.enableShaders = true, + this.fontFamily, + this.textColor = Colors.white, + this.fontSize = 12.0, + this.fontWeight = FontWeight.normal, + this.textShadows, }) : super(child: child); + /// Get the nearest PixelTheme from the widget tree. + /// Returns a default theme if none is found. static PixelTheme of(BuildContext context) { - return context.dependOnInheritedWidgetOfExactType() ?? const PixelTheme(child: SizedBox.shrink()); + return context.dependOnInheritedWidgetOfExactType() ?? + const PixelTheme(child: SizedBox.shrink()); + } + + /// Try to get a PixelTheme from context, returns null if not found. + static PixelTheme? maybeOf(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + /// Get a TextStyle based on the theme settings. + /// Can be overridden with custom parameters. + TextStyle textStyle({ + double? fontSize, + Color? color, + FontWeight? fontWeight, + String? fontFamily, + List? shadows, + }) { + return TextStyle( + fontFamily: fontFamily ?? this.fontFamily, + fontSize: fontSize ?? this.fontSize, + color: color ?? textColor, + fontWeight: fontWeight ?? this.fontWeight, + shadows: shadows ?? textShadows, + ); } @override bool updateShouldNotify(PixelTheme oldWidget) { return accentColor != oldWidget.accentColor || pixelScale != oldWidget.pixelScale || - enableShaders != oldWidget.enableShaders; + enableShaders != oldWidget.enableShaders || + fontFamily != oldWidget.fontFamily || + textColor != oldWidget.textColor || + fontSize != oldWidget.fontSize || + fontWeight != oldWidget.fontWeight || + textShadows != oldWidget.textShadows; + } +} + +/// Extension to easily get pixel theme text style from context. +extension PixelThemeContext on BuildContext { + /// Get the pixel theme's text style. + /// Usage: `context.pixelTextStyle(fontSize: 14)` + TextStyle pixelTextStyle({ + double? fontSize, + Color? color, + FontWeight? fontWeight, + String? fontFamily, + List? shadows, + }) { + return PixelTheme.of(this).textStyle( + fontSize: fontSize, + color: color, + fontWeight: fontWeight, + fontFamily: fontFamily, + shadows: shadows, + ); } } diff --git a/lib/src/widgets/pixel_button.dart b/lib/src/widgets/pixel_button.dart index a973765..30b2920 100644 --- a/lib/src/widgets/pixel_button.dart +++ b/lib/src/widgets/pixel_button.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../core/pixel_theme.dart'; /// Button shape style enum PixelButtonStyle { @@ -110,7 +111,10 @@ class _PixelButtonState extends State with SingleTickerProviderStat @override Widget build(BuildContext context) { + // Use provided textStyle, or fall back to theme, or use defaults + final theme = PixelTheme.maybeOf(context); final textStyle = widget.textStyle ?? + theme?.textStyle(fontSize: 10) ?? const TextStyle( fontFamily: 'PressStart2P', fontSize: 10, diff --git a/lib/src/widgets/pixel_notification_card.dart b/lib/src/widgets/pixel_notification_card.dart index dd9ef9a..fec4733 100644 --- a/lib/src/widgets/pixel_notification_card.dart +++ b/lib/src/widgets/pixel_notification_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../core/pixel_theme.dart'; /// Pixel-art style notification card with avatar, title and action text. /// Inspired by retro game UI notifications. @@ -82,9 +83,12 @@ class _PixelNotificationCardState extends State { @override Widget build(BuildContext context) { + final theme = PixelTheme.maybeOf(context); + final themeFont = theme?.fontFamily ?? 'PressStart2P'; + final defaultTitleStyle = widget.titleStyle ?? TextStyle( - fontFamily: 'PressStart2P', + fontFamily: themeFont, fontSize: 11, color: widget.titleColor, letterSpacing: 0.5, @@ -93,7 +97,7 @@ class _PixelNotificationCardState extends State { final defaultActionStyle = widget.actionStyle ?? TextStyle( - fontFamily: 'PressStart2P', + fontFamily: themeFont, fontSize: 8, color: widget.actionColor, letterSpacing: 0.5, diff --git a/lib/src/widgets/pixel_text.dart b/lib/src/widgets/pixel_text.dart index c0a0870..3e6bd14 100644 --- a/lib/src/widgets/pixel_text.dart +++ b/lib/src/widgets/pixel_text.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; +import '../core/pixel_theme.dart'; enum PixelTextEffect { none, flicker, glitch, scanline, pixelate, typewriter } @@ -176,7 +177,12 @@ class _PixelTextState extends State with SingleTickerProviderStateMix @override Widget build(BuildContext context) { + // Merge theme font family if the provided style doesn't have one + final theme = PixelTheme.maybeOf(context); TextStyle baseStyle = widget.style; + if (baseStyle.fontFamily == null && theme?.fontFamily != null) { + baseStyle = baseStyle.copyWith(fontFamily: theme!.fontFamily); + } Widget textWidget; diff --git a/lib/src/widgets/pixel_text_field.dart b/lib/src/widgets/pixel_text_field.dart index 5151d74..eef35a1 100644 --- a/lib/src/widgets/pixel_text_field.dart +++ b/lib/src/widgets/pixel_text_field.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:audioplayers/audioplayers.dart'; +import '../core/pixel_theme.dart'; enum PixelCursorStyle { block, underline, bar, scanlineFlicker } enum PixelFieldDesign { classic, neon, outlined, pixel } @@ -210,6 +211,11 @@ class _PixelTextFieldState extends FormFieldState } } + String get _themeFont { + final theme = PixelTheme.maybeOf(context); + return theme?.fontFamily ?? 'PressStart2P'; + } + InputDecoration _getInputDecoration() { switch (widget.design) { case PixelFieldDesign.classic: @@ -220,7 +226,7 @@ class _PixelTextFieldState extends FormFieldState hintStyle: widget.placeholderStyle ?? TextStyle( color: Colors.grey.shade600, - fontFamily: 'PressStart2P', + fontFamily: _themeFont, fontSize: widget.fontSize, ), border: OutlineInputBorder( @@ -241,7 +247,7 @@ class _PixelTextFieldState extends FormFieldState hintStyle: widget.placeholderStyle ?? TextStyle( color: Colors.cyanAccent.shade200, - fontFamily: 'PressStart2P', + fontFamily: _themeFont, fontSize: widget.fontSize, ), border: OutlineInputBorder( @@ -261,7 +267,7 @@ class _PixelTextFieldState extends FormFieldState hintStyle: widget.placeholderStyle ?? TextStyle( color: Colors.white70, - fontFamily: 'PressStart2P', + fontFamily: _themeFont, fontSize: widget.fontSize, ), border: OutlineInputBorder( @@ -282,7 +288,7 @@ class _PixelTextFieldState extends FormFieldState hintStyle: widget.placeholderStyle ?? TextStyle( color: Colors.grey.shade500, - fontFamily: 'PressStart2P', + fontFamily: _themeFont, fontSize: widget.fontSize, ), border: OutlineInputBorder( @@ -312,7 +318,7 @@ class _PixelTextFieldState extends FormFieldState cursorWidth: 0, // hide default cursor style: widget.textStyle ?? TextStyle( - fontFamily: 'PressStart2P', // Use your pixel font here + fontFamily: _themeFont, fontSize: widget.fontSize, color: Colors.white, ), @@ -352,7 +358,7 @@ class _PixelTextFieldState extends FormFieldState text: text.isEmpty ? ' ' : text, style: widget.textStyle ?? TextStyle( - fontFamily: 'PressStart2P', + fontFamily: _themeFont, fontSize: widget.fontSize, ), ), From 0575b37eed237d351befd349dcfce9b029f9a07a Mon Sep 17 00:00:00 2001 From: "John D. Pope" Date: Sun, 21 Dec 2025 00:40:11 +1100 Subject: [PATCH 14/15] fix: Downgrade dependencies for Flutter SDK compatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sdk: ^3.9.0 → ^3.0.0 - google_fonts: ^6.2.1 → ^6.1.0 - audioplayers: ^6.5.0 → ^6.0.0 - flutter_lints: ^5.0.0 → ^4.0.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index ab66080..ed277bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,19 +6,19 @@ repository: https://github.com/jyothish-ram/pixelify_flutter environment: - sdk: ^3.9.0 + sdk: ^3.0.0 flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - audioplayers: ^6.5.0 - google_fonts: ^6.2.1 + audioplayers: ^6.0.0 + google_fonts: ^6.1.0 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^4.0.0 flutter: From 58f834e5efa4405c901cc8010f4912178d6524ce Mon Sep 17 00:00:00 2001 From: John Pope Date: Sun, 21 Dec 2025 22:07:25 +0800 Subject: [PATCH 15/15] toast --- example/lib/main.dart | 235 ++++++++ lib/pixelify_flutter.dart | 3 + lib/src/widgets/pixel_toast/pixel_toast.dart | 566 ++++++++++++++++++ .../pixel_toast/pixel_toast.exports.dart | 39 ++ .../widgets/pixel_toast/pixel_toast_icon.dart | 222 +++++++ .../pixel_toast/pixel_toast_painters.dart | 492 +++++++++++++++ .../pixel_toast/pixel_toast_progress.dart | 219 +++++++ .../pixel_toast/pixel_toast_style.dart | 20 + .../widgets/pixel_toast/pixel_toast_type.dart | 62 ++ .../pixel_toast/pixel_toastification.dart | 532 ++++++++++++++++ 10 files changed, 2390 insertions(+) create mode 100644 lib/src/widgets/pixel_toast/pixel_toast.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toast.exports.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toast_icon.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toast_painters.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toast_progress.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toast_style.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toast_type.dart create mode 100644 lib/src/widgets/pixel_toast/pixel_toastification.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index 4db683c..253911d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -228,6 +228,7 @@ class ComponentListPage extends StatelessWidget { @override Widget build(BuildContext context) { final components = [ + ComponentItem('PixelToast', const PixelToastPage()), ComponentItem('PixelBottomBar', const PixelBottomBarPage()), ComponentItem('PixelSteppedPanel', const PixelSteppedPanelPage()), ComponentItem('PixelButton', const PixelButtonPage()), @@ -2034,3 +2035,237 @@ class _PixelSteppedPanelPageState extends State { ); } } + +/// Demo page for PixelToast (toastification integration) +class PixelToastPage extends StatefulWidget { + const PixelToastPage({super.key}); + + @override + State createState() => _PixelToastPageState(); +} + +class _PixelToastPageState extends State { + PixelToastStyle _selectedStyle = PixelToastStyle.stepped; + bool _showScanlines = false; + bool _showProgressBar = true; + bool _showAccentBar = true; + bool _usePixelArtIcons = true; + + @override + Widget build(BuildContext context) { + final smallFont = GoogleFonts.pressStart2p(fontSize: 8, color: Colors.white70); + + return ComponentDemoPage( + title: 'PixelToast', + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('TOAST STYLE:', style: smallFont), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: PixelToastStyle.values.map((style) { + final isSelected = _selectedStyle == style; + return PixelButton( + onPressed: () => setState(() => _selectedStyle = style), + label: style.name.toUpperCase(), + color: isSelected ? Colors.cyanAccent : const Color(0xFF2A2D3A), + textStyle: GoogleFonts.pressStart2p( + fontSize: 8, + color: isSelected ? Colors.black : Colors.white, + ), + pixelSize: 2, + ); + }).toList(), + ), + const SizedBox(height: 24), + + // Options + Text('OPTIONS:', style: smallFont), + const SizedBox(height: 8), + Row( + children: [ + Text('SCANLINES:', style: smallFont), + const SizedBox(width: 8), + PixelToggle( + value: _showScanlines, + onChanged: (v) => setState(() => _showScanlines = v), + onColor: Colors.cyanAccent, + ), + const SizedBox(width: 16), + Text('PROGRESS:', style: smallFont), + const SizedBox(width: 8), + PixelToggle( + value: _showProgressBar, + onChanged: (v) => setState(() => _showProgressBar = v), + onColor: Colors.cyanAccent, + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + Text('ACCENT BAR:', style: smallFont), + const SizedBox(width: 8), + PixelToggle( + value: _showAccentBar, + onChanged: (v) => setState(() => _showAccentBar = v), + onColor: Colors.cyanAccent, + ), + const SizedBox(width: 16), + Text('PIXEL ICONS:', style: smallFont), + const SizedBox(width: 8), + PixelToggle( + value: _usePixelArtIcons, + onChanged: (v) => setState(() => _usePixelArtIcons = v), + onColor: Colors.cyanAccent, + ), + ], + ), + + const SizedBox(height: 24), + + // Show Toast Buttons + Text('SHOW TOASTS:', style: smallFont), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + PixelButton( + onPressed: () => _showToast(PixelToastType.info), + label: 'INFO', + color: const Color(0xFF47AFFF), + pixelSize: 2, + ), + PixelButton( + onPressed: () => _showToast(PixelToastType.success), + label: 'SUCCESS', + color: const Color(0xFF32BC32), + pixelSize: 2, + ), + PixelButton( + onPressed: () => _showToast(PixelToastType.warning), + label: 'WARNING', + color: const Color(0xFFFFB600), + textStyle: GoogleFonts.pressStart2p(fontSize: 8, color: Colors.black), + pixelSize: 2, + ), + PixelButton( + onPressed: () => _showToast(PixelToastType.error), + label: 'ERROR', + color: const Color(0xFFFF3A30), + pixelSize: 2, + ), + ], + ), + + const SizedBox(height: 32), + + // Static Preview + Text('STATIC PREVIEW:', style: smallFont), + const SizedBox(height: 16), + Center( + child: PixelToast( + type: PixelToastType.success, + style: _selectedStyle, + title: 'Quest Complete', + description: 'You have defeated the dragon and earned 1000 gold!', + showScanlines: _showScanlines, + showProgressBar: _showProgressBar, + showAccentBar: _showAccentBar, + usePixelArtIcons: _usePixelArtIcons, + width: 340, + onClose: () {}, + ), + ), + const SizedBox(height: 16), + Center( + child: PixelToast( + type: PixelToastType.error, + style: _selectedStyle, + title: 'Connection Lost', + description: 'Failed to connect to server.', + showScanlines: _showScanlines, + showProgressBar: _showProgressBar, + showAccentBar: _showAccentBar, + usePixelArtIcons: _usePixelArtIcons, + width: 340, + onClose: () {}, + ), + ), + const SizedBox(height: 16), + Center( + child: PixelToast( + type: PixelToastType.warning, + style: _selectedStyle, + title: 'Low Health', + description: 'Your health is below 20%!', + showScanlines: _showScanlines, + showProgressBar: _showProgressBar, + showAccentBar: _showAccentBar, + usePixelArtIcons: _usePixelArtIcons, + width: 340, + onClose: () {}, + ), + ), + const SizedBox(height: 16), + Center( + child: PixelToast( + type: PixelToastType.info, + style: _selectedStyle, + title: 'New Message', + description: 'You have received a message from the king.', + showScanlines: _showScanlines, + showProgressBar: _showProgressBar, + showAccentBar: _showAccentBar, + usePixelArtIcons: _usePixelArtIcons, + width: 340, + onClose: () {}, + ), + ), + + const SizedBox(height: 32), + + // Dismiss All Button + Center( + child: PixelButton( + onPressed: () => pixelToastification.dismissAll(), + label: 'DISMISS ALL TOASTS', + color: const Color(0xFF4A4E65), + pixelSize: 2, + ), + ), + + const SizedBox(height: 32), + ], + ), + ), + ); + } + + void _showToast(PixelToastType type) { + final messages = { + PixelToastType.info: ('New Message', 'You have received a message from the king.'), + PixelToastType.success: ('Quest Complete', 'You have defeated the dragon!'), + PixelToastType.warning: ('Low Health', 'Your health is below 20%!'), + PixelToastType.error: ('Connection Lost', 'Failed to connect to server.'), + PixelToastType.custom: ('Notification', 'Something happened.'), + }; + + final msg = messages[type]!; + + pixelToastification.show( + context: context, + type: type, + style: _selectedStyle, + title: msg.$1, + description: msg.$2, + showScanlines: _showScanlines, + showProgressBar: _showProgressBar, + showAccentBar: _showAccentBar, + ); + } +} diff --git a/lib/pixelify_flutter.dart b/lib/pixelify_flutter.dart index 0276f51..c0760c9 100644 --- a/lib/pixelify_flutter.dart +++ b/lib/pixelify_flutter.dart @@ -20,6 +20,9 @@ export 'src/widgets/pixel_toggle.dart'; export 'src/widgets/pixel_bottom_bar.dart'; export 'src/widgets/pixel_stepped_panel.dart'; +// Pixel Toast (toastification support) +export 'src/widgets/pixel_toast/pixel_toast.exports.dart'; + //Animations export 'src/animations/fade_animation.dart'; export 'src/animations/flicker_animation.dart'; diff --git a/lib/src/widgets/pixel_toast/pixel_toast.dart b/lib/src/widgets/pixel_toast/pixel_toast.dart new file mode 100644 index 0000000..270c1d0 --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast.dart @@ -0,0 +1,566 @@ +import 'package:flutter/material.dart'; +import '../../core/pixel_theme.dart'; +import 'pixel_toast_type.dart'; +import 'pixel_toast_style.dart'; +import 'pixel_toast_painters.dart'; +import 'pixel_toast_progress.dart'; +import 'pixel_toast_icon.dart'; + +/// A pixel-art styled toast notification widget. +/// +/// This widget provides a retro game UI style toast with stepped corners, +/// 3D bevel effects, and pixel-perfect rendering. +/// +/// {@tool snippet} +/// Basic usage: +/// ```dart +/// PixelToast( +/// type: PixelToastType.success, +/// title: 'QUEST COMPLETE', +/// description: 'You have defeated the dragon!', +/// ) +/// ``` +/// {@end-tool} +class PixelToast extends StatefulWidget { + /// The type of toast (info, success, warning, error, custom). + final PixelToastType type; + + /// Visual style of the toast. + final PixelToastStyle style; + + /// Title text displayed in the toast. + final String? title; + + /// Description text displayed below the title. + final String? description; + + /// Custom widget to display instead of title/description. + final Widget? child; + + /// Custom icon to override the default type icon. + final IconData? icon; + + /// Whether to show the icon. + final bool showIcon; + + /// Whether to show the close button. + final bool showCloseButton; + + /// Called when the close button is tapped. + final VoidCallback? onClose; + + /// Called when the toast is tapped. + final VoidCallback? onTap; + + /// Width of the toast. + final double? width; + + /// Minimum height of the toast. + final double? minHeight; + + /// Padding inside the toast. + final EdgeInsets padding; + + /// Size of each pixel unit. + final double pixelSize; + + /// Background color override. + final Color? backgroundColor; + + /// Border color override. + final Color? borderColor; + + /// Primary/accent color override (icon, progress bar). + final Color? primaryColor; + + /// Title text color override. + final Color? titleColor; + + /// Description text color override. + final Color? descriptionColor; + + /// Title text style override. + final TextStyle? titleStyle; + + /// Description text style override. + final TextStyle? descriptionStyle; + + /// Whether to show scanline overlay effect. + final bool showScanlines; + + /// Whether to show a progress bar. + final bool showProgressBar; + + /// Duration for auto-close progress bar. + final Duration? autoCloseDuration; + + /// Whether the progress timer is paused. + final bool isProgressPaused; + + /// Called when progress completes. + final VoidCallback? onProgressComplete; + + /// Whether to show an accent bar on the left. + final bool showAccentBar; + + /// Number of corner steps (for pill style). + final int cornerSteps; + + /// Whether to show drop shadow. + final bool showShadow; + + /// Shadow offset. + final Offset shadowOffset; + + /// Whether to enable press animation. + final bool enablePressAnimation; + + /// Whether to use pixel-art icons. + final bool usePixelArtIcons; + + /// Icon size. + final double iconSize; + + /// Maximum lines for title. + final int titleMaxLines; + + /// Maximum lines for description. + final int descriptionMaxLines; + + const PixelToast({ + super.key, + this.type = PixelToastType.info, + this.style = PixelToastStyle.stepped, + this.title, + this.description, + this.child, + this.icon, + this.showIcon = true, + this.showCloseButton = true, + this.onClose, + this.onTap, + this.width, + this.minHeight, + this.padding = const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + this.pixelSize = 2.0, + this.backgroundColor, + this.borderColor, + this.primaryColor, + this.titleColor, + this.descriptionColor, + this.titleStyle, + this.descriptionStyle, + this.showScanlines = false, + this.showProgressBar = false, + this.autoCloseDuration, + this.isProgressPaused = false, + this.onProgressComplete, + this.showAccentBar = true, + this.cornerSteps = 3, + this.showShadow = true, + this.shadowOffset = const Offset(2, 2), + this.enablePressAnimation = true, + this.usePixelArtIcons = true, + this.iconSize = 24.0, + this.titleMaxLines = 2, + this.descriptionMaxLines = 3, + }); + + @override + State createState() => _PixelToastState(); +} + +class _PixelToastState extends State { + bool _isPressed = false; + + // Color calculations + Color get _backgroundColor { + if (widget.backgroundColor != null) return widget.backgroundColor!; + switch (widget.style) { + case PixelToastStyle.filled: + return widget.type.defaultColor; + default: + return const Color(0xFF2A2D3A); + } + } + + Color get _borderColor { + return widget.borderColor ?? const Color(0xFF4A4E65); + } + + Color get _primaryColor { + return widget.primaryColor ?? widget.type.defaultColor; + } + + Color get _borderLight { + final hsl = HSLColor.fromColor(_backgroundColor); + return hsl.withLightness((hsl.lightness + 0.15).clamp(0.0, 1.0)).toColor(); + } + + Color get _borderShadow { + final hsl = HSLColor.fromColor(_backgroundColor); + return hsl.withLightness((hsl.lightness - 0.15).clamp(0.0, 1.0)).toColor(); + } + + Color get _shadowColor { + return _borderColor.withValues(alpha: 0.5); + } + + Color get _titleColor { + if (widget.titleColor != null) return widget.titleColor!; + switch (widget.style) { + case PixelToastStyle.filled: + return Colors.white; + default: + return Colors.white; + } + } + + Color get _descriptionColor { + if (widget.descriptionColor != null) return widget.descriptionColor!; + switch (widget.style) { + case PixelToastStyle.filled: + return Colors.white.withValues(alpha: 0.8); + default: + return const Color(0xFF8A8E9E); + } + } + + @override + Widget build(BuildContext context) { + final theme = PixelTheme.maybeOf(context); + final fontFamily = theme?.fontFamily ?? 'PressStart2P'; + + final effectiveTitleStyle = widget.titleStyle ?? + TextStyle( + fontFamily: fontFamily, + fontSize: 10, + color: _titleColor, + letterSpacing: 0.5, + height: 1.3, + ); + + final effectiveDescriptionStyle = widget.descriptionStyle ?? + TextStyle( + fontFamily: fontFamily, + fontSize: 8, + color: _descriptionColor, + letterSpacing: 0.3, + height: 1.3, + ); + + // Calculate content padding based on style + EdgeInsets contentPadding; + final basePadding = widget.padding; + switch (widget.style) { + case PixelToastStyle.stepped: + case PixelToastStyle.inset: + case PixelToastStyle.minimal: + final extra = widget.pixelSize * 2; + contentPadding = EdgeInsets.only( + left: basePadding.left + extra, + right: basePadding.right + extra, + top: basePadding.top + extra, + bottom: basePadding.bottom + extra, + ); + break; + case PixelToastStyle.pill: + final extraH = widget.pixelSize * (widget.cornerSteps + 1); + final extraV = widget.pixelSize * 2; + contentPadding = EdgeInsets.only( + left: basePadding.left + extraH, + right: basePadding.right + extraH, + top: basePadding.top + extraV, + bottom: basePadding.bottom + extraV, + ); + break; + case PixelToastStyle.filled: + final extra = widget.pixelSize * 2; + contentPadding = EdgeInsets.only( + left: basePadding.left + extra, + right: basePadding.right + extra, + top: basePadding.top + extra, + bottom: basePadding.bottom + extra, + ); + break; + case PixelToastStyle.simple: + contentPadding = basePadding; + break; + } + + // Add padding for accent bar + if (widget.showAccentBar && widget.style != PixelToastStyle.simple) { + contentPadding = EdgeInsets.only( + left: contentPadding.left + widget.pixelSize * 3, + right: contentPadding.right, + top: contentPadding.top, + bottom: contentPadding.bottom, + ); + } + + Widget content = _buildContent( + effectiveTitleStyle, + effectiveDescriptionStyle, + ); + + // Wrap with painter + Widget paintedContent = widget.style == PixelToastStyle.simple + ? Container( + padding: contentPadding, + child: content, + ) + : CustomPaint( + painter: _getPainter(), + child: Padding( + padding: contentPadding, + child: content, + ), + ); + + // Add progress bar + if (widget.showProgressBar) { + paintedContent = Column( + mainAxisSize: MainAxisSize.min, + children: [ + paintedContent, + Padding( + padding: EdgeInsets.only( + left: widget.pixelSize * 2, + right: widget.pixelSize * 2, + bottom: widget.pixelSize, + ), + child: widget.autoCloseDuration != null + ? AnimatedPixelToastProgress( + duration: widget.autoCloseDuration!, + fillColor: _primaryColor, + backgroundColor: _backgroundColor, + pixelSize: widget.pixelSize, + isPaused: widget.isProgressPaused, + onComplete: widget.onProgressComplete, + ) + : PixelToastProgressBar( + progress: 0.5, + fillColor: _primaryColor, + backgroundColor: _backgroundColor, + pixelSize: widget.pixelSize, + ), + ), + ], + ); + } + + // Add scanline overlay + if (widget.showScanlines) { + paintedContent = Stack( + children: [ + paintedContent, + Positioned.fill( + child: IgnorePointer( + child: CustomPaint( + painter: PixelToastScanlinePainter(), + ), + ), + ), + ], + ); + } + + // Wrap with gesture detector + Widget gestureWrapped = GestureDetector( + onTap: widget.onTap, + onTapDown: widget.enablePressAnimation && widget.onTap != null + ? (_) => setState(() => _isPressed = true) + : null, + onTapUp: widget.enablePressAnimation && widget.onTap != null + ? (_) => setState(() => _isPressed = false) + : null, + onTapCancel: widget.enablePressAnimation && widget.onTap != null + ? () => setState(() => _isPressed = false) + : null, + child: AnimatedContainer( + duration: const Duration(milliseconds: 100), + width: widget.width, + constraints: BoxConstraints( + minHeight: widget.minHeight ?? 0, + ), + transform: _isPressed ? Matrix4.translationValues(1, 1, 0) : Matrix4.identity(), + child: paintedContent, + ), + ); + + return gestureWrapped; + } + + Widget _buildContent(TextStyle titleStyle, TextStyle descriptionStyle) { + if (widget.child != null) { + return widget.child!; + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Icon + if (widget.showIcon) ...[ + PixelToastIcon( + type: widget.type, + icon: widget.icon, + color: _primaryColor, + size: widget.iconSize, + pixelSize: widget.pixelSize, + usePixelArt: widget.usePixelArtIcons, + ), + SizedBox(width: widget.pixelSize * 4), + ], + + // Title and description + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.title != null) + Text( + widget.title!.toUpperCase(), + style: titleStyle, + maxLines: widget.titleMaxLines, + overflow: TextOverflow.ellipsis, + ), + if (widget.title != null && widget.description != null) + SizedBox(height: widget.pixelSize * 2), + if (widget.description != null) + Text( + widget.description!.toUpperCase(), + style: descriptionStyle, + maxLines: widget.descriptionMaxLines, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + + // Close button + if (widget.showCloseButton) ...[ + SizedBox(width: widget.pixelSize * 2), + GestureDetector( + onTap: widget.onClose, + child: _buildCloseButton(), + ), + ], + ], + ); + } + + Widget _buildCloseButton() { + return SizedBox( + width: widget.pixelSize * 8, + height: widget.pixelSize * 8, + child: CustomPaint( + painter: _PixelCloseButtonPainter( + color: _descriptionColor, + pixelSize: widget.pixelSize, + ), + ), + ); + } + + CustomPainter _getPainter() { + switch (widget.style) { + case PixelToastStyle.stepped: + return PixelSteppedBorderPainter( + backgroundColor: _backgroundColor, + borderColor: _borderColor, + borderLight: _borderLight, + borderShadow: _borderShadow, + pixelSize: widget.pixelSize, + accentColor: widget.showAccentBar ? _primaryColor : null, + showAccentBar: widget.showAccentBar, + ); + case PixelToastStyle.pill: + return PixelPillBorderPainter( + backgroundColor: _backgroundColor, + borderColor: _borderColor, + shadowColor: _shadowColor, + pixelSize: widget.pixelSize, + steps: widget.cornerSteps, + showShadow: widget.showShadow, + shadowOffset: widget.shadowOffset, + accentColor: widget.showAccentBar ? _primaryColor : null, + ); + case PixelToastStyle.inset: + return PixelInsetBorderPainter( + backgroundColor: _backgroundColor, + borderColor: _borderColor, + highlightColor: _borderLight, + shadowColor: _borderShadow, + pixelSize: widget.pixelSize, + accentColor: widget.showAccentBar ? _primaryColor : null, + ); + case PixelToastStyle.filled: + return PixelFilledBorderPainter( + fillColor: _backgroundColor, + borderColor: _borderShadow, + pixelSize: widget.pixelSize, + ); + case PixelToastStyle.minimal: + return PixelMinimalBorderPainter( + backgroundColor: _backgroundColor, + borderColor: _borderColor, + pixelSize: widget.pixelSize, + accentColor: widget.showAccentBar ? _primaryColor : null, + ); + case PixelToastStyle.simple: + // Simple style doesn't use a painter + return PixelMinimalBorderPainter( + backgroundColor: Colors.transparent, + borderColor: Colors.transparent, + pixelSize: widget.pixelSize, + ); + } + } +} + +/// Painter for pixel-art close button (X). +class _PixelCloseButtonPainter extends CustomPainter { + final Color color; + final double pixelSize; + + _PixelCloseButtonPainter({ + required this.color, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint()..color = color; + final p = pixelSize; + + // X pattern (4x4 pixels) + // .X..X. + // ..XX.. + // ..XX.. + // .X..X. + void drawPixel(double x, double y) { + canvas.drawRect(Rect.fromLTWH(x, y, p, p), paint); + } + + final cx = size.width / 2; + final cy = size.height / 2; + + // Top-left to bottom-right diagonal + drawPixel(cx - p * 1.5, cy - p * 1.5); + drawPixel(cx - p * 0.5, cy - p * 0.5); + drawPixel(cx + p * 0.5, cy + p * 0.5); + drawPixel(cx + p * 1.5, cy + p * 1.5); + + // Top-right to bottom-left diagonal + drawPixel(cx + p * 1.5, cy - p * 1.5); + drawPixel(cx + p * 0.5, cy - p * 0.5); + drawPixel(cx - p * 0.5, cy + p * 0.5); + drawPixel(cx - p * 1.5, cy + p * 1.5); + } + + @override + bool shouldRepaint(covariant _PixelCloseButtonPainter oldDelegate) { + return oldDelegate.color != color || oldDelegate.pixelSize != pixelSize; + } +} diff --git a/lib/src/widgets/pixel_toast/pixel_toast.exports.dart b/lib/src/widgets/pixel_toast/pixel_toast.exports.dart new file mode 100644 index 0000000..57e695e --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast.exports.dart @@ -0,0 +1,39 @@ +/// Pixel Toast - Retro game UI style toasts with stepped corners. +/// +/// This module provides pixel-art styled toast notifications compatible with +/// the PixelTheme system in pixelify_flutter. +/// +/// Usage: +/// ```dart +/// // Wrap your app (optional, for global config) +/// PixelToastificationWrapper( +/// config: PixelToastConfig( +/// alignment: Alignment.topRight, +/// autoCloseDuration: Duration(seconds: 4), +/// ), +/// child: MaterialApp(...), +/// ) +/// +/// // Show toasts anywhere +/// pixelToastification.show( +/// context: context, +/// type: PixelToastType.success, +/// title: 'Quest Complete', +/// description: 'You earned 100 gold!', +/// ); +/// +/// // Or use context extension +/// context.showPixelToastSuccess( +/// title: 'Level Up!', +/// description: 'You reached level 10.', +/// ); +/// ``` +library pixel_toast; + +export 'pixel_toast.dart'; +export 'pixel_toast_type.dart'; +export 'pixel_toast_style.dart'; +export 'pixel_toast_painters.dart'; +export 'pixel_toast_progress.dart'; +export 'pixel_toast_icon.dart'; +export 'pixel_toastification.dart'; diff --git a/lib/src/widgets/pixel_toast/pixel_toast_icon.dart b/lib/src/widgets/pixel_toast/pixel_toast_icon.dart new file mode 100644 index 0000000..e446a0a --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast_icon.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'pixel_toast_type.dart'; + +/// Pixel-style icon for toast notifications. +/// Uses CustomPaint to create pixel-perfect icons that match the retro aesthetic. +class PixelToastIcon extends StatelessWidget { + /// The toast type determines the default icon. + final PixelToastType type; + + /// Custom icon to override the default. + final IconData? icon; + + /// Icon color. + final Color? color; + + /// Size of the icon. + final double size; + + /// Size of each pixel unit. + final double pixelSize; + + /// Whether to use custom pixel-art icons or Material icons. + final bool usePixelArt; + + const PixelToastIcon({ + super.key, + required this.type, + this.icon, + this.color, + this.size = 24.0, + this.pixelSize = 2.0, + this.usePixelArt = true, + }); + + @override + Widget build(BuildContext context) { + final iconColor = color ?? type.defaultColor; + + if (usePixelArt) { + return SizedBox( + width: size, + height: size, + child: CustomPaint( + painter: _PixelIconPainter( + type: type, + color: iconColor, + pixelSize: pixelSize, + ), + ), + ); + } + + return Icon( + icon ?? type.defaultIcon, + color: iconColor, + size: size, + ); + } +} + +/// Custom painter for pixel-art style icons. +class _PixelIconPainter extends CustomPainter { + final PixelToastType type; + final Color color; + final double pixelSize; + + _PixelIconPainter({ + required this.type, + required this.color, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final paint = Paint()..color = color; + + // Center the icon + final offsetX = (size.width - p * 8) / 2; + final offsetY = (size.height - p * 8) / 2; + + void drawPixel(int x, int y) { + canvas.drawRect( + Rect.fromLTWH(offsetX + x * p, offsetY + y * p, p, p), + paint, + ); + } + + switch (type) { + case PixelToastType.info: + _drawInfoIcon(drawPixel); + break; + case PixelToastType.success: + _drawSuccessIcon(drawPixel); + break; + case PixelToastType.warning: + _drawWarningIcon(drawPixel); + break; + case PixelToastType.error: + _drawErrorIcon(drawPixel); + break; + case PixelToastType.custom: + _drawInfoIcon(drawPixel); + break; + } + } + + /// 8x8 pixel "i" info icon. + void _drawInfoIcon(void Function(int x, int y) drawPixel) { + // Circle outline + for (int x = 2; x <= 5; x++) { + drawPixel(x, 0); // top + drawPixel(x, 7); // bottom + } + for (int y = 2; y <= 5; y++) { + drawPixel(0, y); // left + drawPixel(7, y); // right + } + drawPixel(1, 1); + drawPixel(6, 1); + drawPixel(1, 6); + drawPixel(6, 6); + + // "i" letter + drawPixel(3, 2); + drawPixel(4, 2); + drawPixel(3, 4); + drawPixel(4, 4); + drawPixel(3, 5); + drawPixel(4, 5); + } + + /// 8x8 pixel checkmark success icon. + void _drawSuccessIcon(void Function(int x, int y) drawPixel) { + // Circle outline + for (int x = 2; x <= 5; x++) { + drawPixel(x, 0); + drawPixel(x, 7); + } + for (int y = 2; y <= 5; y++) { + drawPixel(0, y); + drawPixel(7, y); + } + drawPixel(1, 1); + drawPixel(6, 1); + drawPixel(1, 6); + drawPixel(6, 6); + + // Checkmark + drawPixel(2, 4); + drawPixel(3, 5); + drawPixel(4, 4); + drawPixel(5, 3); + drawPixel(6, 2); + } + + /// 8x8 pixel warning triangle icon. + void _drawWarningIcon(void Function(int x, int y) drawPixel) { + // Triangle outline + drawPixel(3, 0); + drawPixel(4, 0); + drawPixel(2, 1); + drawPixel(5, 1); + drawPixel(2, 2); + drawPixel(5, 2); + drawPixel(1, 3); + drawPixel(6, 3); + drawPixel(1, 4); + drawPixel(6, 4); + drawPixel(0, 5); + drawPixel(7, 5); + drawPixel(0, 6); + drawPixel(7, 6); + for (int x = 0; x <= 7; x++) { + drawPixel(x, 7); + } + + // Exclamation mark + drawPixel(3, 2); + drawPixel(4, 2); + drawPixel(3, 3); + drawPixel(4, 3); + drawPixel(3, 4); + drawPixel(4, 4); + drawPixel(3, 6); + drawPixel(4, 6); + } + + /// 8x8 pixel X error icon. + void _drawErrorIcon(void Function(int x, int y) drawPixel) { + // Circle outline + for (int x = 2; x <= 5; x++) { + drawPixel(x, 0); + drawPixel(x, 7); + } + for (int y = 2; y <= 5; y++) { + drawPixel(0, y); + drawPixel(7, y); + } + drawPixel(1, 1); + drawPixel(6, 1); + drawPixel(1, 6); + drawPixel(6, 6); + + // X mark + drawPixel(2, 2); + drawPixel(5, 2); + drawPixel(3, 3); + drawPixel(4, 3); + drawPixel(3, 4); + drawPixel(4, 4); + drawPixel(2, 5); + drawPixel(5, 5); + } + + @override + bool shouldRepaint(covariant _PixelIconPainter oldDelegate) { + return oldDelegate.type != type || + oldDelegate.color != color || + oldDelegate.pixelSize != pixelSize; + } +} diff --git a/lib/src/widgets/pixel_toast/pixel_toast_painters.dart b/lib/src/widgets/pixel_toast/pixel_toast_painters.dart new file mode 100644 index 0000000..6f20e03 --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast_painters.dart @@ -0,0 +1,492 @@ +import 'package:flutter/material.dart'; + +/// Painter for stepped pixel border with 3D bevel effect. +/// This is the classic retro game UI border style. +class PixelSteppedBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color borderLight; + final Color borderShadow; + final double pixelSize; + final Color? accentColor; + final bool showAccentBar; + + PixelSteppedBorderPainter({ + required this.backgroundColor, + required this.borderColor, + required this.borderLight, + required this.borderShadow, + required this.pixelSize, + this.accentColor, + this.showAccentBar = true, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with 3D bevel === + drawHLine(p * 2, w - p * 2, p, borderLight); + drawVLine(p, p * 2, h - p * 2, borderLight); + drawHLine(p * 2, w - p * 2, h - p * 2, borderShadow); + drawVLine(w - p * 2, p * 2, h - p * 2, borderShadow); + + // Inner corner pixels for bevel + drawPixel(p, p, borderLight); + drawPixel(w - p * 2, p, borderLight); + drawPixel(p, h - p * 2, borderShadow); + drawPixel(w - p * 2, h - p * 2, borderShadow); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + + // === Accent bar on left side (optional) === + if (showAccentBar && accentColor != null) { + drawVLine(p * 2, p * 2, h - p * 2, accentColor!); + drawVLine(p * 3, p * 2, h - p * 2, accentColor!); + } + } + + @override + bool shouldRepaint(covariant PixelSteppedBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.borderLight != borderLight || + oldDelegate.borderShadow != borderShadow || + oldDelegate.pixelSize != pixelSize || + oldDelegate.accentColor != accentColor || + oldDelegate.showAccentBar != showAccentBar; + } +} + +/// Painter for pill-shaped toast with staircase corners. +class PixelPillBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color shadowColor; + final double pixelSize; + final int steps; + final bool showShadow; + final Offset shadowOffset; + final Color? accentColor; + + PixelPillBorderPainter({ + required this.backgroundColor, + required this.borderColor, + required this.shadowColor, + required this.pixelSize, + this.steps = 3, + this.showShadow = true, + this.shadowOffset = const Offset(2, 2), + this.accentColor, + }); + + Path _buildSteppedPath(double w, double h, double p, double offsetX, double offsetY) { + final path = Path(); + final int s = steps; + + path.moveTo(p * s + offsetX, offsetY); + path.lineTo(w - p * s + offsetX, offsetY); + + // Top-right corner + for (int i = 0; i < s; i++) { + path.lineTo(w - p * (s - 1 - i) + offsetX, p * i + offsetY); + path.lineTo(w - p * (s - 1 - i) + offsetX, p * (i + 1) + offsetY); + } + + path.lineTo(w + offsetX, h - p * s + offsetY); + + // Bottom-right corner + for (int i = 0; i < s; i++) { + path.lineTo(w - p * i + offsetX, h - p * (s - 1 - i) + offsetY); + path.lineTo(w - p * (i + 1) + offsetX, h - p * (s - 1 - i) + offsetY); + } + + path.lineTo(p * s + offsetX, h + offsetY); + + // Bottom-left corner + for (int i = 0; i < s; i++) { + path.lineTo(p * (s - 1 - i) + offsetX, h - p * i + offsetY); + path.lineTo(p * (s - 1 - i) + offsetX, h - p * (i + 1) + offsetY); + } + + path.lineTo(offsetX, p * s + offsetY); + + // Top-left corner + for (int i = 0; i < s; i++) { + path.lineTo(p * i + offsetX, p * (s - 1 - i) + offsetY); + path.lineTo(p * (i + 1) + offsetX, p * (s - 1 - i) + offsetY); + } + + path.close(); + return path; + } + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + // Draw shadow first + if (showShadow) { + final shadowPath = _buildSteppedPath(w, h, p, shadowOffset.dx, shadowOffset.dy); + canvas.drawPath(shadowPath, Paint()..color = shadowColor); + } + + // Build main path + final path = _buildSteppedPath(w, h, p, 0, 0); + + // Fill background + canvas.drawPath(path, Paint()..color = backgroundColor); + + // Draw border + canvas.drawPath( + path, + Paint() + ..color = borderColor + ..style = PaintingStyle.stroke + ..strokeWidth = p, + ); + + // Draw accent bar on left if provided + if (accentColor != null) { + final accentPath = Path(); + accentPath.addRect(Rect.fromLTWH(p * steps, p * 2, p * 2, h - p * 4)); + canvas.drawPath(accentPath, Paint()..color = accentColor!); + } + } + + @override + bool shouldRepaint(covariant PixelPillBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.steps != steps || + oldDelegate.showShadow != showShadow || + oldDelegate.shadowOffset != shadowOffset || + oldDelegate.accentColor != accentColor; + } +} + +/// Painter for inset/recessed toast with pressed-in 3D bevel. +class PixelInsetBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final Color highlightColor; + final Color shadowColor; + final double pixelSize; + final Color? accentColor; + + PixelInsetBorderPainter({ + required this.backgroundColor, + required this.borderColor, + required this.highlightColor, + required this.shadowColor, + required this.pixelSize, + this.accentColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === BORDER 1: Outer border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels for stepped effect + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === BORDER 2: Inner border with inverted 3D bevel (inset) === + drawHLine(p * 2, w - p * 2, p, shadowColor); + drawVLine(p, p * 2, h - p * 2, shadowColor); + drawHLine(p * 2, w - p * 2, h - p * 2, highlightColor); + drawVLine(w - p * 2, p * 2, h - p * 2, highlightColor); + + // Inner corner pixels for bevel + drawPixel(p, p, shadowColor); + drawPixel(w - p * 2, p, shadowColor); + drawPixel(p, h - p * 2, highlightColor); + drawPixel(w - p * 2, h - p * 2, highlightColor); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p * 2, p * 2, w - p * 4, h - p * 4), + bgPaint, + ); + + // === Accent bar on left side (optional) === + if (accentColor != null) { + drawVLine(p * 2, p * 2, h - p * 2, accentColor!); + drawVLine(p * 3, p * 2, h - p * 2, accentColor!); + } + } + + @override + bool shouldRepaint(covariant PixelInsetBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.highlightColor != highlightColor || + oldDelegate.shadowColor != shadowColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.accentColor != accentColor; + } +} + +/// Painter for filled style toast (solid background with type color). +class PixelFilledBorderPainter extends CustomPainter { + final Color fillColor; + final Color borderColor; + final double pixelSize; + + PixelFilledBorderPainter({ + required this.fillColor, + required this.borderColor, + required this.pixelSize, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === Border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === Background fill (including edge pixels) === + final bgPaint = Paint()..color = fillColor; + // Main fill + canvas.drawRect( + Rect.fromLTWH(p, p * 2, w - p * 2, h - p * 4), + bgPaint, + ); + // Top and bottom fills + canvas.drawRect( + Rect.fromLTWH(p * 2, p, w - p * 4, p), + bgPaint, + ); + canvas.drawRect( + Rect.fromLTWH(p * 2, h - p * 2, w - p * 4, p), + bgPaint, + ); + } + + @override + bool shouldRepaint(covariant PixelFilledBorderPainter oldDelegate) { + return oldDelegate.fillColor != fillColor || + oldDelegate.borderColor != borderColor || + oldDelegate.pixelSize != pixelSize; + } +} + +/// Painter for minimal style toast (just stepped border, no 3D effect). +class PixelMinimalBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderColor; + final double pixelSize; + final Color? accentColor; + + PixelMinimalBorderPainter({ + required this.backgroundColor, + required this.borderColor, + required this.pixelSize, + this.accentColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final double p = pixelSize; + final double w = size.width; + final double h = size.height; + + void drawPixel(double x, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y, p, p), + Paint()..color = color, + ); + } + + void drawHLine(double x1, double x2, double y, Color color) { + canvas.drawRect( + Rect.fromLTWH(x1, y, x2 - x1, p), + Paint()..color = color, + ); + } + + void drawVLine(double x, double y1, double y2, Color color) { + canvas.drawRect( + Rect.fromLTWH(x, y1, p, y2 - y1), + Paint()..color = color, + ); + } + + // === Border with stepped corners === + drawHLine(p * 2, w - p * 2, 0, borderColor); + drawHLine(p * 2, w - p * 2, h - p, borderColor); + drawVLine(0, p * 2, h - p * 2, borderColor); + drawVLine(w - p, p * 2, h - p * 2, borderColor); + + // Corner pixels + drawPixel(p, p, borderColor); + drawPixel(w - p * 2, p, borderColor); + drawPixel(p, h - p * 2, borderColor); + drawPixel(w - p * 2, h - p * 2, borderColor); + + // === Background fill === + final bgPaint = Paint()..color = backgroundColor; + canvas.drawRect( + Rect.fromLTWH(p, p * 2, w - p * 2, h - p * 4), + bgPaint, + ); + canvas.drawRect( + Rect.fromLTWH(p * 2, p, w - p * 4, p), + bgPaint, + ); + canvas.drawRect( + Rect.fromLTWH(p * 2, h - p * 2, w - p * 4, p), + bgPaint, + ); + + // === Accent bar on left side (optional) === + if (accentColor != null) { + drawVLine(p, p * 2, h - p * 2, accentColor!); + drawVLine(p * 2, p * 2, h - p * 2, accentColor!); + } + } + + @override + bool shouldRepaint(covariant PixelMinimalBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderColor != borderColor || + oldDelegate.pixelSize != pixelSize || + oldDelegate.accentColor != accentColor; + } +} + +/// Simple scanline painter for CRT overlay effect on toasts. +class PixelToastScanlinePainter extends CustomPainter { + final double opacity; + final double spacing; + + PixelToastScanlinePainter({ + this.opacity = 0.1, + this.spacing = 2.0, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = Colors.black.withValues(alpha: opacity) + ..strokeWidth = 1; + + for (double y = 0; y < size.height; y += spacing) { + canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); + } + } + + @override + bool shouldRepaint(covariant PixelToastScanlinePainter oldDelegate) { + return oldDelegate.opacity != opacity || oldDelegate.spacing != spacing; + } +} diff --git a/lib/src/widgets/pixel_toast/pixel_toast_progress.dart b/lib/src/widgets/pixel_toast/pixel_toast_progress.dart new file mode 100644 index 0000000..7533baf --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast_progress.dart @@ -0,0 +1,219 @@ +import 'package:flutter/material.dart'; + +/// Pixel-style progress bar for toast auto-close indication. +/// Uses segmented pixel design matching the retro aesthetic. +class PixelToastProgressBar extends StatelessWidget { + /// Current progress value from 0.0 to 1.0. + final double progress; + + /// Color of the filled progress segments. + final Color fillColor; + + /// Background color for unfilled segments. + final Color backgroundColor; + + /// Size of each pixel segment. + final double pixelSize; + + /// Height of the progress bar (in pixel units). + final int heightInPixels; + + /// Whether to show segmented style or smooth. + final bool segmented; + + /// Gap between segments (only for segmented style). + final double segmentGap; + + const PixelToastProgressBar({ + super.key, + required this.progress, + this.fillColor = Colors.cyan, + this.backgroundColor = const Color(0xFF1A1C24), + this.pixelSize = 2.0, + this.heightInPixels = 2, + this.segmented = true, + this.segmentGap = 1.0, + }); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: pixelSize * heightInPixels, + child: CustomPaint( + painter: _PixelProgressPainter( + progress: progress.clamp(0.0, 1.0), + fillColor: fillColor, + backgroundColor: backgroundColor, + pixelSize: pixelSize, + segmented: segmented, + segmentGap: segmentGap, + ), + size: Size.infinite, + ), + ); + } +} + +class _PixelProgressPainter extends CustomPainter { + final double progress; + final Color fillColor; + final Color backgroundColor; + final double pixelSize; + final bool segmented; + final double segmentGap; + + _PixelProgressPainter({ + required this.progress, + required this.fillColor, + required this.backgroundColor, + required this.pixelSize, + required this.segmented, + required this.segmentGap, + }); + + @override + void paint(Canvas canvas, Size size) { + final double w = size.width; + final double h = size.height; + + // Draw background + canvas.drawRect( + Rect.fromLTWH(0, 0, w, h), + Paint()..color = backgroundColor, + ); + + if (progress <= 0) return; + + if (segmented) { + // Calculate number of segments + final segmentWidth = pixelSize * 2; + final totalSegmentWidth = segmentWidth + segmentGap; + final numSegments = (w / totalSegmentWidth).floor(); + final filledSegments = (numSegments * progress).ceil(); + + final fillPaint = Paint()..color = fillColor; + + for (int i = 0; i < filledSegments; i++) { + final x = i * totalSegmentWidth; + if (x + segmentWidth <= w) { + canvas.drawRect( + Rect.fromLTWH(x, 0, segmentWidth, h), + fillPaint, + ); + } + } + } else { + // Smooth progress bar + canvas.drawRect( + Rect.fromLTWH(0, 0, w * progress, h), + Paint()..color = fillColor, + ); + } + } + + @override + bool shouldRepaint(covariant _PixelProgressPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.fillColor != fillColor || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.segmented != segmented; + } +} + +/// Animated pixel progress bar that counts down automatically. +class AnimatedPixelToastProgress extends StatefulWidget { + /// Total duration for the progress animation. + final Duration duration; + + /// Called when progress completes. + final VoidCallback? onComplete; + + /// Color of the filled progress segments. + final Color fillColor; + + /// Background color for unfilled segments. + final Color backgroundColor; + + /// Size of each pixel segment. + final double pixelSize; + + /// Whether the timer should be paused. + final bool isPaused; + + /// Whether to animate from full to empty (true) or empty to full (false). + final bool countdown; + + const AnimatedPixelToastProgress({ + super.key, + required this.duration, + this.onComplete, + this.fillColor = Colors.cyan, + this.backgroundColor = const Color(0xFF1A1C24), + this.pixelSize = 2.0, + this.isPaused = false, + this.countdown = true, + }); + + @override + State createState() => _AnimatedPixelToastProgressState(); +} + +class _AnimatedPixelToastProgressState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + ); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + widget.onComplete?.call(); + } + }); + + if (!widget.isPaused) { + _controller.forward(); + } + } + + @override + void didUpdateWidget(AnimatedPixelToastProgress oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.isPaused != oldWidget.isPaused) { + if (widget.isPaused) { + _controller.stop(); + } else { + _controller.forward(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + final progress = widget.countdown + ? 1.0 - _controller.value + : _controller.value; + return PixelToastProgressBar( + progress: progress, + fillColor: widget.fillColor, + backgroundColor: widget.backgroundColor, + pixelSize: widget.pixelSize, + ); + }, + ); + } +} diff --git a/lib/src/widgets/pixel_toast/pixel_toast_style.dart b/lib/src/widgets/pixel_toast/pixel_toast_style.dart new file mode 100644 index 0000000..dff776c --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast_style.dart @@ -0,0 +1,20 @@ +/// Visual styles for pixel toasts. +enum PixelToastStyle { + /// Stepped pixel border with 3D bevel effect (classic retro game UI). + stepped, + + /// Pill-shaped with multi-step staircase corners. + pill, + + /// Inset/recessed style with pressed-in 3D bevel. + inset, + + /// Filled background with the toast type color. + filled, + + /// Minimal style with just stepped border, no 3D effect. + minimal, + + /// Simple text-only style (no border, no icons). + simple, +} diff --git a/lib/src/widgets/pixel_toast/pixel_toast_type.dart b/lib/src/widgets/pixel_toast/pixel_toast_type.dart new file mode 100644 index 0000000..dd0979a --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toast_type.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +/// Toast type for pixel-style notifications. +/// Each type has a default color and icon. +enum PixelToastType { + info, + success, + warning, + error, + custom, +} + +/// Extension to provide colors and icons for each toast type. +extension PixelToastTypeExtension on PixelToastType { + /// Default color for each toast type (retro game palette). + Color get defaultColor { + switch (this) { + case PixelToastType.info: + return const Color(0xFF47AFFF); // Cyan blue + case PixelToastType.success: + return const Color(0xFF32BC32); // Bright green + case PixelToastType.warning: + return const Color(0xFFFFB600); // Amber/Gold + case PixelToastType.error: + return const Color(0xFFFF3A30); // Red + case PixelToastType.custom: + return const Color(0xFF8A8E9E); // Gray + } + } + + /// Darker variant for backgrounds. + Color get darkColor { + switch (this) { + case PixelToastType.info: + return const Color(0xFF1A3A5C); + case PixelToastType.success: + return const Color(0xFF1A4A1A); + case PixelToastType.warning: + return const Color(0xFF4A3A00); + case PixelToastType.error: + return const Color(0xFF4A1A1A); + case PixelToastType.custom: + return const Color(0xFF2A2D3A); + } + } + + /// Default icon for each toast type (using Material icons that look good pixelated). + IconData get defaultIcon { + switch (this) { + case PixelToastType.info: + return Icons.info_outline; + case PixelToastType.success: + return Icons.check_circle_outline; + case PixelToastType.warning: + return Icons.warning_amber_rounded; + case PixelToastType.error: + return Icons.error_outline; + case PixelToastType.custom: + return Icons.notifications_outlined; + } + } +} diff --git a/lib/src/widgets/pixel_toast/pixel_toastification.dart b/lib/src/widgets/pixel_toast/pixel_toastification.dart new file mode 100644 index 0000000..ce865a8 --- /dev/null +++ b/lib/src/widgets/pixel_toast/pixel_toastification.dart @@ -0,0 +1,532 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'pixel_toast.dart'; +import 'pixel_toast_type.dart'; +import 'pixel_toast_style.dart'; + +/// Configuration for pixel toasts. +class PixelToastConfig { + /// Default alignment for toasts. + final Alignment alignment; + + /// Default width for toasts. + final double width; + + /// Duration for auto-close. + final Duration autoCloseDuration; + + /// Animation duration for show/hide. + final Duration animationDuration; + + /// Maximum number of toasts visible at once. + final int maxToasts; + + /// Margin between toasts. + final double toastMargin; + + /// Margin from screen edges. + final EdgeInsets screenMargin; + + const PixelToastConfig({ + this.alignment = Alignment.topRight, + this.width = 320, + this.autoCloseDuration = const Duration(seconds: 4), + this.animationDuration = const Duration(milliseconds: 300), + this.maxToasts = 5, + this.toastMargin = 8, + this.screenMargin = const EdgeInsets.all(16), + }); +} + +/// Item representing a single toast in the queue. +class PixelToastItem { + final String id; + final Widget toast; + final Duration? autoCloseDuration; + final VoidCallback? onDismiss; + Timer? _timer; + bool _isPaused = false; + Duration? _remainingDuration; + + PixelToastItem({ + required this.id, + required this.toast, + this.autoCloseDuration, + this.onDismiss, + }); + + void startTimer(VoidCallback onComplete) { + if (autoCloseDuration == null) return; + _remainingDuration = autoCloseDuration; + _timer = Timer(_remainingDuration!, onComplete); + } + + void pauseTimer() { + if (_timer != null && _timer!.isActive) { + _isPaused = true; + _timer!.cancel(); + } + } + + void resumeTimer(VoidCallback onComplete) { + if (_isPaused && _remainingDuration != null) { + _isPaused = false; + _timer = Timer(_remainingDuration!, onComplete); + } + } + + void cancelTimer() { + _timer?.cancel(); + _timer = null; + } +} + +/// Singleton manager for pixel toasts. +/// +/// Usage: +/// ```dart +/// // Wrap your app +/// PixelToastificationWrapper( +/// child: MaterialApp(...), +/// ) +/// +/// // Show toasts anywhere +/// pixelToastification.show( +/// context: context, +/// type: PixelToastType.success, +/// title: 'Success!', +/// description: 'Operation completed.', +/// ); +/// ``` +class PixelToastification { + static final PixelToastification _instance = PixelToastification._internal(); + factory PixelToastification() => _instance; + PixelToastification._internal(); + + final List _items = []; + final _itemsNotifier = ValueNotifier>([]); + OverlayEntry? _overlayEntry; + PixelToastConfig _config = const PixelToastConfig(); + + /// Get current items (for overlay builder). + ValueNotifier> get itemsNotifier => _itemsNotifier; + + /// Update the global configuration. + void configure(PixelToastConfig config) { + _config = config; + } + + /// Show a pixel toast. + PixelToastItem show({ + required BuildContext context, + PixelToastType type = PixelToastType.info, + PixelToastStyle style = PixelToastStyle.stepped, + String? title, + String? description, + Widget? child, + IconData? icon, + bool showIcon = true, + bool showCloseButton = true, + VoidCallback? onTap, + VoidCallback? onDismiss, + Duration? autoCloseDuration, + double? width, + double pixelSize = 2.0, + Color? backgroundColor, + Color? borderColor, + Color? primaryColor, + bool showScanlines = false, + bool showProgressBar = true, + bool showAccentBar = true, + bool pauseOnHover = true, + }) { + final effectiveDuration = autoCloseDuration ?? _config.autoCloseDuration; + final id = DateTime.now().millisecondsSinceEpoch.toString(); + + late PixelToastItem item; + + final toast = _PixelToastWrapper( + id: id, + pauseOnHover: pauseOnHover, + onPauseChanged: (paused) { + if (paused) { + item.pauseTimer(); + } else { + item.resumeTimer(() => dismiss(item)); + } + }, + child: PixelToast( + type: type, + style: style, + title: title, + description: description, + icon: icon, + showIcon: showIcon, + showCloseButton: showCloseButton, + onClose: () => dismiss(item), + onTap: onTap, + width: width ?? _config.width, + pixelSize: pixelSize, + backgroundColor: backgroundColor, + borderColor: borderColor, + primaryColor: primaryColor, + showScanlines: showScanlines, + showProgressBar: showProgressBar, + autoCloseDuration: showProgressBar ? effectiveDuration : null, + showAccentBar: showAccentBar, + child: child, + ), + ); + + item = PixelToastItem( + id: id, + toast: toast, + autoCloseDuration: effectiveDuration, + onDismiss: onDismiss, + ); + + _items.insert(0, item); + + // Remove oldest if exceeds max + while (_items.length > _config.maxToasts) { + final removed = _items.removeLast(); + removed.cancelTimer(); + removed.onDismiss?.call(); + } + + _itemsNotifier.value = List.from(_items); + + // Start auto-close timer + item.startTimer(() => dismiss(item)); + + // Ensure overlay is showing + _ensureOverlay(context); + + return item; + } + + /// Show a custom toast widget. + PixelToastItem showCustom({ + required BuildContext context, + required Widget toast, + Duration? autoCloseDuration, + VoidCallback? onDismiss, + }) { + final id = DateTime.now().millisecondsSinceEpoch.toString(); + final effectiveDuration = autoCloseDuration ?? _config.autoCloseDuration; + + final item = PixelToastItem( + id: id, + toast: toast, + autoCloseDuration: effectiveDuration, + onDismiss: onDismiss, + ); + + _items.insert(0, item); + + while (_items.length > _config.maxToasts) { + final removed = _items.removeLast(); + removed.cancelTimer(); + removed.onDismiss?.call(); + } + + _itemsNotifier.value = List.from(_items); + item.startTimer(() => dismiss(item)); + _ensureOverlay(context); + + return item; + } + + /// Dismiss a specific toast. + void dismiss(PixelToastItem item) { + item.cancelTimer(); + _items.removeWhere((i) => i.id == item.id); + _itemsNotifier.value = List.from(_items); + item.onDismiss?.call(); + + if (_items.isEmpty) { + _removeOverlay(); + } + } + + /// Dismiss toast by ID. + void dismissById(String id) { + final item = _items.firstWhere( + (i) => i.id == id, + orElse: () => throw StateError('Toast not found'), + ); + dismiss(item); + } + + /// Dismiss all toasts. + void dismissAll() { + for (final item in _items) { + item.cancelTimer(); + item.onDismiss?.call(); + } + _items.clear(); + _itemsNotifier.value = []; + _removeOverlay(); + } + + void _ensureOverlay(BuildContext context) { + if (_overlayEntry != null) return; + + _overlayEntry = OverlayEntry( + builder: (context) => _PixelToastOverlay( + config: _config, + itemsNotifier: _itemsNotifier, + onDismiss: dismiss, + ), + ); + + Overlay.of(context).insert(_overlayEntry!); + } + + void _removeOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + } +} + +/// Global instance for easy access. +final pixelToastification = PixelToastification(); + +/// Wrapper widget that provides the overlay. +class PixelToastificationWrapper extends StatefulWidget { + final Widget child; + final PixelToastConfig? config; + + const PixelToastificationWrapper({ + super.key, + required this.child, + this.config, + }); + + @override + State createState() => _PixelToastificationWrapperState(); +} + +class _PixelToastificationWrapperState extends State { + @override + void initState() { + super.initState(); + if (widget.config != null) { + pixelToastification.configure(widget.config!); + } + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} + +/// Internal overlay widget for displaying toasts. +class _PixelToastOverlay extends StatelessWidget { + final PixelToastConfig config; + final ValueNotifier> itemsNotifier; + final void Function(PixelToastItem) onDismiss; + + const _PixelToastOverlay({ + required this.config, + required this.itemsNotifier, + required this.onDismiss, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder>( + valueListenable: itemsNotifier, + builder: (context, items, _) { + if (items.isEmpty) return const SizedBox.shrink(); + + return Positioned( + top: config.alignment.y <= 0 ? config.screenMargin.top : null, + bottom: config.alignment.y > 0 ? config.screenMargin.bottom : null, + left: config.alignment.x <= 0 ? config.screenMargin.left : null, + right: config.alignment.x >= 0 ? config.screenMargin.right : null, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: config.alignment.x < 0 + ? CrossAxisAlignment.start + : config.alignment.x > 0 + ? CrossAxisAlignment.end + : CrossAxisAlignment.center, + children: items.map((item) { + return Padding( + padding: EdgeInsets.only(bottom: config.toastMargin), + child: _AnimatedToastEntry( + key: ValueKey(item.id), + animationDuration: config.animationDuration, + alignment: config.alignment, + child: item.toast, + ), + ); + }).toList(), + ), + ); + }, + ); + } +} + +/// Animated wrapper for toast entry/exit. +class _AnimatedToastEntry extends StatefulWidget { + final Widget child; + final Duration animationDuration; + final Alignment alignment; + + const _AnimatedToastEntry({ + super.key, + required this.child, + required this.animationDuration, + required this.alignment, + }); + + @override + State<_AnimatedToastEntry> createState() => _AnimatedToastEntryState(); +} + +class _AnimatedToastEntryState extends State<_AnimatedToastEntry> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.animationDuration, + ); + + _fadeAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + + // Slide from the direction of alignment + final slideBegin = Offset( + widget.alignment.x > 0 ? 0.3 : widget.alignment.x < 0 ? -0.3 : 0, + widget.alignment.y < 0 ? -0.3 : 0.3, + ); + + _slideAnimation = Tween(begin: slideBegin, end: Offset.zero).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeOut), + ); + + _controller.forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: widget.child, + ), + ); + } +} + +/// Wrapper to handle hover pause functionality. +class _PixelToastWrapper extends StatefulWidget { + final String id; + final Widget child; + final bool pauseOnHover; + final ValueChanged onPauseChanged; + + const _PixelToastWrapper({ + required this.id, + required this.child, + required this.pauseOnHover, + required this.onPauseChanged, + }); + + @override + State<_PixelToastWrapper> createState() => _PixelToastWrapperState(); +} + +class _PixelToastWrapperState extends State<_PixelToastWrapper> { + @override + Widget build(BuildContext context) { + if (!widget.pauseOnHover) return widget.child; + + return MouseRegion( + onEnter: (_) => widget.onPauseChanged(true), + onExit: (_) => widget.onPauseChanged(false), + child: widget.child, + ); + } +} + +/// Extension methods for easy toast showing. +extension PixelToastificationContext on BuildContext { + /// Show an info toast. + PixelToastItem showPixelToastInfo({ + String? title, + String? description, + PixelToastStyle style = PixelToastStyle.stepped, + }) { + return pixelToastification.show( + context: this, + type: PixelToastType.info, + style: style, + title: title, + description: description, + ); + } + + /// Show a success toast. + PixelToastItem showPixelToastSuccess({ + String? title, + String? description, + PixelToastStyle style = PixelToastStyle.stepped, + }) { + return pixelToastification.show( + context: this, + type: PixelToastType.success, + style: style, + title: title, + description: description, + ); + } + + /// Show a warning toast. + PixelToastItem showPixelToastWarning({ + String? title, + String? description, + PixelToastStyle style = PixelToastStyle.stepped, + }) { + return pixelToastification.show( + context: this, + type: PixelToastType.warning, + style: style, + title: title, + description: description, + ); + } + + /// Show an error toast. + PixelToastItem showPixelToastError({ + String? title, + String? description, + PixelToastStyle style = PixelToastStyle.stepped, + }) { + return pixelToastification.show( + context: this, + type: PixelToastType.error, + style: style, + title: title, + description: description, + ); + } +}