Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PAINTROID-737: Add Pipette Tool #78

Open
wants to merge 47 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
da5297f
Add Color Picker
bhav-khurana Dec 28, 2023
5aaaaaa
Merge branch 'Catrobat:develop' into color_picker
bhav-khurana Jan 16, 2024
a6cafb0
Implement color picker
bhav-khurana Jan 16, 2024
372b63a
Merge branch 'color_picker' of https://github.com/bhav-khurana/Paintr…
bhav-khurana Jan 16, 2024
29f0187
Fix issues
bhav-khurana Jan 16, 2024
432fbf8
Merge with develop
bhav-khurana Jan 22, 2024
492d8c1
Fix issues
bhav-khurana Jan 22, 2024
98fe1e7
Merge branch 'develop' into color_picker
bhav-khurana Jan 22, 2024
eca8dcd
Resolve pubspec errors
bhav-khurana Jan 22, 2024
0a14b44
address requested changes
bhav-khurana Feb 18, 2024
de3ce87
Merge branch 'develop' into color_picker
bhav-khurana Feb 18, 2024
c90bdd6
fix minor issues
bhav-khurana Feb 18, 2024
34f9ba2
Merge branch 'color_picker' of https://github.com/bhav-khurana/Paintr…
bhav-khurana Feb 18, 2024
6d2e803
refactoring and minor fixes
bhav-khurana Feb 21, 2024
c05f08f
use checkerboardimg from component_library
bhav-khurana Feb 21, 2024
4596426
fix minor issues
bhav-khurana Feb 21, 2024
0c15732
revert app_localizations file
bhav-khurana Feb 21, 2024
f7526f5
add dynamic color change, separate colorpicker package
bhav-khurana Feb 23, 2024
37eac01
use riverpod generator
bhav-khurana Feb 27, 2024
10c8e56
fix lint errors
bhav-khurana Feb 27, 2024
e9d9f76
add tests
bhav-khurana Feb 28, 2024
1272ceb
fix riverpod issues, remove redundancies
bhav-khurana Feb 28, 2024
34235dc
remove comments, capitalize text
bhav-khurana Mar 5, 2024
7a430c2
convert stateful to stateless, rename widgets
bhav-khurana Mar 6, 2024
8a347b9
address changes, remove slider position state provider
bhav-khurana Mar 10, 2024
749274a
fix minor issues
bhav-khurana Mar 10, 2024
e8a6e03
debug test error
bhav-khurana Mar 11, 2024
06972ec
Revert "debug test error"
bhav-khurana Mar 29, 2024
f17b9fd
debug test error
bhav-khurana Mar 11, 2024
9b1ba2e
fix test error
bhav-khurana Apr 6, 2024
48bd9de
resolve conflicts
bhav-khurana May 18, 2024
34a6c9f
Merge branch 'develop' into color_picker
bhav-khurana May 18, 2024
c47fef8
sort imports, remove duplicates
bhav-khurana May 18, 2024
096ac43
fix lint errors
bhav-khurana May 18, 2024
fb23944
use paintroid theme colors
bhav-khurana May 29, 2024
2aabe54
fix minor issues
bhav-khurana Jun 1, 2024
f0d97d5
fix lint errors
bhav-khurana Jun 1, 2024
22c993d
fix background color
bhav-khurana Jun 1, 2024
e3353b5
fix test error, minor refactoring
bhav-khurana Jun 4, 2024
5bab29a
add pipette tool
bhav-khurana Jun 6, 2024
5550010
Merge remote-tracking branch 'origin/develop' into PAINTROID-737
bhav-khurana Jun 7, 2024
5215c12
fix dependencies
bhav-khurana Jun 7, 2024
3e64b1f
fix test error
bhav-khurana Jun 9, 2024
3677ed4
update makefile to run pub get in packages
bhav-khurana Jun 9, 2024
d6d8c92
add basic color match test
bhav-khurana Jun 23, 2024
05aa422
Merge remote-tracking branch 'origin/develop' into PAINTROID-737
bhav-khurana Aug 19, 2024
6fb279e
fix errors
bhav-khurana Aug 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
144 changes: 144 additions & 0 deletions packages/colorpicker/lib/pages/pipette_tool_page.dart
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opening a new page for pipette tool is overkill. As mentioned in my previous comment the kotlin version of Pocket Paint doesnt open a new page. You can approach this tool the easiet way:

  1. onTap -> get color of the pixel under the point
  2. set paint color to that color using paint provider

This simplyfies the user experience and matches the original implementation of the tool and removes alot of unnessary code.

const PipetteToolPage({
super.key,
required this.snapshot,
});

final ui.Image? snapshot;

@override
ConsumerState<ConsumerStatefulWidget> createState() =>
_PipetteToolPageState();
}

class _PipetteToolPageState extends ConsumerState<PipetteToolPage> {
GlobalKey imageKey = GlobalKey();
ui.Image? displayImage;

Future<void> _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<ui.Image> completer = Completer<ui.Image>();

void imageListener(ImageInfo info, bool synchronousCall) {
completer.complete(info.image);
imageStream.removeListener(ImageStreamListener(imageListener));
}

imageStream.addListener(ImageStreamListener(imageListener));
displayImage = await completer.future;

setState(() {});
}

Future<void> _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;
}
}
45 changes: 45 additions & 0 deletions packages/colorpicker/lib/src/components/pipette_tool_button.dart
Original file line number Diff line number Diff line change
@@ -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(),
],
),
);
}
}
39 changes: 39 additions & 0 deletions packages/colorpicker/lib/src/components/top_bar.dart
Original file line number Diff line number Diff line change
@@ -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),
],
);
}
}
58 changes: 58 additions & 0 deletions packages/colorpicker/test/widget/pipette_tool_test.dart
Original file line number Diff line number Diff line change
@@ -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<ui.Image> 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);
});
}