Skip to content

Commit be928bd

Browse files
fix: improve InteractiveViewer programmatic transformations (#5775)
* initial commit * more docs * example + docs * cleanup * comment code and fix dart analysis * refactor: enhance documentation for reorderable components * fix `ScaleUpdateDetails.toMap()` * fix markdown tests: set similarity threshold to 97 * fix: update documentation for image repeat modes in `types.py` * apply review suggestions * Refactor markdown test to use local Markdown instances Replaces the shared 'md' Markdown instance with local instances in each test, and renames sample variables for clarity. This improves test isolation and readability. --------- Co-authored-by: Feodor Fitsner <feodor@appveyor.com>
1 parent 0044b8e commit be928bd

File tree

11 files changed

+560
-61
lines changed

11 files changed

+560
-61
lines changed

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

Lines changed: 345 additions & 11 deletions
Large diffs are not rendered by default.

packages/flet/lib/src/utils/events.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ extension ScaleEndDetailsExtension on ScaleEndDetails {
1313
extension ScaleUpdateDetailsExtension on ScaleUpdateDetails {
1414
Map<String, dynamic> toMap() => {
1515
"gfp": {"x": focalPoint.dx, "y": focalPoint.dy},
16-
"fpdx": focalPointDelta.dx,
17-
"fpdy": focalPointDelta.dy,
16+
"fpd": {"x": focalPointDelta.dx, "y": focalPointDelta.dy},
1817
"lfp": {"x": localFocalPoint.dx, "y": localFocalPoint.dy},
1918
"pc": pointerCount,
2019
"hs": horizontalScale,

packages/flet/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ dependencies:
3939
sensors_plus: ^6.1.1
4040
shared_preferences: 2.5.3
4141
url_launcher: 6.3.2
42+
vector_math: ^2.2.0
4243
web: ^1.1.1
4344
web_socket_channel: ^3.0.2
4445
window_manager: ^0.5.1
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import flet as ft
2+
3+
4+
def main(page: ft.Page):
5+
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
6+
page.vertical_alignment = ft.MainAxisAlignment.CENTER
7+
8+
async def handle_zoom_in(e: ft.Event[ft.Button]):
9+
await i.zoom(1.2)
10+
11+
async def handle_zoom_out(e: ft.Event[ft.Button]):
12+
await i.zoom(0.8)
13+
14+
async def handle_pan(e: ft.Event[ft.Button]):
15+
await i.pan(dx=50, dy=50)
16+
17+
async def handle_reset(e: ft.Event[ft.Button]):
18+
await i.reset()
19+
20+
async def handle_reset_slow(e: ft.Event[ft.Button]):
21+
await i.reset(animation_duration=ft.Duration(seconds=2))
22+
23+
async def handle_save_state(e: ft.Event[ft.Button]):
24+
await i.save_state()
25+
26+
async def handle_restore_state(e: ft.Event[ft.Button]):
27+
await i.restore_state()
28+
29+
page.add(
30+
i := ft.InteractiveViewer(
31+
min_scale=0.1,
32+
max_scale=5,
33+
boundary_margin=ft.Margin.all(20),
34+
content=ft.Image(src="https://picsum.photos/500/500"),
35+
),
36+
ft.Row(
37+
wrap=True,
38+
controls=[
39+
ft.Button("Zoom In", on_click=handle_zoom_in),
40+
ft.Button("Zoom Out", on_click=handle_zoom_out),
41+
ft.Button("Pan", on_click=handle_pan),
42+
ft.Button("Save State", on_click=handle_save_state),
43+
ft.Button("Restore State", on_click=handle_restore_state),
44+
ft.Button("Reset (instant)", on_click=handle_reset),
45+
ft.Button("Reset (slow)", on_click=handle_reset_slow),
46+
],
47+
),
48+
)
49+
50+
51+
ft.run(main)

sdk/python/examples/controls/reorderable_draggable/basic.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,21 @@
22

33

44
def main(page: ft.Page):
5-
get_color = lambda i: (
6-
ft.Colors.ERROR if i % 2 == 0 else ft.Colors.ON_ERROR_CONTAINER
7-
)
5+
def get_color(index: int) -> ft.Colors:
6+
return ft.Colors.ERROR if index % 2 == 0 else ft.Colors.ON_ERROR_CONTAINER
87

98
page.add(
109
ft.ReorderableListView(
1110
expand=True,
12-
build_controls_on_demand=False,
11+
show_default_drag_handles=False,
1312
on_reorder=lambda e: print(
1413
f"Reordered from {e.old_index} to {e.new_index}"
1514
),
16-
show_default_drag_handles=True,
1715
controls=[
1816
ft.ReorderableDraggable(
1917
index=i,
2018
content=ft.ListTile(
21-
title=ft.Text(f"Item {i}", color=ft.Colors.BLACK),
19+
title=ft.Text(f"Draggable Item {i}", color=ft.Colors.BLACK),
2220
leading=ft.Icon(ft.Icons.CHECK, color=ft.Colors.RED),
2321
bgcolor=get_color(i),
2422
),

sdk/python/packages/flet/docs/controls/interactiveviewer.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,10 @@ examples: ../../examples/controls/interactive_viewer
1515
--8<-- "{{ examples }}/handling_events.py"
1616
```
1717

18+
### Programmatic transformations
19+
20+
```python
21+
--8<-- "{{ examples }}/transformations.py"
22+
```
23+
1824
{{ class_members(class_name) }}

sdk/python/packages/flet/integration_tests/controls/material/test_markdown.py

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import flet as ft
44
import flet.testing as ftt
55

6-
sample1 = """
6+
sample_1 = """
77
# Markdown Example
88
Markdown allows you to easily include formatted text, images, and even formatted Dart
99
code in your app.
@@ -46,7 +46,7 @@
4646
4747
"""
4848

49-
sample2 = """
49+
sample_2 = """
5050
## Tables
5151
5252
|Syntax |Result |
@@ -77,25 +77,26 @@
7777
```
7878
"""
7979

80-
md = ft.Markdown(
81-
value=sample1,
82-
selectable=True,
83-
extension_set=ft.MarkdownExtensionSet.GITHUB_WEB,
84-
)
85-
8680

8781
@pytest.mark.asyncio(loop_scope="module")
8882
async def test_md_1(flet_app: ftt.FletTestApp, request):
8983
await flet_app.assert_control_screenshot(
9084
request.node.name,
91-
md,
85+
ft.Markdown(
86+
value=sample_1,
87+
selectable=True,
88+
extension_set=ft.MarkdownExtensionSet.GITHUB_WEB,
89+
),
9290
)
9391

9492

9593
@pytest.mark.asyncio(loop_scope="module")
9694
async def test_md_2(flet_app: ftt.FletTestApp, request):
97-
md.value = sample2
9895
await flet_app.assert_control_screenshot(
9996
request.node.name,
100-
md,
97+
ft.Markdown(
98+
value=sample_2,
99+
selectable=True,
100+
extension_set=ft.MarkdownExtensionSet.GITHUB_WEB,
101+
),
101102
)

sdk/python/packages/flet/src/flet/controls/core/interactive_viewer.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dataclasses import field
12
from typing import Optional
23

34
from flet.controls.alignment import Alignment
@@ -11,7 +12,7 @@
1112
ScaleUpdateEvent,
1213
)
1314
from flet.controls.layout_control import LayoutControl
14-
from flet.controls.margin import MarginValue
15+
from flet.controls.margin import Margin, MarginValue
1516
from flet.controls.types import ClipBehavior, Number
1617

1718
__all__ = ["InteractiveViewer"]
@@ -50,8 +51,20 @@ class InteractiveViewer(LayoutControl):
5051

5152
constrained: bool = True
5253
"""
53-
Whether the normal size constraints at this point in the widget tree are applied
54-
to the child.
54+
Whether the normal size constraints at this point in the control tree are applied
55+
to the [`content`][(c).].
56+
57+
If set to `False`, then the content will be given infinite constraints. This
58+
is often useful when a content should be bigger than this `InteractiveViewer`.
59+
60+
For example, for a content which is bigger than the viewport but can be
61+
panned to reveal parts that were initially offscreen, `constrained` must
62+
be set to `False` to allow it to size itself properly. If `constrained` is
63+
`True` and the content can only size itself to the viewport, then areas
64+
initially outside of the viewport will not be able to receive user
65+
interaction events. If experiencing regions of the content that are not
66+
receptive to user gestures, make sure `constrained` is `False` and the content
67+
is sized properly.
5568
"""
5669

5770
max_scale: Number = 2.5
@@ -67,6 +80,12 @@ class InteractiveViewer(LayoutControl):
6780
"""
6881
The minimum allowed scale.
6982
83+
The effective scale is limited by the value of [`boundary_margin`][(c).].
84+
If scaling would cause the content to be displayed outside the defined boundary,
85+
it is prevented. By default, `boundary_margin` is set to `Margin.all(0)`,
86+
so scaling below `1.0` is typically not possible unless you increase the
87+
`boundary_margin` value.
88+
7089
Raises:
7190
ValueError: If it is not greater than `0` or less than [`max_scale`][(c).].
7291
"""
@@ -82,21 +101,42 @@ class InteractiveViewer(LayoutControl):
82101
scale_factor: Number = 200
83102
"""
84103
The amount of scale to be performed per pointer scroll.
104+
105+
Increasing this value above the default causes scaling to feel slower,
106+
while decreasing it causes scaling to feel faster.
107+
108+
Note:
109+
Has effect only on pointer device scrolling, not pinch to zoom.
85110
"""
86111

87112
clip_behavior: ClipBehavior = ClipBehavior.HARD_EDGE
88113
"""
89114
Defines how to clip the [`content`][(c).].
115+
116+
If set to [`ClipBehavior.NONE`][flet.], the [`content`][(c).] can visually overflow
117+
the bounds of this `InteractiveViewer`, but gesture events (such as pan or zoom)
118+
will only be recognized within the viewer's area. Ensure this `InteractiveViewer`
119+
is sized appropriately when using [`ClipBehavior.NONE`][flet.].
90120
"""
91121

92122
alignment: Optional[Alignment] = None
93123
"""
94124
The alignment of the [`content`][(c).] within this viewer.
95125
"""
96126

97-
boundary_margin: MarginValue = 0
127+
boundary_margin: MarginValue = field(default_factory=lambda: Margin.all(0))
98128
"""
99129
A margin for the visible boundaries of the [`content`][(c).].
130+
131+
Any transformation that results in the viewport being able to view outside
132+
of the boundaries will be stopped at the boundary. The boundaries do not
133+
rotate with the rest of the scene, so they are always aligned with the
134+
viewport.
135+
136+
To produce no boundaries at all, pass an infinite value.
137+
138+
Defaults to `Margin.all(0)`, which results in boundaries that are the
139+
exact same size and position as the [`content`][(c).].
100140
"""
101141

102142
interaction_update_interval: int = 200

sdk/python/packages/flet/src/flet/controls/core/reorderable_draggable.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ class ReorderableDraggable(LayoutControl, AdaptiveControl):
1111
1212
It creates a listener for a drag immediately following a pointer down
1313
event over the given [`content`][(c).] control.
14+
15+
Example:
16+
```python
17+
ft.ReorderableListView(
18+
expand=True,
19+
show_default_drag_handles=False,
20+
controls=[
21+
ft.ReorderableDraggable(
22+
index=i,
23+
content=ft.ListTile(
24+
title=f"Draggable Item {i}",
25+
bgcolor=ft.Colors.GREY if i % 2 == 0 else ft.Colors.BLUE_ACCENT,
26+
),
27+
)
28+
for i in range(10)
29+
],
30+
)
31+
```
1432
"""
1533

1634
index: int

0 commit comments

Comments
 (0)