Skip to content

v1: Canvas.capture(), canvas draw images, GestureDetector right-click pan updates. #5444

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

Merged
merged 41 commits into from
Aug 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
86b7e90
Draw image on Canvas - a working draft
FeodorFitsner Jul 11, 2025
720f056
New Canvas methods: `capture()`, `get_capture()`, `clear_capture()`
FeodorFitsner Jul 14, 2025
cb2424f
New `GestureDetector` events: `on_right_pan_start`, `on_right_pan_upd…
FeodorFitsner Jul 14, 2025
15af0f9
Added `PageMediaData.device_pixel_ratio` property
FeodorFitsner Jul 15, 2025
effb0c2
Canvas.capture() fixed for DPR
FeodorFitsner Jul 17, 2025
f482769
BaseControl. _trigger_event, context moved to a separate module
FeodorFitsner Jul 17, 2025
236c4fe
Scale canvas to account for DPR when taking capture
FeodorFitsner Jul 18, 2025
5d55afb
Preserve capture size
FeodorFitsner Jul 20, 2025
e4837dd
Fix: page.page should return Page.
FeodorFitsner Jul 20, 2025
d9e7e8b
KeyboardListener control
FeodorFitsner Jul 21, 2025
1b63fa7
Allow FilePicker.save_file on web
FeodorFitsner Jul 21, 2025
7748440
Merge branch 'main' into feodor/v1-canvas-recording
FeodorFitsner Aug 2, 2025
be9d3c4
Merge branch 'main' into feodor/v1-canvas-recording
FeodorFitsner Aug 5, 2025
1041f48
Update right-pan event
FeodorFitsner Aug 5, 2025
b18b3d3
All canvas shapes can be dashed
FeodorFitsner Aug 5, 2025
3642e02
Renamed example
FeodorFitsner Aug 6, 2025
8663e3b
Add KeyboardListener docs and improve canvas docstrings
FeodorFitsner Aug 6, 2025
8220e36
Add keyboard listener example and improve docs
FeodorFitsner Aug 6, 2025
07dc1fd
Enhance PageView with detailed docstrings and base class
FeodorFitsner Aug 6, 2025
2f0ed30
Fix tests: Remove ScrollableControl from PageView inheritance
FeodorFitsner Aug 6, 2025
643fdff
Merge branch 'main' into feodor/v1-canvas-recording
FeodorFitsner Aug 6, 2025
922012e
Refactor integration tests and add canvas test coverage
FeodorFitsner Aug 7, 2025
f4b9908
Lower pytest log level to INFO in AppVeyor config
FeodorFitsner Aug 7, 2025
d018c39
Update CI config and test finders only
FeodorFitsner Aug 7, 2025
78e5372
Run all integration tests
FeodorFitsner Aug 7, 2025
e2e0655
Update test runner and increase test delay in test_canvas.py
FeodorFitsner Aug 7, 2025
3778399
Run all tests, again
FeodorFitsner Aug 7, 2025
c0775e6
Refactor screenshot test timing in canvas tests
FeodorFitsner Aug 7, 2025
a4838fb
Add canvas capture tests and enable full CI matrix
FeodorFitsner Aug 8, 2025
d761dce
Refactor control initialization and property handling
FeodorFitsner Aug 8, 2025
06ad8ff
Add pixel_ratio support to Canvas capture methods
FeodorFitsner Aug 8, 2025
40a15e6
Apply suggestion from @Copilot
FeodorFitsner Aug 8, 2025
dfa7299
Update __init__.py
FeodorFitsner Aug 9, 2025
c36c5f4
improve some docstrings
ndonkoHenri Aug 9, 2025
035ad3d
`on_media_change` event receives new media data
ndonkoHenri Aug 9, 2025
03fb5cb
Canvas example: Add image saving and optimize canvas capture
FeodorFitsner Aug 9, 2025
083ce4e
Refactor canvas shape constructors to use keyword args
FeodorFitsner Aug 9, 2025
7eb6bb4
Add platform checks and validation to FilePicker methods
FeodorFitsner Aug 9, 2025
36d9da4
Remove base64 decoding fallback in loadCanvasImage
FeodorFitsner Aug 9, 2025
0b14355
Merge branch 'feodor/v1-canvas-recording' of https://github.com/flet-…
FeodorFitsner Aug 9, 2025
e67d21b
use FletExceptions and remove outdated brush.gif
ndonkoHenri Aug 9, 2025
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
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ for:

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

on_failure:
- find packages/flet/integration_tests -type f -name '*_actual.png' -exec appveyor PushArtifact {} \;
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
.python-version
vendor/
/client/android/app/.cxx
client/devtools_options.yaml
14 changes: 13 additions & 1 deletion client/integration_test/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,23 @@ void main() {
if (fletTestAppUrl != "") {
args.add(fletTestAppUrl);
}

const fletTestPidFile = String.fromEnvironment("FLET_TEST_PID_FILE_PATH");
if (fletTestPidFile != "") {
args.add(fletTestPidFile);
}

const fletTestAssetsDir = String.fromEnvironment("FLET_TEST_ASSETS_DIR");
if (fletTestAssetsDir != "") {
args.add(fletTestAssetsDir);
}

app.main(args);

await Future.delayed(const Duration(milliseconds: 500));
await app.tester?.pump(duration: const Duration(seconds: 1));
await app.tester?.pumpAndSettle(const Duration(milliseconds: 100));
await app.tester
?.pumpAndSettle(duration: const Duration(milliseconds: 100));
await app.tester?.waitForTeardown();
});
});
Expand Down
6 changes: 3 additions & 3 deletions client/integration_test/flutter_tester.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ class FlutterWidgetTester implements Tester {
FlutterWidgetTester(this._tester, this._binding);

@override
Future<void> pumpAndSettle(
[Duration duration = const Duration(milliseconds: 100)]) async {
Future<void> pumpAndSettle({Duration? duration}) async {
await lock.acquire();
try {
await _tester.pumpAndSettle(duration);
await _tester
.pumpAndSettle(duration ?? const Duration(milliseconds: 100));
} finally {
lock.release();
}
Expand Down
2 changes: 1 addition & 1 deletion client/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ void main([List<String>? args]) async {
//debugPrint("Uri.base: ${Uri.base}");

if (kDebugMode) {
pageUrl = "tcp://localhost:8550";
pageUrl = "http://localhost:8550";
}

if (kIsWeb) {
Expand Down
48 changes: 26 additions & 22 deletions packages/flet/lib/src/controls/base_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,21 +81,15 @@ Widget _directionality(Widget widget, Control control) {
}

Widget _expandable(Widget widget, Control control) {
var parent = control.parent;
if (parent != null && ["View", "Column", "Row"].contains(parent.type)) {
int? expand = control.properties.containsKey("expand")
? control.get("expand") == true
? 1
: control.get("expand") == false
? 0
: control.getInt("expand")
: null;
var expandLoose = control.getBool("expand_loose", false)!;
return expand != null
? (expandLoose == true)
? Flexible(flex: expand, child: widget)
: Expanded(flex: expand, child: widget)
: widget;
int? expand = control.get("expand") == true
? 1
: control.get("expand") == false
? 0
: control.getInt("expand");
if (expand != null && control.parent?.internals?["host_expanded"] == true) {
return (control.getBool("expand_loose") == true)
? Flexible(flex: expand, child: widget)
: Expanded(flex: expand, child: widget);
}
return widget;
}
Expand Down Expand Up @@ -206,8 +200,15 @@ Widget _positionedControl(
var right = control.getDouble("right", null);
var bottom = control.getDouble("bottom", null);

var errorControl = ErrorControl("Error displaying ${control.type}",
description:
"Control can be positioned absolutely with \"left\", \"top\", \"right\" and \"bottom\" properties inside Stack control only and page.overlay.");

var animation = control.getAnimation("animate_position");
if (animation != null) {
if (control.parent?.internals?["host_positioned"] != true) {
return errorControl;
}
if (left == null && top == null && right == null && bottom == null) {
left = 0;
top = 0;
Expand All @@ -228,11 +229,8 @@ Widget _positionedControl(
child: widget,
);
} else if (left != null || top != null || right != null || bottom != null) {
var parent = control.parent;
if (!["Stack", "Page", "Overlay"].contains(parent?.type)) {
return ErrorControl("Error displaying ${control.type}",
description:
"Control can be positioned absolutely with \"left\", \"top\", \"right\" and \"bottom\" properties inside Stack control only.");
if (control.parent?.internals?["host_positioned"] != true) {
return errorControl;
}
return Positioned(
left: left,
Expand All @@ -246,10 +244,16 @@ Widget _positionedControl(
}

Widget _sizedControl(Widget widget, Control control) {
final skipProps = control.internals?["skip_properties"] as List?;
if (skipProps?.contains("width") == true ||
skipProps?.contains("height") == true) {
return widget;
}

var width = control.getDouble("width");
var height = control.getDouble("height");
if ((width != null || height != null) &&
!["container", "image"].contains(control.type)) {

if ((width != null || height != null)) {
widget = ConstrainedBox(
constraints: BoxConstraints.tightFor(width: width, height: height),
child: widget,
Expand Down
Loading