Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
356 changes: 345 additions & 11 deletions packages/flet/lib/src/controls/interactive_viewer.dart

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions packages/flet/lib/src/utils/events.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ extension ScaleEndDetailsExtension on ScaleEndDetails {
extension ScaleUpdateDetailsExtension on ScaleUpdateDetails {
Map<String, dynamic> toMap() => {
"gfp": {"x": focalPoint.dx, "y": focalPoint.dy},
"fpdx": focalPointDelta.dx,
"fpdy": focalPointDelta.dy,
"fpd": {"x": focalPointDelta.dx, "y": focalPointDelta.dy},
"lfp": {"x": localFocalPoint.dx, "y": localFocalPoint.dy},
"pc": pointerCount,
"hs": horizontalScale,
Expand Down
1 change: 1 addition & 0 deletions packages/flet/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ dependencies:
sensors_plus: ^6.1.1
shared_preferences: 2.5.3
url_launcher: 6.3.2
vector_math: ^2.2.0
web: ^1.1.1
web_socket_channel: ^3.0.2
window_manager: ^0.5.1
Expand Down
51 changes: 51 additions & 0 deletions sdk/python/examples/controls/interactive_viewer/transformations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import flet as ft


def main(page: ft.Page):
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.vertical_alignment = ft.MainAxisAlignment.CENTER

async def handle_zoom_in(e: ft.Event[ft.Button]):
await i.zoom(1.2)

async def handle_zoom_out(e: ft.Event[ft.Button]):
await i.zoom(0.8)

async def handle_pan(e: ft.Event[ft.Button]):
await i.pan(dx=50, dy=50)

async def handle_reset(e: ft.Event[ft.Button]):
await i.reset()

async def handle_reset_slow(e: ft.Event[ft.Button]):
await i.reset(animation_duration=ft.Duration(seconds=2))

async def handle_save_state(e: ft.Event[ft.Button]):
await i.save_state()

async def handle_restore_state(e: ft.Event[ft.Button]):
await i.restore_state()

page.add(
i := ft.InteractiveViewer(
min_scale=0.1,
max_scale=5,
boundary_margin=ft.Margin.all(20),
content=ft.Image(src="https://picsum.photos/500/500"),
),
ft.Row(
wrap=True,
controls=[
ft.Button("Zoom In", on_click=handle_zoom_in),
ft.Button("Zoom Out", on_click=handle_zoom_out),
ft.Button("Pan", on_click=handle_pan),
ft.Button("Save State", on_click=handle_save_state),
ft.Button("Restore State", on_click=handle_restore_state),
ft.Button("Reset (instant)", on_click=handle_reset),
ft.Button("Reset (slow)", on_click=handle_reset_slow),
],
),
)


ft.run(main)
10 changes: 4 additions & 6 deletions sdk/python/examples/controls/reorderable_draggable/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,21 @@


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

page.add(
ft.ReorderableListView(
expand=True,
build_controls_on_demand=False,
show_default_drag_handles=False,
on_reorder=lambda e: print(
f"Reordered from {e.old_index} to {e.new_index}"
),
show_default_drag_handles=True,
controls=[
ft.ReorderableDraggable(
index=i,
content=ft.ListTile(
title=ft.Text(f"Item {i}", color=ft.Colors.BLACK),
title=ft.Text(f"Draggable Item {i}", color=ft.Colors.BLACK),
leading=ft.Icon(ft.Icons.CHECK, color=ft.Colors.RED),
bgcolor=get_color(i),
),
Expand Down
6 changes: 6 additions & 0 deletions sdk/python/packages/flet/docs/controls/interactiveviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ examples: ../../examples/controls/interactive_viewer
--8<-- "{{ examples }}/handling_events.py"
```

### Programmatic transformations

```python
--8<-- "{{ examples }}/transformations.py"
```

{{ class_members(class_name) }}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from dataclasses import field
from typing import Optional

from flet.controls.alignment import Alignment
Expand All @@ -11,7 +12,7 @@
ScaleUpdateEvent,
)
from flet.controls.layout_control import LayoutControl
from flet.controls.margin import MarginValue
from flet.controls.margin import Margin, MarginValue
from flet.controls.types import ClipBehavior, Number

__all__ = ["InteractiveViewer"]
Expand Down Expand Up @@ -50,8 +51,20 @@ class InteractiveViewer(LayoutControl):

constrained: bool = True
"""
Whether the normal size constraints at this point in the widget tree are applied
to the child.
Whether the normal size constraints at this point in the control tree are applied
to the [`content`][(c).].

If set to `False`, then the content will be given infinite constraints. This
is often useful when a content should be bigger than this `InteractiveViewer`.

For example, for a content which is bigger than the viewport but can be
panned to reveal parts that were initially offscreen, `constrained` must
be set to `False` to allow it to size itself properly. If `constrained` is
`True` and the content can only size itself to the viewport, then areas
initially outside of the viewport will not be able to receive user
interaction events. If experiencing regions of the content that are not
receptive to user gestures, make sure `constrained` is `False` and the content
is sized properly.
"""

max_scale: Number = 2.5
Expand All @@ -67,6 +80,12 @@ class InteractiveViewer(LayoutControl):
"""
The minimum allowed scale.

The effective scale is limited by the value of [`boundary_margin`][(c).].
If scaling would cause the content to be displayed outside the defined boundary,
it is prevented. By default, `boundary_margin` is set to `Margin.all(0)`,
so scaling below `1.0` is typically not possible unless you increase the
`boundary_margin` value.

Raises:
ValueError: If it is not greater than `0` or less than [`max_scale`][(c).].
"""
Expand All @@ -82,21 +101,42 @@ class InteractiveViewer(LayoutControl):
scale_factor: Number = 200
"""
The amount of scale to be performed per pointer scroll.

Increasing this value above the default causes scaling to feel slower,
while decreasing it causes scaling to feel faster.

Note:
Has effect only on pointer device scrolling, not pinch to zoom.
"""

clip_behavior: ClipBehavior = ClipBehavior.HARD_EDGE
"""
Defines how to clip the [`content`][(c).].

If set to [`ClipBehavior.NONE`][flet.], the [`content`][(c).] can visually overflow
the bounds of this `InteractiveViewer`, but gesture events (such as pan or zoom)
will only be recognized within the viewer's area. Ensure this `InteractiveViewer`
is sized appropriately when using [`ClipBehavior.NONE`][flet.].
"""

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

boundary_margin: MarginValue = 0
boundary_margin: MarginValue = field(default_factory=lambda: Margin.all(0))
"""
A margin for the visible boundaries of the [`content`][(c).].

Any transformation that results in the viewport being able to view outside
of the boundaries will be stopped at the boundary. The boundaries do not
Copy link

Copilot AI Nov 5, 2025

Choose a reason for hiding this comment

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

[nitpick] Using a lambda in default_factory is unnecessarily verbose. Consider using default_factory=lambda: Margin.all(0) can be simplified to a partial function or a named function if this pattern is repeated, or use field(default_factory=Margin) if Margin.all(0) is the default constructor behavior.

Copilot uses AI. Check for mistakes.
rotate with the rest of the scene, so they are always aligned with the
viewport.

To produce no boundaries at all, pass an infinite value.

Defaults to `Margin.all(0)`, which results in boundaries that are the
exact same size and position as the [`content`][(c).].
"""

interaction_update_interval: int = 200
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,24 @@ class ReorderableDraggable(LayoutControl, AdaptiveControl):

It creates a listener for a drag immediately following a pointer down
event over the given [`content`][(c).] control.

Example:
```python
ft.ReorderableListView(
expand=True,
show_default_drag_handles=False,
controls=[
ft.ReorderableDraggable(
index=i,
content=ft.ListTile(
title=f"Draggable Item {i}",
bgcolor=ft.Colors.GREY if i % 2 == 0 else ft.Colors.BLUE_ACCENT,
),
)
for i in range(10)
],
)
```
"""

index: int
Expand Down
Loading
Loading