-
Notifications
You must be signed in to change notification settings - Fork 23
feat: Add Dynamic Auto-Resizing Text Editor #286
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
base: main
Are you sure you want to change the base?
feat: Add Dynamic Auto-Resizing Text Editor #286
Conversation
Reviewer's GuideAdds a new TextFitEditor widget for dynamic, auto-resizing text editing and integrates it into the image editor's BottomActionMenu, enabling users to insert text that always fits the canvas and export it as an image. Sequence diagram for adding text via TextFitEditorsequenceDiagram
actor User
participant "BottomActionMenu"
participant "TextFitEditor"
participant "imgLoader"
User->>"BottomActionMenu": Tap 'Text' button
"BottomActionMenu"->>"TextFitEditor": Open editor with canvas size
User->>"TextFitEditor": Enter text, select options
"TextFitEditor"->>"BottomActionMenu": Return exported image bytes
"BottomActionMenu"->>"imgLoader": updateImage(bytes)
"BottomActionMenu"->>"imgLoader": saveFinalizedImageBytes(bytes)
"BottomActionMenu"->>User: Show updated canvas
Class diagram for new TextFitEditor widgetclassDiagram
class TextFitEditor {
+int width
+int height
+TextFitEditor(width, height)
}
class TextFitEditorState {
-TextEditingController _controller
-GlobalKey _repaintKey
-Color _textColor
-Color _backgroundColor
-TextAlign _align
-List<Color> _availableColors
+initState()
+build(context)
+_calculateCanvas(screenSize)
+_fitFontSize(text, maxW, maxH, align)
+_export(canvasSize)
}
TextFitEditor o-- TextFitEditorState
File-Level Changes
Assessment against linked issues
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes - here's some feedback:
- Dispose the TextEditingController in TextFitEditorState.dispose to avoid memory leaks.
- Wrap the editor UI in a SafeArea or handle keyboard insets so the text field and controls aren’t obscured by system UI or the keyboard.
- Consider extracting the font fitting binary‐search logic (_fitFontSize) into a separate utility to improve reusability and testability.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Dispose the TextEditingController in TextFitEditorState.dispose to avoid memory leaks.
- Wrap the editor UI in a SafeArea or handle keyboard insets so the text field and controls aren’t obscured by system UI or the keyboard.
- Consider extracting the font fitting binary‐search logic (_fitFontSize) into a separate utility to improve reusability and testability.
## Individual Comments
### Comment 1
<location> `lib/view/text_fit_editor.dart:79-83` </location>
<code_context>
+ }
+
+ Future<Uint8List?> _export(Size canvasSize) async {
+ final boundary = _repaintKey.currentContext?.findRenderObject()
+ as RenderRepaintBoundary?;
+ if (boundary == null) return null;
+ final pixelRatio = widget.width / canvasSize.width;
+ final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio);
+ final ui.ByteData? data =
+ await image.toByteData(format: ui.ImageByteFormat.png);
</code_context>
<issue_to_address>
**issue (bug_risk):** Check for zero or negative pixelRatio before calling toImage.
A guard clause to ensure pixelRatio is positive will prevent runtime errors if canvasSize.width is zero or negative.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Vishveshwara
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me
mariobehling
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please fix screenshot builds.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new dynamic text editor feature that allows users to enter text which automatically resizes to fit within a fixed canvas matching the e-paper display dimensions. The text is then exported as a PNG image, maintaining consistency with how other content elements are handled in the application.
Key Changes
- New
TextFitEditorwidget with auto-sizing text functionality using binary search algorithm - Integration of text editor into the bottom action menu alongside existing import/edit options
- Support for customizable text color, background color, and text alignment within available device color palette
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| lib/view/text_fit_editor.dart | New stateful widget implementing the text editor with auto-resize, color customization, alignment controls, and PNG export functionality |
| lib/view/image_editor.dart | Adds "Text" action button to BottomActionMenu for launching the text editor and handling the returned image bytes |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
|
|
||
| class TextFitEditorState extends State<TextFitEditor> { | ||
| final TextEditingController _controller = TextEditingController(); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The TextEditingController created on line 19 is never disposed, which causes a memory leak. Add a dispose() method to properly clean up the controller:
@override
void dispose() {
_controller.dispose();
super.dispose();
}| _backgroundColor = _availableColors.contains(Colors.white) | ||
| ? Colors.white | ||
| : _availableColors.first; | ||
| _textColor = _availableColors.firstWhere( | ||
| (c) => c != _backgroundColor, | ||
| orElse: () => Colors.black, |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If _availableColors contains only one color (e.g., only white), the initialization logic has an issue. Line 30-32 sets _backgroundColor to that color, then line 33-36 tries to find a different color but falls back to Colors.black, which might not be in _availableColors. This creates inconsistency where the text color may not be from the available color palette.
Consider handling this edge case by ensuring both colors come from the available palette, or adding validation to ensure at least two colors are available.
| _backgroundColor = _availableColors.contains(Colors.white) | |
| ? Colors.white | |
| : _availableColors.first; | |
| _textColor = _availableColors.firstWhere( | |
| (c) => c != _backgroundColor, | |
| orElse: () => Colors.black, | |
| assert(_availableColors.length >= 2, 'At least two colors are required in the palette.'); | |
| _backgroundColor = _availableColors.contains(Colors.white) | |
| ? Colors.white | |
| : _availableColors.first; | |
| _textColor = _availableColors.firstWhere( | |
| (c) => c != _backgroundColor, | |
| orElse: () => _availableColors.firstWhere((c) => c != _backgroundColor, orElse: () => _backgroundColor), |
| ); | ||
| } | ||
|
|
||
| Size _calculateCanvas(Size screenSize) { |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If widget.height is 0, this will cause a division by zero error. While this might be prevented by validation elsewhere, consider adding a safety check here or documenting the assumption that height is always positive.
| Size _calculateCanvas(Size screenSize) { | |
| Size _calculateCanvas(Size screenSize) { | |
| assert(widget.height != 0, 'widget.height must not be zero to avoid division by zero.'); |
| if (_textColor == _backgroundColor) { | ||
| _textColor = _availableColors.firstWhere( | ||
| (col) => col != _backgroundColor, | ||
| orElse: () => _textColor, |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the orElse callback, returning _textColor means that if all available colors are the same as _backgroundColor, the text color will remain equal to the background color, making the text invisible.
Consider using a fallback that ensures text remains visible, such as:
orElse: () => Colors.black != _backgroundColor ? Colors.black : Colors.white,| orElse: () => _textColor, | |
| orElse: () => Colors.black != _backgroundColor ? Colors.black : Colors.white, |
| final pixelRatio = widget.width / canvasSize.width; | ||
| final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio); | ||
| final ui.ByteData? data = | ||
| await image.toByteData(format: ui.ImageByteFormat.png); | ||
| return data?.buffer.asUint8List(); | ||
| } |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If canvasSize.width is 0, this will cause a division by zero error. While _calculateCanvas should prevent this in normal cases, consider adding a safety check to handle edge cases where the canvas size might be zero.
| final pixelRatio = widget.width / canvasSize.width; | |
| final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio); | |
| final ui.ByteData? data = | |
| await image.toByteData(format: ui.ImageByteFormat.png); | |
| return data?.buffer.asUint8List(); | |
| } | |
| if (canvasSize.width == 0) return null; | |
| final pixelRatio = widget.width / canvasSize.width; | |
| final ui.Image image = await boundary.toImage(pixelRatio: pixelRatio); | |
| final ui.ByteData? data = | |
| await image.toByteData(format: ui.ImageByteFormat.png); | |
| return data?.buffer.asUint8List(); |
| _backgroundColor = _availableColors.contains(Colors.white) | ||
| ? Colors.white | ||
| : _availableColors.first; | ||
| _textColor = _availableColors.firstWhere( | ||
| (c) => c != _backgroundColor, | ||
| orElse: () => Colors.black, | ||
| ); |
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If _availableColors is empty, accessing .first on line 32 will throw a StateError. Consider adding validation to ensure the colors list is not empty, or provide a default fallback.
| _backgroundColor = _availableColors.contains(Colors.white) | |
| ? Colors.white | |
| : _availableColors.first; | |
| _textColor = _availableColors.firstWhere( | |
| (c) => c != _backgroundColor, | |
| orElse: () => Colors.black, | |
| ); | |
| if (_availableColors.isEmpty) { | |
| _backgroundColor = Colors.white; | |
| _textColor = Colors.black; | |
| } else { | |
| _backgroundColor = _availableColors.contains(Colors.white) | |
| ? Colors.white | |
| : _availableColors.first; | |
| _textColor = _availableColors.firstWhere( | |
| (c) => c != _backgroundColor, | |
| orElse: () => Colors.black, | |
| ); | |
| } |
| @@ -0,0 +1,337 @@ | |||
| import 'dart:typed_data' as ui; | |||
Copilot
AI
Dec 1, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The import dart:typed_data on line 1 is aliased as ui, but line 4 also imports dart:ui with the same ui alias. This creates a naming conflict. Since the code uses ui.Image, ui.ByteData, and ui.ImageByteFormat (all from dart:ui), and Uint8List is used without the ui. prefix (from dart:typed_data), the first import should not be aliased.
Change line 1 to:
import 'dart:typed_data';| import 'dart:typed_data' as ui; | |
| import 'dart:typed_data'; |
Build StatusBuild successful. APKs to test: https://github.com/fossasia/magic-epaper-app/actions/runs/19825508746/artifacts/4725324638. Screenshots |







-1_display_selection.png?raw=true)
-2_sidebar.png?raw=true)
-3_ndef_screen.png?raw=true)
-4_filter_selection.png?raw=true)
-5_barcode_screen.png?raw=true)
-6_Barcode_added.png?raw=true)
-7_Templates_screen.png?raw=true)
Fixes: #284
Add a dynamic Text Editor where text always fits inside a fixed canvas by auto-resizing as content grows. On finalize, the text should export as an image similar to other elements.
Here is the demo video of this feature:
textfit_editor.mp4
Summary by Sourcery
Add a dynamic text editor that auto-resizes content to fit a fixed canvas and exports the final text as an image
New Features: