Skip to content

Commit 3b4447b

Browse files
FeodorFitsnerCopilotndonkoHenri
authored
v1: Canvas.capture(), canvas draw images, GestureDetector right-click pan updates. (#5444)
* Draw image on Canvas - a working draft * New Canvas methods: `capture()`, `get_capture()`, `clear_capture()` Other canvas changes: - canvas drawing is clipped - canvas size is stretched to maximum if `expand` is set * New `GestureDetector` events: `on_right_pan_start`, `on_right_pan_update`, `on_right_pan_end` * Added `PageMediaData.device_pixel_ratio` property * Canvas.capture() fixed for DPR * BaseControl. _trigger_event, context moved to a separate module * Scale canvas to account for DPR when taking capture * Preserve capture size * Fix: page.page should return Page. * KeyboardListener control * Allow FilePicker.save_file on web * Update right-pan event * All canvas shapes can be dashed Close #1791 * Renamed example * Add KeyboardListener docs and improve canvas docstrings Added documentation files for KeyboardListener and related key event types. Updated mkdocs navigation to include new docs. Enhanced docstrings for canvas capture methods and removed redundant event handler argument descriptions in GestureDetector and KeyboardListener. * Add keyboard listener example and improve docs Added a new example 'detect_keys.py' demonstrating keyboard event handling. Updated documentation to include the example and enhanced event class docstrings for clarity in 'keyboard_listener.py'. * Enhance PageView with detailed docstrings and base class Added comprehensive docstrings to PageView, PageMediaData, and PageResizeEvent for improved clarity and developer guidance. Changed PageView to inherit from ScrollableControl and AdaptiveControl, and updated the import accordingly. Expanded property documentation and clarified usage of key attributes. * Fix tests: Remove ScrollableControl from PageView inheritance PageView no longer inherits from ScrollableControl, and related imports were removed. Page now calls PageView's __post_init__ instead of AdaptiveControl's. This refactoring clarifies class responsibilities and streamlines control hierarchy. * Refactor integration tests and add canvas test coverage Refactored Python integration tests to use a shared flet_app fixture via conftest.py, reducing code duplication. Added comprehensive canvas control tests with golden image comparisons and supporting assets. Updated canvas text rendering to use 'value' instead of 'text'. Improved tester service and Flutter tester API for more flexible duration handling. Enhanced FastAPI web server integration and updated documentation for new environment variables. * Lower pytest log level to INFO in AppVeyor config Changed the pytest log_cli_level from DEBUG to INFO in the AppVeyor configuration to reduce log verbosity during integration tests. * Update CI config and test finders only Commented out most build and test jobs in .appveyor.yml, limiting test execution to test_finders.py. Increased the test app window width from 400 to 800 in finders.py. Added a pump delay in test_find_by_text to improve test reliability. * Run all integration tests * Update test runner and increase test delay in test_canvas.py The AppVeyor configuration now runs only the test_canvas.py integration test instead of the entire directory. Additionally, the delay in the test_draw_url_image test was increased from 3000ms to 5000ms, likely to improve test reliability. * Run all tests, again * Refactor screenshot test timing in canvas tests Replaces the 'delay' parameter with 'pump_times' and 'pump_duration' in canvas integration tests and the FletTestApp helper. This allows for more precise control over test timing and screenshot capture during automated UI tests. * Add canvas capture tests and enable full CI matrix Introduces integration tests for the Canvas control's capture functionality, including new golden images for macOS. Fixes a bug in Canvas.clear_capture_async() to call the correct method, and moves screenshot pixel ratio and similarity threshold to FletTestApp instance variables. Also enables all previously commented-out jobs in the AppVeyor CI matrix. * Refactor control initialization and property handling Introduces a new build() method for control child initialization, refactors property handling using _internals for host_expanded, host_positioned, and skip_properties, and updates Dart logic to use these internals for sizing and positioning. Updates tests and removes obsolete _skip_inherited_notifier logic. This change improves extensibility and consistency in control property management. * Add pixel_ratio support to Canvas capture methods Introduces an optional pixel_ratio argument to Canvas.capture and Canvas.capture_async, allowing control over the resolution of captured images. Updates the Dart backend to use the provided pixel ratio and adjusts integration tests and Python examples to use the new build method instead of init. * Apply suggestion from @Copilot Co-authored-by: Copilot <[email protected]> * Update __init__.py * improve some docstrings * `on_media_change` event receives new media data * Canvas example: Add image saving and optimize canvas capture Introduced a 'Save image' button that allows users to save the canvas as a PNG file. Optimized the brush drawing by batching shape captures every 30 shapes, clearing the canvas after each capture, and improved state management using a dataclass. Also updated the canvas background to white and added a border for better visibility. * Refactor canvas shape constructors to use keyword args Updated all canvas shape and path constructors in example files to use explicit keyword arguments for clarity and consistency. Added documentation for the Image control and updated the canvas documentation index to include gradients and the new Image page. * Add platform checks and validation to FilePicker methods Introduced platform-specific validation in FilePicker: 'get_directory_path' now raises NotImplementedError on web, and 'save_file' raises ValueError if required arguments are missing on web, iOS, or Android. Updated docstrings to clarify argument requirements and platform behavior. * Remove base64 decoding fallback in loadCanvasImage Eliminated the code path that attempted to decode the image source as base64 when 'src' is provided. Now, an exception is thrown if neither 'src' nor 'src_bytes' is present, clarifying the expected input and simplifying image loading logic. * use FletExceptions and remove outdated brush.gif --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: ndonkoHenri <[email protected]>
1 parent 81a74b6 commit 3b4447b

File tree

106 files changed

+2264
-560
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

106 files changed

+2264
-560
lines changed

.appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ for:
173173

174174
test_script:
175175
- cd sdk/python
176-
- uv run pytest -s -o log_cli=true -o log_cli_level=DEBUG packages/flet/integration_tests/
176+
- uv run pytest -s -o log_cli=true -o log_cli_level=INFO packages/flet/integration_tests
177177

178178
on_failure:
179179
- find packages/flet/integration_tests -type f -name '*_actual.png' -exec appveyor PushArtifact {} \;

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@
2727
.python-version
2828
vendor/
2929
/client/android/app/.cxx
30+
client/devtools_options.yaml

client/integration_test/app_test.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,23 @@ void main() {
2222
if (fletTestAppUrl != "") {
2323
args.add(fletTestAppUrl);
2424
}
25+
26+
const fletTestPidFile = String.fromEnvironment("FLET_TEST_PID_FILE_PATH");
27+
if (fletTestPidFile != "") {
28+
args.add(fletTestPidFile);
29+
}
30+
31+
const fletTestAssetsDir = String.fromEnvironment("FLET_TEST_ASSETS_DIR");
32+
if (fletTestAssetsDir != "") {
33+
args.add(fletTestAssetsDir);
34+
}
35+
2536
app.main(args);
2637

2738
await Future.delayed(const Duration(milliseconds: 500));
2839
await app.tester?.pump(duration: const Duration(seconds: 1));
29-
await app.tester?.pumpAndSettle(const Duration(milliseconds: 100));
40+
await app.tester
41+
?.pumpAndSettle(duration: const Duration(milliseconds: 100));
3042
await app.tester?.waitForTeardown();
3143
});
3244
});

