```
from pathlib import Path
from typing import Optional, Union, Callable

import flet as ft
from PIL import Image

from configs.path_config import EXAMPLE_IMAGE


class CroppingContainer:
    def __init__(self, src: Union[Path, str] = "", on_rect_changed: Optional[Callable] = None):
        """画像と矩形を表示するコンテナを初期化する。

        Args:
            src: 画像のパス。
            on_rect_changed: 矩形が変更されたときに呼び出されるコールバック。
        """
        self._src = src
        self.on_rect_changed = on_rect_changed

        # 画像と矩形の設定
        self._image = ft.Image(src=str(src), fit=ft.ImageFit.CONTAIN)  # コンテナにフィット
        self._rect = ft.Container(
            border=ft.border.all(2, ft.Colors.RED),  # 赤い枠線
            width=100,
            height=100,
            left=0,
            top=0,
        )

        # 画像と矩形を重ねる
        self._stack = ft.Stack([self._image, self._rect])
        self._gesture_detector = ft.GestureDetector(
            content=self._stack,
            on_pan_start=self._on_pan_start,
            on_pan_update=self._on_pan_update,
            on_pan_end=self._on_pan_end,
        )
        self.__container = ft.Container(content=self._gesture_detector, expand=True)

        # ドラッグ状態の管理
        self._dragging = False
        self._resizing = False
        self._start_x = 0
        self._start_y = 0

        # 画像サイズの管理
        self._image_width = 0  # 表示サイズ
        self._image_height = 0
        self._actual_image_width = 0  # 実際のサイズ
        self._actual_image_height = 0
        self._load_actual_image_size()

    def _load_actual_image_size(self):
        """実際の画像サイズをPillowで取得する。"""
        if self._src:
            with Image.open(self._src) as img:
                self._actual_image_width, self._actual_image_height = img.size

    def set_image(self, image: Union[Path, str]):
        """新しい画像を設定し、矩形をリセットする。"""
        self._src = image
        self._image.src = str(image)
        self._load_actual_image_size()
        self._reset_rect()

    def _reset_rect(self):
        """矩形を初期位置とサイズにリセットする。"""
        self._rect.left = 0
        self._rect.top = 0
        self._rect.width = 100
        self._rect.height = 100
        self._update_rect()

    def _on_pan_start(self, e: ft.DragStartEvent):
        """ドラッグ開始時の処理。移動かリサイズかを判定。"""
        self._start_x = e.local_x
        self._start_y = e.local_y
        rect_right = self._rect.left + self._rect.width
        rect_bottom = self._rect.top + self._rect.height
        # 右下角付近ならリサイズ、それ以外は移動
        if abs(self._start_x - rect_right) < 10 and abs(self._start_y - rect_bottom) < 10:
            self._resizing = True
        else:
            self._dragging = True

    def _on_pan_update(self, e: ft.DragUpdateEvent):
        """ドラッグ中の処理。矩形を移動またはリサイズ。"""
        if self._dragging:
            delta_x = e.delta_x
            delta_y = e.delta_y
            new_left = self._rect.left + delta_x
            new_top = self._rect.top + delta_y
            # 画像境界内に制限
            if new_left >= 0 and new_left + self._rect.width <= self._image_width:
                self._rect.left = new_left
            if new_top >= 0 and new_top + self._rect.height <= self._image_height:
                self._rect.top = new_top
        elif self._resizing:
            delta_x = e.delta_x
            delta_y = e.delta_y
            new_width = self._rect.width + delta_x
            new_height = self._rect.height + delta_y
            # 最小サイズと画像境界内に制限
            if new_width > 10 and self._rect.left + new_width <= self._image_width:
                self._rect.width = new_width
            if new_height > 10 and self._rect.top + new_height <= self._image_height:
                self._rect.height = new_height
        self._update_rect()

    def _on_pan_end(self, e: ft.DragEndEvent):
        """ドラッグ終了時の処理。コールバックを呼び出す。"""
        self._dragging = False
        self._resizing = False
        if self.on_rect_changed:
            self.on_rect_changed(self.rect_area)

    def _update_rect(self):
        """矩形の表示を更新する。"""
        self._stack.update()

    @property
    def container(self) -> ft.Container:
        """コンテナを取得する。"""
        return self.__container

    @property
    def rect_area(self) -> tuple[float, float, float, float]:
        """矩形が囲む領域を実際の画像座標で返す。

        Returns:
            tuple: (x1, y1, x2, y2) の形式で、実際の画像座標を返す。
        """
        if self._image_width == 0 or self._image_height == 0:
            return (0, 0, 0, 0)
        # 表示サイズと実際のサイズの比率
        scale_x = self._actual_image_width / self._image_width
        scale_y = self._actual_image_height / self._image_height
        x1 = self._rect.left * scale_x
        y1 = self._rect.top * scale_y
        x2 = (self._rect.left + self._rect.width) * scale_x
        y2 = (self._rect.top + self._rect.height) * scale_y
        return (x1, y1, x2, y2)

    def set_rect(self, left: float, top: float, width: float, height: float):
        """矩形の位置とサイズを実際の画像座標で設定する。

        Args:
            left: 左端の座標（実際の画像座標）。
            top: 上端の座標（実際の画像座標）。
            width: 幅（実際の画像座標）。
            height: 高さ（実際の画像座標）。
        """
        if self._image_width == 0 or self._image_height == 0:
            return
        # 実際の座標を表示座標に変換
        scale_x = self._image_width / self._actual_image_width
        scale_y = self._image_height / self._actual_image_height
        self._rect.left = left * scale_x
        self._rect.top = top * scale_y
        self._rect.width = width * scale_x
        self._rect.height = height * scale_y
        self._update_rect()

    def did_mount(self):
        """マウント後に呼び出され、画像の表示サイズを更新する。"""
        self._update_image_size()

    def _update_image_size(self):
        """画像の表示サイズを更新する。"""
        # 仮にコンテナのサイズを使用（必要に応じて調整）
        self._image_width = self.__container.width if self.__container.width else 100
        self._image_height = self.__container.height if self.__container.height else 100


def main(page: ft.Page):
    # CroppingContainer のインスタンス
    crop = CroppingContainer(EXAMPLE_IMAGE)

    # 矩形の座標を表示するテキスト
    rect_text = ft.Text("現在の矩形のサイズが表示されます")

    # 矩形変更時にテキストを更新
    def update_rect(rect):
        rect_text.value = f"Rect: ({rect[0]:.0f}, {rect[1]:.0f}, {rect[2]:.0f}, {rect[3]:.0f})"
        page.update()

    crop.on_rect_changed = update_rect

    # 矩形をプリセットに設定
    def set_rect(e):
        crop.set_rect(50, 50, 150, 150)  # 実際の画像座標で設定
        update_rect(crop.rect_area)

    # UI
    page.add(
        crop.container,  # 画像と矩形
        ft.ElevatedButton("Set Rect", on_click=set_rect),  # 矩形設定ボタン
        rect_text,  # 座標表示
    )

    crop.did_mount()  # 初期化


ft.app(main)
```

矩形の四隅にハンドルをつけて、矩形を直接操作できるようにして。