diff --git a/packages/colorpicker/lib/pages/pipette_tool_page.dart b/packages/colorpicker/lib/pages/pipette_tool_page.dart new file mode 100644 index 00000000..239fa207 --- /dev/null +++ b/packages/colorpicker/lib/pages/pipette_tool_page.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:colorpicker/src/components/top_bar.dart'; +import 'package:colorpicker/src/state/color_picker_state_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class PipetteToolPage extends ConsumerStatefulWidget { + const PipetteToolPage({ + super.key, + required this.snapshot, + }); + + final ui.Image? snapshot; + + @override + ConsumerState createState() => + _PipetteToolPageState(); +} + +class _PipetteToolPageState extends ConsumerState { + GlobalKey imageKey = GlobalKey(); + ui.Image? displayImage; + + Future _loadImage() async { + final ByteData? bytedata = + await widget.snapshot!.toByteData(format: ui.ImageByteFormat.png); + if (bytedata == null) { + return Future.error('An error occurred while loading the snapshot'); + } + final Uint8List headedIntList = Uint8List.view(bytedata.buffer); + ImageProvider? image = MemoryImage(headedIntList); + final ImageStream imageStream = image.resolve(const ImageConfiguration()); + final Completer completer = Completer(); + + void imageListener(ImageInfo info, bool synchronousCall) { + completer.complete(info.image); + imageStream.removeListener(ImageStreamListener(imageListener)); + } + + imageStream.addListener(ImageStreamListener(imageListener)); + displayImage = await completer.future; + + setState(() {}); + } + + Future _updateColor(TapUpDetails details) async { + RenderBox box = imageKey.currentContext!.findRenderObject() as RenderBox; + Offset localPosition = box.globalToLocal(details.globalPosition); + + double xRatio = localPosition.dx / box.size.width; + double yRatio = localPosition.dy / box.size.height; + + int x = (xRatio * displayImage!.width).toInt(); + int y = (yRatio * displayImage!.height).toInt(); + + ByteData? byteData = + await displayImage!.toByteData(format: ui.ImageByteFormat.rawRgba); + if (byteData == null) return; + + int offset = (y * displayImage!.width + x) * 4; + + int red = byteData.getUint8(offset); + int green = byteData.getUint8(offset + 1); + int blue = byteData.getUint8(offset + 2); + int alpha = byteData.getUint8(offset + 3); + + Color color = Color.fromARGB(alpha, red, green, blue); + + final colorData = ref.read(colorPickerStateProvider.notifier); + colorData.updateColor(color.withOpacity(1)); + colorData.updateOpacity(color.opacity); + } + + @override + void initState() { + super.initState(); + _loadImage(); + } + + @override + Widget build(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + final colorData = ref.watch(colorPickerStateProvider); + return Scaffold( + backgroundColor: Colors.white, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(60.0), + child: TopBar( + color: colorData.currentColor != null + ? colorData.currentColor!.withOpacity(colorData.currentOpacity) + : Colors.transparent), + ), + body: Center( + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + 'packages/colorpicker/assets/img/checkerboard.png', + repeat: ImageRepeat.repeat, + cacheHeight: 16, + cacheWidth: 16, + filterQuality: FilterQuality.none, + ), + ), + if (displayImage != null) + GestureDetector( + onTapUp: _updateColor, + child: SizedBox( + height: screenSize.height, + width: screenSize.width, + child: CustomPaint( + key: imageKey, + painter: ImagePainter(displayImage!), + ), + ), + ), + ], + ), + ), + ); + } +} + +class ImagePainter extends CustomPainter { + final ui.Image image; + + ImagePainter(this.image); + + @override + void paint(Canvas canvas, Size size) { + final Rect srcRect = + Rect.fromLTWH(0, 0, image.width.toDouble(), image.height.toDouble()); + final Rect dstRect = Rect.fromLTWH(0, 0, size.width, size.height); + canvas.drawImageRect(image, srcRect, dstRect, Paint()); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return false; + } +} diff --git a/packages/colorpicker/lib/src/components/pipette_tool_button.dart b/packages/colorpicker/lib/src/components/pipette_tool_button.dart new file mode 100644 index 00000000..81151d02 --- /dev/null +++ b/packages/colorpicker/lib/src/components/pipette_tool_button.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class PipetteToolButton extends StatelessWidget { + const PipetteToolButton({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 50.0, + width: 148.0, + padding: const EdgeInsets.symmetric(horizontal: 10.0, vertical: 4.0), + decoration: BoxDecoration( + color: const Color.fromARGB(255, 204, 204, 204), + borderRadius: const BorderRadius.all(Radius.circular(6.0)), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + offset: const Offset(0, 1), + blurRadius: 1.0, + ), + ], + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Spacer(), + Icon( + Icons.auto_fix_normal, + color: Colors.black, + size: 20, + ), + Spacer(), + Text( + 'PIPETTE', + style: TextStyle( + color: Colors.black, + fontWeight: FontWeight.w500, + ), + ), + Spacer(), + ], + ), + ); + } +} diff --git a/packages/colorpicker/lib/src/components/top_bar.dart b/packages/colorpicker/lib/src/components/top_bar.dart new file mode 100644 index 00000000..b73568bf --- /dev/null +++ b/packages/colorpicker/lib/src/components/top_bar.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class TopBar extends ConsumerWidget { + const TopBar({ + super.key, + required this.color, + }); + + final Color color; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AppBar( + title: Container( + height: 36.0, + width: 36.0, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(2.0), + border: Border.all( + color: Colors.white, + width: 0.4, + ), + ), + ), + centerTitle: true, + actions: [ + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + Navigator.pop(context); + }, + ), + const SizedBox(width: 10.0), + ], + ); + } +} diff --git a/packages/colorpicker/test/widget/pipette_tool_test.dart b/packages/colorpicker/test/widget/pipette_tool_test.dart new file mode 100644 index 00000000..f1d22e6e --- /dev/null +++ b/packages/colorpicker/test/widget/pipette_tool_test.dart @@ -0,0 +1,58 @@ +import 'dart:ui' as ui; +import 'package:colorpicker/pages/pipette_tool_page.dart'; +import 'package:colorpicker/src/state/color_picker_state_data.dart'; +import 'package:colorpicker/src/state/color_picker_state_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; + +// Helper function to create a sample image for testing +Future createTestImage() async { + final pictureRecorder = ui.PictureRecorder(); + final canvas = Canvas(pictureRecorder); + final paint = Paint()..color = const Color(0xFFFF0000); + canvas.drawRect(const Rect.fromLTWH(0, 0, 100, 100), paint); + final picture = pictureRecorder.endRecording(); + return picture.toImage(100, 100); +} + +void main() { + testWidgets('PipetteToolPage displays image and updates color on tap', + (WidgetTester tester) async { + // Create a sample image for testing + final image = await createTestImage(); + + // Override the provider for testing + final container = ProviderContainer(overrides: [ + colorPickerStateProvider.overrideWith( + () => ColorPickerState() + ..state = const ColorPickerStateData( + currentColor: Colors.red, currentOpacity: 1.0), + ), + ]); + + // Build the widget + await tester.pumpWidget( + UncontrolledProviderScope( + container: container, + child: MaterialApp( + home: PipetteToolPage(snapshot: image), + ), + ), + ); + + // Wait for the image to load + await tester.pumpAndSettle(); + + // Verify that the image is displayed + expect(find.byType(CustomPaint), findsOneWidget); + + // Tap on the image to update the color + await tester.tap(find.byType(CustomPaint)); + await tester.pumpAndSettle(); + + // Verify that the color update was called + final colorState = container.read(colorPickerStateProvider); + expect(colorState.currentColor, isNotNull); + }); +}