client/integration_test/flutter_tester.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class FlutterWidgetTester implements Tester {
1818
FlutterWidgetTester(this._tester, this._binding);
1919

2020
@override
21-
Future<void> pumpAndSettle(
22-
[Duration duration = const Duration(milliseconds: 100)]) async {
21+
Future<void> pumpAndSettle({Duration? duration}) async {
2322
await lock.acquire();
2423
try {
25-
await _tester.pumpAndSettle(duration);
24+
await _tester
25+
.pumpAndSettle(duration ?? const Duration(milliseconds: 100));
2626
} finally {
2727
lock.release();
2828
}

client/lib/main.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ void main([List<String>? args]) async {
6464
//debugPrint("Uri.base: ${Uri.base}");
6565

6666
if (kDebugMode) {
67-
pageUrl = "tcp://localhost:8550";
67+
pageUrl = "http://localhost:8550";
6868
}
6969

7070
if (kIsWeb) {

packages/flet/lib/src/controls/base_controls.dart

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -81,21 +81,15 @@ Widget _directionality(Widget widget, Control control) {
8181
}
8282

8383
Widget _expandable(Widget widget, Control control) {
84-
var parent = control.parent;
85-
if (parent != null && ["View", "Column", "Row"].contains(parent.type)) {
86-
int? expand = control.properties.containsKey("expand")
87-
? control.get("expand") == true
88-
? 1
89-
: control.get("expand") == false
90-
? 0
91-
: control.getInt("expand")
92-
: null;
93-
var expandLoose = control.getBool("expand_loose", false)!;
94-
return expand != null
95-
? (expandLoose == true)
96-
? Flexible(flex: expand, child: widget)
97-
: Expanded(flex: expand, child: widget)
98-
: widget;
84+
int? expand = control.get("expand") == true
85+
? 1
86+
: control.get("expand") == false
87+
? 0
88+
: control.getInt("expand");
89+
if (expand != null && control.parent?.internals?["host_expanded"] == true) {
90+
return (control.getBool("expand_loose") == true)
91+
? Flexible(flex: expand, child: widget)
92+
: Expanded(flex: expand, child: widget);
9993
}
10094
return widget;
10195
}
@@ -206,8 +200,15 @@ Widget _positionedControl(
206200
var right = control.getDouble("right", null);
207201
var bottom = control.getDouble("bottom", null);
208202

203+
var errorControl = ErrorControl("Error displaying ${control.type}",
204+
description:
205+
"Control can be positioned absolutely with \"left\", \"top\", \"right\" and \"bottom\" properties inside Stack control only and page.overlay.");
206+
209207
var animation = control.getAnimation("animate_position");
210208
if (animation != null) {
209+
if (control.parent?.internals?["host_positioned"] != true) {
210+
return errorControl;
211+
}
211212
if (left == null && top == null && right == null && bottom == null) {
212213
left = 0;
213214
top = 0;
@@ -228,11 +229,8 @@ Widget _positionedControl(
228229
child: widget,
229230
);
230231
} else if (left != null || top != null || right != null || bottom != null) {
231-
var parent = control.parent;
232-
if (!["Stack", "Page", "Overlay"].contains(parent?.type)) {
233-
return ErrorControl("Error displaying ${control.type}",
234-
description:
235-
"Control can be positioned absolutely with \"left\", \"top\", \"right\" and \"bottom\" properties inside Stack control only.");
232+
if (control.parent?.internals?["host_positioned"] != true) {
233+
return errorControl;
236234
}
237235
return Positioned(
238236
left: left,
@@ -246,10 +244,16 @@ Widget _positionedControl(
246244
}
247245

248246
Widget _sizedControl(Widget widget, Control control) {
247+
final skipProps = control.internals?["skip_properties"] as List?;
248+
if (skipProps?.contains("width") == true ||
249+
skipProps?.contains("height") == true) {
250+
return widget;
251+
}
252+
249253
var width = control.getDouble("width");
250254
var height = control.getDouble("height");
251-
if ((width != null || height != null) &&
252-
!["container", "image"].contains(control.type)) {
255+
256+
if ((width != null || height != null)) {
253257
widget = ConstrainedBox(
254258
constraints: BoxConstraints.tightFor(width: width, height: height),
255259
child: widget,

0 commit comments

Comments
 (0)