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/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/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 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/assets/landscape.jpeg b/example/assets/landscape.jpeg new file mode 100644 index 0000000..7d33ed1 Binary files /dev/null and b/example/assets/landscape.jpeg differ 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 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ 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 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ 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 0000000..6ed2d93 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ 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 0000000..4cd7b00 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ 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 0000000..fe73094 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ 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 0000000..502f463 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ 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 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ 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 0000000..e9f5fea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ 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 0000000..0467bf1 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ 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 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ 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 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ 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 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ 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..253911d --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,2271 @@ +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(), + ), + ); + } +} + +/// 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}); + + @override + Widget build(BuildContext context) { + final components = [ + ComponentItem('PixelToast', const PixelToastPage()), + ComponentItem('PixelBottomBar', const PixelBottomBarPage()), + ComponentItem('PixelSteppedPanel', const PixelSteppedPanelPage()), + 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(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', + style: GoogleFonts.pressStart2p( + fontSize: 14, + color: Colors.cyanAccent, + ), + ), + centerTitle: true, + ), + body: Stack( + children: [ + // 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: CustomPaint( + painter: _CircuitPatternPainter( + lineColor: const Color(0xFF1E2540), + dotColor: const Color(0xFF2A3555), + spacing: 50, + ), + ), + ), + // Subtle scanline overlay + Positioned.fill( + child: CustomPaint( + painter: _ScanlineOverlayPainter(), + ), + ), + // 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), + ), + ); + }, + ), + ], + ), + ); + } +} + +/// 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; + 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; + 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 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: 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: 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, + ), + ], + ), + 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, + ), + ], + ), + ], + ), + ); + } +} + +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, + ], + ), + 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: '_', + ), + ], + ), + ); + } +} + +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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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, + ), + ), + ), + ], + ), + 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, + ), + ], + ), + 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)), + ], + ), + ], + ), + ], + ), + ); + } +} + +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; + 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) { + 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), + ), + ), + 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), + ], + ), + ], + ), + ], + ), + ); + } +} + +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, + ), + ), + 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 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + 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: () {}, + ), + ], + ), + ], + ), + ); + } +} + +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), + ), + ], + ), + ); + } +} + +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; + bool _useBlur = false; + bool _useUnderline = 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: [ + // 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), + 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: 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: 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), + // 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.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: _useUnderline ? Colors.white : Colors.cyanAccent, + activeHighlightColor: const Color(0xFF3D4155), + borderColor: const Color(0xFF4A4E65), + indicatorStyle: _useUnderline + ? PixelBottomBarIndicatorStyle.underline + : PixelBottomBarIndicatorStyle.highlight, + activeIndicatorColor: Colors.redAccent, + ), + ), + ], + ), + ); + } + + String _getPageName(int index) { + switch (index) { + case 0: + return 'HOME'; + case 1: + return 'SEARCH'; + case 2: + return 'ALERTS'; + 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), + ], + ), + ); + } +} + +// ============================================================================= +// 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)), + ], + ), + ); + } +} + +/// 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/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 0000000..82b6f9d Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ 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 0000000..13b35eb Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ 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 0000000..0a3f5fa Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ 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 0000000..2f1632c Binary files /dev/null and b/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/example/macos/Runner/Base.lproj/MainMenu.xib b/example/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/example/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..f56858b --- /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: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + 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.2" + 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: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + url: "https://pub.dev" + source: hosted + version: "0.7.7" + 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..815dbf1 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,24 @@ +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 + assets: + - assets/landscape.jpeg 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..c0760c9 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'; @@ -16,4 +17,23 @@ 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'; +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'; +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/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/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) { 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_bottom_bar.dart b/lib/src/widgets/pixel_bottom_bar.dart new file mode 100644 index 0000000..3d810a3 --- /dev/null +++ b/lib/src/widgets/pixel_bottom_bar.dart @@ -0,0 +1,865 @@ +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. + 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 highlight behind the active icon. + final Color activeHighlightColor; + + /// 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; + + /// 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 (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; + + /// Blur intensity (sigma) when useBlur is true. + final double blurSigma; + + const PixelBottomBar({ + super.key, + required this.currentIndex, + required this.icons, + this.onTap, + this.gradient, + 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(0xFF9CA3AF), // Lighter gray for better contrast + this.borderColor = const Color(0xFF374151), // Neutral gray border + this.pixelSize = 3.0, + this.height = 56, + this.horizontalMargin = 16, + this.useSteppedCorners = true, + this.showActiveGlow = false, + this.iconSize = 24, + this.badges, + this.badgeColor = const Color(0xFFE53935), + this.badgeTextColor = Colors.white, + this.showActiveIndicator = 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, + }); + + @override + Widget build(BuildContext context) { + 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: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + // Active highlight with pixelated corners (only for highlight style) + if (isActive && indicatorStyle == PixelBottomBarIndicatorStyle.highlight) + 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: Icon( + icon, + size: iconSize, + color: activeIconColor, + ), + ), + ) + else + Padding( + padding: EdgeInsets.symmetric( + horizontal: pixelSize * 5, + vertical: pixelSize * 3, + ), + child: Icon( + icon, + size: iconSize, + color: isActive ? activeIconColor : 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 (for underline style or showActiveIndicator) + if ((indicatorStyle == PixelBottomBarIndicatorStyle.underline || showActiveIndicator) && isActive) + Container( + margin: const EdgeInsets.only(top: 4), + width: iconSize + pixelSize * 4, + 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; + + // 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 - 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, 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 - 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, p * (steps - 1 - i)); + path.lineTo(p * (i + 1), p * (steps - 1 - i)); + } + + 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, + ), + ), + ), + ); + } +} + +/// 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 - 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 * steps, 0); + + // Top edge + path.lineTo(w - p * steps, 0); + + // 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 * steps); + + // 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 * steps, h); + + // 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 * steps); + + // 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(); + + 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; + 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 + ..style = PaintingStyle.stroke + ..strokeWidth = p; + + 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); + } + } + + void _drawSteppedBackground( + Canvas canvas, + Size size, + Paint bgPaint, + Paint borderPaint, + double p, + ) { + final double w = size.width; + final double h = size.height; + + // Fixed number of steps for consistent pixel look + const int steps = 4; + + // 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 + path.moveTo(p * steps, 0); + + // Top edge + path.lineTo(w - p * steps, 0); + + // Top-right corner - staircase down (horizontal right, then vertical down) + for (int i = 0; i < steps; i++) { + // 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 - staircase left (vertical down, then horizontal left) + for (int i = 0; i < steps; i++) { + // 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 - staircase up (horizontal left, then vertical up) + for (int i = 0; i < steps; i++) { + // 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 - staircase right (vertical up, then horizontal right) + for (int i = 0; i < steps; i++) { + // 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(); + + // Draw background + canvas.drawPath(path, bgPaint); + + // Draw border + canvas.drawPath(path, borderPaint); + + // Draw highlight on top edge + final highlightPaint = Paint() + ..color = Colors.white.withValues(alpha: 0.08) + ..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 with badge support. +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; + + /// Optional badge count (0 or null = no badge). + final int? badgeCount; + + const PixelBottomBarItem({ + required this.icon, + this.label, + this.customIcon, + this.badgeCount, + }); +} + +/// 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 highlight behind the active item. + final Color activeHighlightColor; + + /// 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; + + /// 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; + + /// 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, + required this.items, + this.onTap, + this.gradient, + 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(0xFF9CA3AF), // Lighter gray for better contrast + this.borderColor = const Color(0xFF374151), // Neutral gray border + this.pixelSize = 3.0, + this.height = 56, + this.horizontalMargin = 16, + this.useSteppedCorners = true, + this.showLabels = false, + this.labelStyle, + this.iconSize = 24, + this.badgeColor = const Color(0xFFE53935), + this.badgeTextColor = Colors.white, + this.showActiveGlow = false, + this.useBlur = true, // Enable blur by default for translucent effect + this.blurSigma = 10.0, + this.indicatorStyle = PixelBottomBarIndicatorStyle.underline, + this.activeIndicatorColor = const Color(0xFFE53935), // Red underline + }); + + @override + Widget build(BuildContext context) { + 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; + + return GestureDetector( + onTap: () => onTap?.call(index), + behavior: HitTestBehavior.opaque, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + // Active highlight with pixelated corners (only for highlight style) + if (isActive && indicatorStyle == PixelBottomBarIndicatorStyle.highlight) + 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: isActive ? activeIconColor : 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, + ), + ), + ], + ), + // 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( + 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, + ), + ), + ); + } +} diff --git a/lib/src/widgets/pixel_button.dart b/lib/src/widgets/pixel_button.dart index 0ab32b1..30b2920 100644 --- a/lib/src/widgets/pixel_button.dart +++ b/lib/src/widgets/pixel_button.dart @@ -1,12 +1,16 @@ 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 +import '../core/pixel_theme.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 { final VoidCallback? onPressed; final bool enabled; @@ -17,16 +21,23 @@ class PixelButton extends StatefulWidget { final Color hoverColor; final Color pressedColor; final Color disabledColor; - final Color focusColor; - final Color outlineColor; - final double pixelEdgeThickness; + // 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; - final String? clickSoundAsset; // e.g., 'assets/sounds/click.wav' - final String? hoverSoundAsset; // e.g., 'assets/sounds/hover.wav' + /// 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, @@ -34,119 +45,164 @@ class PixelButton extends StatefulWidget { 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), + this.pressedColor = const Color(0xFF2A2D3E), + this.disabledColor = const Color(0xFF555555), + this.borderDark = const Color(0xFF000000), + this.pixelSize = 2.0, + this.enableGlowAnimation = false, + this.glowColor, + this.enableScanlineAnimation = false, + this.useSteppedCorners = true, + this.style = PixelButtonStyle.rectangular, + this.cornerSteps = 4, }) : super(key: key); @override - _PixelButtonState createState() => _PixelButtonState(); + State createState() => _PixelButtonState(); } class _PixelButtonState extends State with SingleTickerProviderStateMixin { bool _hovering = false; bool _pressed = false; - bool _focused = false; - late final AnimationController _glowController; - late final Animation _glowAnimation; + AnimationController? _glowController; + Animation? _glowAnimation; + + Color get _backgroundColor { + if (!widget.enabled) return widget.disabledColor; + if (_pressed) return widget.pressedColor; + if (_hovering) return widget.hoverColor; + return widget.color; + } - late final AudioPlayer _audioPlayer; + 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(); - - _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); + _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(); - _audioPlayer.dispose(); + _glowController?.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 ?? + // 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', // 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, + 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) { + 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), + 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,48 +210,125 @@ 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: Stack( - alignment: Alignment.center, - children: [ - Text(widget.label, style: textStyle), - if (widget.enableScanlineAnimation) _buildScanlineOverlay(Size.infinite), - ], - ), - ), + child: buttonContent, ), ); } } +/// Draws pixel-perfect stepped corner borders - 2 borders only +class _PixelBorderPainter extends CustomPainter { + final Color backgroundColor; + final Color borderDark; + final Color borderLight; + final Color borderShadow; + final double pixelSize; + final bool useSteppedCorners; + + _PixelBorderPainter({ + required this.backgroundColor, + required this.borderDark, + required this.borderLight, + required this.borderShadow, + required this.pixelSize, + this.useSteppedCorners = 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, + ); + } + + 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 + + // === 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 _PixelBorderPainter oldDelegate) { + return oldDelegate.backgroundColor != backgroundColor || + oldDelegate.borderDark != borderDark || + 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.white.withOpacity(0.1) + ..color = Colors.black.withOpacity(0.15) ..strokeWidth = 1; - // Draw horizontal scanlines spaced by 4 pixels - for (double y = 0; y < size.height; y += 4) { + for (double y = 0; y < size.height; y += 3) { canvas.drawLine(Offset(0, y), Offset(size.width, y), paint); } } @@ -203,3 +336,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_notification_card.dart b/lib/src/widgets/pixel_notification_card.dart new file mode 100644 index 0000000..fec4733 --- /dev/null +++ b/lib/src/widgets/pixel_notification_card.dart @@ -0,0 +1,500 @@ +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. +/// +/// 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 +/// - 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; + + /// 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, + 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, + this.useSteppedBorder = false, + this.pixelSize = 2.0, + }); + + @override + State createState() => _PixelNotificationCardState(); +} + +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 theme = PixelTheme.maybeOf(context); + final themeFont = theme?.fontFamily ?? 'PressStart2P'; + + final defaultTitleStyle = widget.titleStyle ?? + TextStyle( + fontFamily: themeFont, + fontSize: 11, + color: widget.titleColor, + letterSpacing: 0.5, + height: 1.2, + ); + + final defaultActionStyle = widget.actionStyle ?? + TextStyle( + fontFamily: themeFont, + fontSize: 8, + color: widget.actionColor, + letterSpacing: 0.5, + 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) + : 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: 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: contentPadding, + 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 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; + 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_panel.dart b/lib/src/widgets/pixel_panel.dart index c106c38..42d7fd1 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,26 @@ class PixelPanel extends StatelessWidget { 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}) - : super(key: key); + const _NoiseOverlay({ + Key? key, + required this.intensity, + required this.pixelSize, + this.seed, + this.noiseColor = Colors.white, + }) : super(key: key); @override Widget build(BuildContext context) { return CustomPaint( - painter: _NoisePainter(intensity: intensity, pixelSize: pixelSize), + painter: _NoisePainter( + intensity: intensity, + pixelSize: pixelSize, + seed: seed, + noiseColor: noiseColor, + ), ); } } @@ -152,14 +212,21 @@ class _NoiseOverlay extends StatelessWidget { class _NoisePainter extends CustomPainter { final double intensity; final double pixelSize; - final Random _random = Random(); + final int? seed; + final Color noiseColor; + late final Random _random; - _NoisePainter({required this.intensity, required this.pixelSize}); + _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) { @@ -172,23 +239,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 +272,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 +286,261 @@ 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; + + /// Color of the noise effect (default white, use Colors.black for dark noise) + final Color noiseColor; + + 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, + this.noiseColor = Colors.white, + }); + + @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, + noiseColor: widget.noiseColor, + ), + ), + ], + ), + ); + }, + ); + } +} -// usage +// usage // PixelPanel( // width: 320, diff --git a/lib/src/widgets/pixel_progress_bar.dart b/lib/src/widgets/pixel_progress_bar.dart index 1c2aa86..aeb2c14 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, @@ -252,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/lib/src/widgets/pixel_stepped_panel.dart b/lib/src/widgets/pixel_stepped_panel.dart new file mode 100644 index 0000000..784f6ea --- /dev/null +++ b/lib/src/widgets/pixel_stepped_panel.dart @@ -0,0 +1,667 @@ +import 'package:flutter/material.dart'; + +/// 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, + 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; + } +} + +/// 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, + 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; + } +} + +/// 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, + 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; + } +} diff --git a/lib/src/widgets/pixel_text.dart b/lib/src/widgets/pixel_text.dart index ffae963..3e6bd14 100644 --- a/lib/src/widgets/pixel_text.dart +++ b/lib/src/widgets/pixel_text.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; +import '../core/pixel_theme.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 +29,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 +47,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); @@ -52,9 +63,17 @@ class _PixelTextState extends State with SingleTickerProviderStateMix Offset _glitchOffset1 = Offset.zero; Offset _glitchOffset2 = Offset.zero; - late Timer? _effectTimer; + 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 +81,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,12 +170,19 @@ class _PixelTextState extends State with SingleTickerProviderStateMix @override void dispose() { _effectTimer?.cancel(); + _typewriterTimer?.cancel(); + _cursorBlinkTimer?.cancel(); super.dispose(); } @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; @@ -149,6 +239,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/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, ), ), 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, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index a949e46..ed277bc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,23 +1,24 @@ 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 environment: - sdk: ^3.9.0 + sdk: ^3.0.0 flutter: ">=1.17.0" dependencies: flutter: sdk: flutter - audioplayers: ^6.5.0 + 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: 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..926e818 100644 --- a/test/pixelify_flutter_test.dart +++ b/test/pixelify_flutter_test.dart @@ -1,12 +1,857 @@ +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, + borderDark: 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', + pixelSize: 3.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('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( + 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('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( + 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); + } + }); + }); +}