from __future__ import annotations

import warnings
from abc import ABC, abstractmethod
from collections.abc import Iterator
from contextlib import contextmanager
from math import cos, pi, sin, tan
from typing import (
    TYPE_CHECKING,
    Any,
    ContextManager,
    Literal,
    NoReturn,
    Protocol,
)

from travertino.colors import Color

import toga
from toga.colors import BLACK, color as parse_color
from toga.constants import Baseline, FillRule
from toga.fonts import (
    SYSTEM,
    SYSTEM_DEFAULT_FONT_SIZE,
    Font,
)
from toga.handlers import wrapped_handler

from .base import StyleT, Widget

if TYPE_CHECKING:
    from toga.images import ImageT

#######################################################################################
# Simple drawing objects
#######################################################################################


class DrawingObject(ABC):
    """A drawing operation in a :any:`Context`.

    Every context drawing method creates a ``DrawingObject``, adds it to the context,
    and returns it. Each argument passed to the method becomes a property of the
    ``DrawingObject``, which can be modified as shown in the `Usage`_ section.

    ``DrawingObjects`` can also be created manually, then added to a context using the
    :meth:`~Context.append` or :meth:`~Context.insert` methods. Their constructors take
    the same arguments as the corresponding :any:`Context` method, and their classes
    have the same names, but capitalized:

    * :meth:`toga.widgets.canvas.Arc <Context.arc>`
    * :meth:`toga.widgets.canvas.BeginPath <Context.begin_path>`
    * :meth:`toga.widgets.canvas.BezierCurveTo <Context.bezier_curve_to>`
    * :meth:`toga.widgets.canvas.ClosePath <Context.close_path>`
    * :meth:`toga.widgets.canvas.Ellipse <Context.ellipse>`
    * :meth:`toga.widgets.canvas.Fill <Context.fill>`
    * :meth:`toga.widgets.canvas.LineTo <Context.line_to>`
    * :meth:`toga.widgets.canvas.MoveTo <Context.move_to>`
    * :meth:`toga.widgets.canvas.QuadraticCurveTo <Context.quadratic_curve_to>`
    * :meth:`toga.widgets.canvas.Rect <Context.rect>`
    * :meth:`toga.widgets.canvas.ResetTransform <Context.reset_transform>`
    * :meth:`toga.widgets.canvas.Rotate <Context.rotate>`
    * :meth:`toga.widgets.canvas.Scale <Context.scale>`
    * :meth:`toga.widgets.canvas.Stroke <Context.stroke>`
    * :meth:`toga.widgets.canvas.Translate <Context.translate>`
    * :meth:`toga.widgets.canvas.WriteText <Context.write_text>`
    """

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}()"

    @abstractmethod
    def _draw(self, impl: Any, **kwargs: Any) -> None: ...


class BeginPath(DrawingObject):
    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.begin_path(**kwargs)


class ClosePath(DrawingObject):
    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.close_path(**kwargs)


class Fill(DrawingObject):
    def __init__(
        self,
        color: str = BLACK,
        fill_rule: FillRule = FillRule.NONZERO,
    ):
        super().__init__()
        self.color = color
        self.fill_rule = fill_rule

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(color={self.color!r}, "
            f"fill_rule={self.fill_rule})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.fill(self.color, self.fill_rule, **kwargs)

    @property
    def fill_rule(self) -> FillRule:
        return self._fill_rule

    @fill_rule.setter
    def fill_rule(self, fill_rule: FillRule) -> None:
        self._fill_rule = fill_rule

    @property
    def color(self) -> Color:
        return self._color

    @color.setter
    def color(self, value: Color | str | None) -> None:
        if value is None:
            self._color = parse_color(BLACK)
        else:
            self._color = parse_color(value)

    ###########################################################################
    # 2023-07 Backwards incompatibility
    ###########################################################################

    # `context.fill()` used to be a context manager, but is now a primitive.
    # If you try to use the Fill drawing object as a context, raise an exception.
    def __enter__(self) -> NoReturn:
        raise RuntimeError("Context.fill() has been renamed Context.Fill().")

    def __exit__(self) -> None:  # pragma: no cover
        # This method is required to make the object a context manager, but as the
        # __enter__ method raises an exception, the __exit__ can't be called.
        pass


class Stroke(DrawingObject):
    def __init__(
        self,
        color: Color | str | None = BLACK,
        line_width: float = 2.0,
        line_dash: list[float] | None = None,
    ):
        super().__init__()
        self.color = color
        self.line_width = line_width
        self.line_dash = line_dash

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(color={self.color!r}, "
            f"line_width={self.line_width}, line_dash={self.line_dash!r})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.stroke(self.color, self.line_width, self.line_dash, **kwargs)

    @property
    def color(self) -> Color:
        return self._color

    @color.setter
    def color(self, value: Color | str | None) -> None:
        if value is None:
            self._color = parse_color(BLACK)
        else:
            self._color = parse_color(value)

    @property
    def line_width(self) -> float:
        return self._line_width

    @line_width.setter
    def line_width(self, value: float) -> None:
        self._line_width = float(value)

    @property
    def line_dash(self) -> list[float] | None:
        return self._line_dash

    @line_dash.setter
    def line_dash(self, value: list[float] | None) -> None:
        self._line_dash = value

    ###########################################################################
    # 2023-07 Backwards incompatibility
    ###########################################################################

    # `context.stroke()` used to be a context manager, but is now a primitive.
    # If you try to use the Stroke drawing object as a context, raise an exception.
    def __enter__(self) -> NoReturn:
        raise RuntimeError("Context.stroke() has been renamed Context.Stroke().")

    def __exit__(self) -> None:  # pragma: no cover
        # This method is required to make the object a context manager, but as the
        # __enter__ method raises an exception, the __exit__ can't be called.
        pass


class MoveTo(DrawingObject):
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(x={self.x}, y={self.y})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.move_to(self.x, self.y, **kwargs)


class LineTo(DrawingObject):
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(x={self.x}, y={self.y})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.line_to(self.x, self.y, **kwargs)


class BezierCurveTo(DrawingObject):
    def __init__(
        self, cp1x: float, cp1y: float, cp2x: float, cp2y: float, x: float, y: float
    ):
        self.cp1x = cp1x
        self.cp1y = cp1y
        self.cp2x = cp2x
        self.cp2y = cp2y
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(cp1x={self.cp1x}, cp1y={self.cp1y}, "
            f"cp2x={self.cp2x}, cp2y={self.cp2y}, "
            f"x={self.x}, y={self.y})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.bezier_curve_to(
            self.cp1x, self.cp1y, self.cp2x, self.cp2y, self.x, self.y, **kwargs
        )


class QuadraticCurveTo(DrawingObject):
    def __init__(self, cpx: float, cpy: float, x: float, y: float):
        self.cpx = cpx
        self.cpy = cpy
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(cpx={self.cpx}, cpy={self.cpy}, x={self.x}, y={self.y})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.quadratic_curve_to(self.cpx, self.cpy, self.x, self.y, **kwargs)


class Arc(DrawingObject):
    def __init__(
        self,
        x: float,
        y: float,
        radius: float,
        startangle: float = 0.0,
        endangle: float = 2 * pi,
        anticlockwise: bool = False,
    ):
        self.x = x
        self.y = y
        self.radius = radius
        self.startangle = startangle
        self.endangle = endangle
        self.anticlockwise = anticlockwise

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(x={self.x}, y={self.y}, "
            f"radius={self.radius}, startangle={self.startangle:.3f}, "
            f"endangle={self.endangle:.3f}, anticlockwise={self.anticlockwise})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.arc(
            self.x,
            self.y,
            self.radius,
            self.startangle,
            self.endangle,
            self.anticlockwise,
            **kwargs,
        )


class Ellipse(DrawingObject):
    def __init__(
        self,
        x: float,
        y: float,
        radiusx: float,
        radiusy: float,
        rotation: float = 0.0,
        startangle: float = 0.0,
        endangle: float = 2 * pi,
        anticlockwise: bool = False,
    ):
        self.x = x
        self.y = y
        self.radiusx = radiusx
        self.radiusy = radiusy
        self.rotation = rotation
        self.startangle = startangle
        self.endangle = endangle
        self.anticlockwise = anticlockwise

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(x={self.x}, y={self.y}, "
            f"radiusx={self.radiusx}, radiusy={self.radiusy}, "
            f"rotation={self.rotation:.3f}, startangle={self.startangle:.3f}, "
            f"endangle={self.endangle:.3f}, anticlockwise={self.anticlockwise})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.ellipse(
            self.x,
            self.y,
            self.radiusx,
            self.radiusy,
            self.rotation,
            self.startangle,
            self.endangle,
            self.anticlockwise,
            **kwargs,
        )


class Rect(DrawingObject):
    def __init__(self, x: float, y: float, width: float, height: float):
        self.x = x
        self.y = y
        self.width = width
        self.height = height

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(x={self.x}, y={self.y}, "
            f"width={self.width}, height={self.height})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.rect(self.x, self.y, self.width, self.height, **kwargs)


class WriteText(DrawingObject):
    def __init__(
        self,
        text: str,
        x: float = 0.0,
        y: float = 0.0,
        font: Font | None = None,
        baseline: Baseline = Baseline.ALPHABETIC,
    ):
        self.text = text
        self.x = x
        self.y = y
        self.font = font
        self.baseline = baseline

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(text={self.text!r}, x={self.x}, y={self.y}, "
            f"font={self.font!r}, baseline={self.baseline})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.write_text(
            str(self.text), self.x, self.y, self.font._impl, self.baseline, **kwargs
        )

    @property
    def font(self) -> Font:
        return self._font

    @font.setter
    def font(self, value: Font | None) -> None:
        if value is None:
            self._font = Font(family=SYSTEM, size=SYSTEM_DEFAULT_FONT_SIZE)
        else:
            self._font = value


class Rotate(DrawingObject):
    def __init__(self, radians: float):
        self.radians = radians

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(radians={self.radians:.3f})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.rotate(self.radians, **kwargs)


class Scale(DrawingObject):
    def __init__(self, sx: float, sy: float):
        self.sx = sx
        self.sy = sy

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(sx={self.sx:.3f}, sy={self.sy:.3f})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.scale(self.sx, self.sy, **kwargs)


class Translate(DrawingObject):
    def __init__(self, tx: float, ty: float):
        self.tx = tx
        self.ty = ty

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(tx={self.tx}, ty={self.ty})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.translate(self.tx, self.ty, **kwargs)


class ResetTransform(DrawingObject):
    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.reset_transform(**kwargs)


#######################################################################################
# Drawing Contexts
#######################################################################################


class Context(DrawingObject):
    """A drawing context for a canvas.

    You should not create a :class:`~toga.widgets.canvas.Context` directly; instead,
    you should use the :meth:`~Context.Context` method on an existing context,
    or use :any:`Canvas.context` to access the root context of the canvas.
    """

    def __init__(self, canvas: toga.Canvas, **kwargs: Any):
        # kwargs used to support multiple inheritance
        super().__init__(**kwargs)
        self._canvas = canvas
        self.drawing_objects: list[DrawingObject] = []

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.push_context(**kwargs)
        for obj in self.drawing_objects:
            obj._draw(impl, **kwargs)
        impl.pop_context(**kwargs)

    ###########################################################################
    # Methods to keep track of the canvas, automatically redraw it
    ###########################################################################

    @property
    def canvas(self) -> Canvas:
        """The canvas that is associated with this drawing context."""
        return self._canvas

    def redraw(self) -> None:
        """Calls :any:`Canvas.redraw` on the parent Canvas."""
        self.canvas.redraw()

    ###########################################################################
    # Operations on drawing objects
    ###########################################################################

    def __len__(self) -> int:
        """Returns the number of drawing objects that are in this context."""
        return len(self.drawing_objects)

    def __getitem__(self, index: int) -> DrawingObject:
        """Returns the drawing object at the given index."""
        return self.drawing_objects[index]

    def append(self, obj: DrawingObject) -> None:
        """Append a drawing object to the context.

        :param obj: The drawing object to add to the context.
        """
        self.drawing_objects.append(obj)
        self.redraw()

    def insert(self, index: int, obj: DrawingObject) -> None:
        """Insert a drawing object into the context at a specific index.

        :param index: The index at which the drawing object should be inserted.
        :param obj: The drawing object to add to the context.
        """
        self.drawing_objects.insert(index, obj)
        self.redraw()

    def remove(self, obj: DrawingObject) -> None:
        """Remove a drawing object from the context.

        :param obj: The drawing object to remove.
        """
        self.drawing_objects.remove(obj)
        self.redraw()

    def clear(self) -> None:
        """Remove all drawing objects from the context."""
        self.drawing_objects.clear()
        self.redraw()

    ###########################################################################
    # Path manipulation
    ###########################################################################

    def begin_path(self) -> BeginPath:
        """Start a new path in the canvas context.

        :returns: The ``BeginPath`` :any:`DrawingObject` for the operation.
        """
        begin_path = BeginPath()
        self.append(begin_path)
        return begin_path

    def close_path(self) -> ClosePath:
        """Close the current path in the canvas context.

        This closes the current path as a simple drawing operation. It should be paired
        with a :meth:`~toga.widgets.canvas.Context.begin_path()` operation; or, to
        complete a complete closed path, use the
        :meth:`~toga.widgets.canvas.Context.ClosedPath()` context manager.

        :returns: The ``ClosePath`` :any:`DrawingObject` for the operation.
        """
        close_path = ClosePath()
        self.append(close_path)
        return close_path

    def move_to(self, x: float, y: float) -> MoveTo:
        """Moves the current point of the canvas context without drawing.

        :param x: The x coordinate of the new current point.
        :param y: The y coordinate of the new current point.
        :returns: The ``MoveTo`` :any:`DrawingObject` for the operation.
        """
        move_to = MoveTo(x, y)
        self.append(move_to)
        return move_to

    def line_to(self, x: float, y: float) -> LineTo:
        """Draw a line segment ending at a point in the canvas context.

        :param x: The x coordinate for the end point of the line segment.
        :param y: The y coordinate for the end point of the line segment.
        :returns: The ``LineTo`` :any:`DrawingObject` for the operation.
        """
        line_to = LineTo(x, y)
        self.append(line_to)
        return line_to

    def bezier_curve_to(
        self,
        cp1x: float,
        cp1y: float,
        cp2x: float,
        cp2y: float,
        x: float,
        y: float,
    ) -> BezierCurveTo:
        """Draw a Bézier curve in the canvas context.

        A Bézier curve requires three points. The first two are control points; the
        third is the end point for the curve. The starting point is the last point in
        the current path, which can be changed using move_to() before creating the
        Bézier curve.

        :param cp1y: The y coordinate for the first control point of the Bézier curve.
        :param cp1x: The x coordinate for the first control point of the Bézier curve.
        :param cp2x: The x coordinate for the second control point of the Bézier curve.
        :param cp2y: The y coordinate for the second control point of the Bézier curve.
        :param x: The x coordinate for the end point.
        :param y: The y coordinate for the end point.
        :returns: The ``BezierCurveTo`` :any:`DrawingObject` for the operation.
        """
        bezier_curve_to = BezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
        self.append(bezier_curve_to)
        return bezier_curve_to

    def quadratic_curve_to(
        self,
        cpx: float,
        cpy: float,
        x: float,
        y: float,
    ) -> QuadraticCurveTo:
        """Draw a quadratic curve in the canvas context.

        A quadratic curve requires two points. The first point is a control point; the
        second is the end point. The starting point of the curve is the last point in
        the current path, which can be changed using ``moveTo()`` before creating the
        quadratic curve.

        :param cpx: The x axis of the coordinate for the control point of the quadratic
            curve.
        :param cpy: The y axis of the coordinate for the control point of the quadratic
            curve.
        :param x: The x axis of the coordinate for the end point.
        :param y: The y axis of the coordinate for the end point.
        :returns: The ``QuadraticCurveTo`` :any:`DrawingObject` for the operation.
        """
        quadratic_curve_to = QuadraticCurveTo(cpx, cpy, x, y)
        self.append(quadratic_curve_to)
        return quadratic_curve_to

    def arc(
        self,
        x: float,
        y: float,
        radius: float,
        startangle: float = 0.0,
        endangle: float = 2 * pi,
        anticlockwise: bool = False,
    ) -> Arc:
        """Draw a circular arc in the canvas context.

        A full circle will be drawn by default; an arc can be drawn by specifying a
        start and end angle.

        :param x: The X coordinate of the circle's center.
        :param y: The Y coordinate of the circle's center.
        :param startangle: The start angle in radians, measured clockwise from the
            positive X axis.
        :param endangle: The end angle in radians, measured clockwise from the positive
            X axis.
        :param anticlockwise: If true, the arc is swept anticlockwise. The default is
            clockwise.
        :returns: The ``Arc`` :any:`DrawingObject` for the operation.
        """
        arc = Arc(x, y, radius, startangle, endangle, anticlockwise)
        self.append(arc)
        return arc

    def ellipse(
        self,
        x: float,
        y: float,
        radiusx: float,
        radiusy: float,
        rotation: float = 0.0,
        startangle: float = 0.0,
        endangle: float = 2 * pi,
        anticlockwise: bool = False,
    ) -> Ellipse:
        """Draw an elliptical arc in the canvas context.

        A full ellipse will be drawn by default; an arc can be drawn by specifying a
        start and end angle.

        :param x: The X coordinate of the ellipse's center.
        :param y: The Y coordinate of the ellipse's center.
        :param radiusx: The ellipse's horizontal axis radius.
        :param radiusy: The ellipse's vertical axis radius.
        :param rotation: The ellipse's rotation in radians, measured clockwise around
            its center.
        :param startangle: The start angle in radians, measured clockwise from the
            positive X axis.
        :param endangle: The end angle in radians, measured clockwise from the positive
            X axis.
        :param anticlockwise: If true, the arc is swept anticlockwise. The default is
            clockwise.
        :returns: The ``Ellipse`` :any:`DrawingObject` for the operation.
        """
        ellipse = Ellipse(
            x,
            y,
            radiusx,
            radiusy,
            rotation,
            startangle,
            endangle,
            anticlockwise,
        )
        self.append(ellipse)
        return ellipse

    def rect(self, x: float, y: float, width: float, height: float) -> Rect:
        """Draw a rectangle in the canvas context.

        :param x: The horizontal coordinate of the left of the rectangle.
        :param y: The vertical coordinate of the top of the rectangle.
        :param width: The width of the rectangle.
        :param height: The height of the rectangle.
        :returns: The ``Rect`` :any:`DrawingObject` for the operation.
        """
        rect = Rect(x, y, width, height)
        self.append(rect)
        return rect

    def fill(
        self,
        color: str = BLACK,
        fill_rule: FillRule = FillRule.NONZERO,
        preserve: None = None,  # DEPRECATED
    ) -> Fill:
        """Fill the current path.

        The fill can use either the `Non-Zero
        <https://en.wikipedia.org/wiki/Nonzero-rule>`__ or `Even-Odd
        <https://en.wikipedia.org/wiki/Even-odd_rule>`__ winding rule for filling paths.

        :param fill_rule: `nonzero` is the non-zero winding rule; `evenodd` is the
            even-odd winding rule.
        :param color: The fill color.
        :param preserve: **DEPRECATED**: this argument has no effect.
        :returns: The ``Fill`` :any:`DrawingObject` for the operation.
        """
        if preserve is not None:
            warnings.warn(
                "The `preserve` argument on fill() has been deprecated.",
                DeprecationWarning,
            )

        fill = Fill(color, fill_rule)
        self.append(fill)
        return fill

    def stroke(
        self,
        color: str = BLACK,
        line_width: float = 2.0,
        line_dash: list[float] | None = None,
    ) -> Stroke:
        """Draw the current path as a stroke.

        :param color: The color for the stroke.
        :param line_width: The width of the stroke.
        :param line_dash: The dash pattern to follow when drawing the line, expressed as
            alternating lengths of dashes and spaces. The default is a solid line.
        :returns: The ``Stroke`` :any:`DrawingObject` for the operation.
        """
        stroke = Stroke(color, line_width, line_dash)
        self.append(stroke)
        return stroke

    ###########################################################################
    # Text drawing
    ###########################################################################

    def write_text(
        self,
        text: str,
        x: float = 0.0,
        y: float = 0.0,
        font: Font | None = None,
        baseline: Baseline = Baseline.ALPHABETIC,
    ) -> WriteText:
        """Write text at a given position in the canvas context.

        Drawing text is effectively a series of path operations, so the text will have
        the color and fill properties of the canvas context.

        :param text: The text to draw. Newlines will cause line breaks, but long lines
            will not be wrapped.
        :param x: The X coordinate of the text's left edge.
        :param y: The Y coordinate: its meaning depends on ``baseline``.
        :param font: The font in which to draw the text. The default is the system font.
        :param baseline: Alignment of text relative to the Y coordinate.
        :returns: The ``WriteText`` :any:`DrawingObject` for the operation.
        """
        write_text = WriteText(text, x, y, font, baseline)
        self.append(write_text)
        return write_text

    ###########################################################################
    # Transformations
    ###########################################################################
    def rotate(self, radians: float) -> Rotate:
        """Add a rotation to the canvas context.

        :param radians: The angle to rotate clockwise in radians.
        :returns: The ``Rotate`` :any:`DrawingObject` for the transformation.
        """
        rotate = Rotate(radians)
        self.append(rotate)
        return rotate

    def scale(self, sx: float, sy: float) -> Scale:
        """Add a scaling transformation to the canvas context.

        :param sx: Scale factor for the X dimension. A negative value flips the
            image horizontally.
        :param sy: Scale factor for the Y dimension. A negative value flips the
            image vertically.
        :returns: The ``Scale`` :any:`DrawingObject` for the transformation.
        """
        scale = Scale(sx, sy)
        self.append(scale)
        return scale

    def translate(self, tx: float, ty: float) -> Translate:
        """Add a translation to the canvas context.

        :param tx: Translation for the X dimension.
        :param ty: Translation for the Y dimension.
        :returns: The ``Translate`` :any:`DrawingObject` for the transformation.
        """
        translate = Translate(tx, ty)
        self.append(translate)
        return translate

    def reset_transform(self) -> ResetTransform:
        """Reset all transformations in the canvas context.

        :returns: A ``ResetTransform`` :any:`DrawingObject`.
        """
        reset_transform = ResetTransform()
        self.append(reset_transform)
        return reset_transform

    ###########################################################################
    # Subcontexts of this context
    ###########################################################################

    @contextmanager
    def Context(self) -> Iterator[Context]:
        """Construct and yield a new sub-:class:`~toga.widgets.canvas.Context` within
        this context.

        :yields: The new :class:`~toga.widgets.canvas.Context` object.
        """
        context = Context(canvas=self._canvas)
        self.append(context)
        yield context
        self.redraw()

    @contextmanager
    def ClosedPath(
        self,
        x: float | None = None,
        y: float | None = None,
    ) -> Iterator[ClosedPathContext]:
        """Construct and yield a new :class:`~toga.widgets.canvas.ClosedPath`
        sub-context that will draw a closed path, starting from an origin.

        This is a context manager; it creates a new path and moves to the start
        coordinate; when the context exits, the path is closed. For fine-grained control
        of a path, you can use :meth:`~toga.widgets.canvas.Context.begin_path` and
        :meth:`~toga.widgets.canvas.Context.close_path`.

        :param x: The x coordinate of the path's starting point.
        :param y: The y coordinate of the path's starting point.
        :yields: The :class:`~toga.widgets.canvas.ClosedPathContext` context object.
        """
        closed_path = ClosedPathContext(canvas=self.canvas, x=x, y=y)
        self.append(closed_path)
        yield closed_path

    @contextmanager
    def Fill(
        self,
        x: float | None = None,
        y: float | None = None,
        color: str = BLACK,
        fill_rule: FillRule = FillRule.NONZERO,
    ) -> Iterator[FillContext]:
        """Construct and yield a new :class:`~toga.widgets.canvas.Fill` sub-context
        within this context.

        This is a context manager; it creates a new path, and moves to the start
        coordinate; when the context exits, the path is closed with a fill. For
        fine-grained control of a path, you can use
        :class:`~toga.widgets.canvas.Context.begin_path`,
        :class:`~toga.widgets.canvas.Context.move_to`,
        :class:`~toga.widgets.canvas.Context.close_path` and
        :class:`~toga.widgets.canvas.Context.fill`.

        If both an x and y coordinate is provided, the drawing context will begin with
        a ``move_to`` operation in that context.

        :param x: The x coordinate of the path's starting point.
        :param y: The y coordinate of the path's starting point.
        :param fill_rule: `nonzero` is the non-zero winding rule; `evenodd` is the
            even-odd winding rule.
        :param color: The fill color.
        :yields: The new :class:`~toga.widgets.canvas.FillContext` context object.
        """
        fill = FillContext(
            canvas=self.canvas,
            x=x,
            y=y,
            color=color,
            fill_rule=fill_rule,
        )
        self.append(fill)
        yield fill

    @contextmanager
    def Stroke(
        self,
        x: float | None = None,
        y: float | None = None,
        color: str = BLACK,
        line_width: float = 2.0,
        line_dash: list[float] | None = None,
    ) -> Iterator[StrokeContext]:
        """Construct and yield a new :class:`~toga.widgets.canvas.Stroke` sub-context
        within this context.

        This is a context manager; it creates a new path, and moves to the start
        coordinate; when the context exits, the path is closed with a stroke. For
        fine-grained control of a path, you can use
        :class:`~toga.widgets.canvas.Context.begin_path`,
        :class:`~toga.widgets.canvas.Context.move_to`,
        :class:`~toga.widgets.canvas.Context.close_path` and
        :class:`~toga.widgets.canvas.Context.stroke`.

        If both an x and y coordinate is provided, the drawing context will begin with
        a ``move_to`` operation in that context.

        :param x: The x coordinate of the path's starting point.
        :param y: The y coordinate of the path's starting point.
        :param color: The color for the stroke.
        :param line_width: The width of the stroke.
        :param line_dash: The dash pattern to follow when drawing the line. Default is a
            solid line.
        :yields: The new :class:`~toga.widgets.canvas.StrokeContext` context object.
        """
        stroke = StrokeContext(
            canvas=self.canvas,
            x=x,
            y=y,
            color=color,
            line_width=line_width,
            line_dash=line_dash,
        )
        self.append(stroke)
        yield stroke

    ###########################################################################
    # 2023-07 Backwards incompatibility
    ###########################################################################

    def new_path(self) -> BeginPath:
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.begin_path`."""
        warnings.warn(
            "Context.new_path() has been renamed Context.begin_path()",
            DeprecationWarning,
        )
        return self.begin_path()

    def context(self):
        """**DEPRECATED** - use :meth:`~toga.widgets.canvas.Context.Context`"""
        warnings.warn(
            "Context.context() has been renamed Context.Context()", DeprecationWarning
        )
        return self.Context()

    def closed_path(self, x: float, y: float):
        """**DEPRECATED** - use :meth:`~toga.widgets.canvas.Context.ClosedPath`"""
        warnings.warn(
            "Context.closed_path() has been renamed Context.ClosedPath()",
            DeprecationWarning,
        )
        return self.ClosedPath(x, y)


class ClosedPathContext(Context):
    """A drawing context that will build a closed path, starting from an
    origin.

    This is a context manager; it creates a new path and moves to the start coordinate;
    when the context exits, the path is closed. For fine-grained control of a path, you
    can use :class:`~toga.widgets.canvas.Context.begin_path`,
    :class:`~toga.widgets.canvas.Context.move_to` and
    :class:`~toga.widgets.canvas.Context.close_path`.

    If both an x and y coordinate is provided, the drawing context will begin with
    a ``move_to`` operation in that context.

    You should not create a :class:`~toga.widgets.canvas.ClosedPathContext` context
    directly; instead, you should use the
    :meth:`~toga.widgets.canvas.Context.ClosedPath` method on an existing context.
    """

    def __init__(
        self,
        canvas: toga.Canvas,
        x: float | None = None,
        y: float | None = None,
    ):
        super().__init__(canvas=canvas)
        self.x = x
        self.y = y

    def __repr__(self) -> str:
        return f"{self.__class__.__name__}(x={self.x}, y={self.y})"

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        """Used by parent to draw all objects that are part of the context."""
        impl.push_context(**kwargs)
        impl.begin_path(**kwargs)
        if self.x is not None and self.y is not None:
            impl.move_to(x=self.x, y=self.y, **kwargs)

        sub_kwargs = kwargs.copy()
        for obj in self.drawing_objects:
            obj._draw(impl, **sub_kwargs)

        impl.close_path(**kwargs)
        impl.pop_context(**kwargs)


class FillContext(ClosedPathContext):
    """A drawing context that will apply a fill to any paths all objects in the
    context.

    The fill can use either the `Non-Zero
    <https://en.wikipedia.org/wiki/Nonzero-rule>`__ or `Even-Odd
    <https://en.wikipedia.org/wiki/Even-odd_rule>`__ winding rule for filling paths.

    This is a context manager; it creates a new path, and moves to the start coordinate;
    when the context exits, the path is closed with a fill. For fine-grained control of
    a path, you can use :class:`~toga.widgets.canvas.Context.begin_path`,
    :class:`~toga.widgets.canvas.Context.move_to`,
    :class:`~toga.widgets.canvas.Context.close_path` and
    :class:`~toga.widgets.canvas.Context.fill`.

    If both an x and y coordinate is provided, the drawing context will begin with
    a ``move_to`` operation in that context.

    You should not create a :class:`~toga.widgets.canvas.FillContext` context directly;
    instead, you should use the :meth:`~toga.widgets.canvas.Context.Fill` method on an
    existing context.
    """

    def __init__(
        self,
        canvas: toga.Canvas,
        x: float | None = None,
        y: float | None = None,
        color: str = BLACK,
        fill_rule: FillRule = FillRule.NONZERO,
    ):
        super().__init__(canvas=canvas, x=x, y=y)
        self.color = color
        self.fill_rule = fill_rule

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(x={self.x}, y={self.y}, "
            f"color={self.color!r}, fill_rule={self.fill_rule})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.push_context(**kwargs)
        impl.begin_path(**kwargs)
        if self.x is not None and self.y is not None:
            impl.move_to(x=self.x, y=self.y, **kwargs)

        sub_kwargs = kwargs.copy()
        sub_kwargs.update(fill_color=self.color, fill_rule=self.fill_rule)
        for obj in self.drawing_objects:
            obj._draw(impl, **sub_kwargs)

        # Fill passes fill_rule to its children; but that is also a valid argument for
        # fill(), so if a fill context is a child of a fill context, there's an argument
        # collision. Duplicate the kwargs and explicitly overwrite to avoid the
        # collision.
        draw_kwargs = kwargs.copy()
        draw_kwargs.update(fill_rule=self.fill_rule)
        impl.fill(self.color, **draw_kwargs)

        impl.pop_context(**kwargs)

    @property
    def color(self) -> Color:
        """The fill color."""
        return self._color

    @color.setter
    def color(self, value: Color | str | None) -> None:
        if value is None:
            self._color = parse_color(BLACK)
        else:
            self._color = parse_color(value)


class StrokeContext(ClosedPathContext):
    """Construct a drawing context that will draw a stroke on all paths defined
    within the context.

    This is a context manager; it creates a new path, and moves to the start coordinate;
    when the context exits, the path is drawn with the stroke. For fine-grained control of
    a path, you can use :class:`~toga.widgets.canvas.Context.begin_path`,
    :class:`~toga.widgets.canvas.Context.move_to`,
    :class:`~toga.widgets.canvas.Context.close_path` and
    :class:`~toga.widgets.canvas.Context.stroke`.

    If both an x and y coordinate is provided, the drawing context will begin with
    a ``move_to`` operation in that context.

    You should not create a :class:`~toga.widgets.canvas.StrokeContext` context directly;
    instead, you should use the :meth:`~toga.widgets.canvas.Context.Stroke` method on
    an existing context.
    """

    def __init__(
        self,
        canvas: toga.Canvas,
        x: float | None = None,
        y: float | None = None,
        color: str | None = BLACK,
        line_width: float = 2.0,
        line_dash: list[float] | None = None,
    ):
        super().__init__(canvas=canvas, x=x, y=y)
        self.color = color
        self.line_width = line_width
        self.line_dash = line_dash

    def __repr__(self) -> str:
        return (
            f"{self.__class__.__name__}(x={self.x}, y={self.y}, color={self.color!r}, "
            f"line_width={self.line_width}, line_dash={self.line_dash!r})"
        )

    def _draw(self, impl: Any, **kwargs: Any) -> None:
        impl.push_context(**kwargs)
        impl.begin_path(**kwargs)

        if self.x is not None and self.y is not None:
            impl.move_to(x=self.x, y=self.y, **kwargs)

        sub_kwargs = kwargs.copy()
        sub_kwargs["stroke_color"] = self.color
        sub_kwargs["line_width"] = self.line_width
        sub_kwargs["line_dash"] = self.line_dash
        for obj in self.drawing_objects:
            obj._draw(impl, **sub_kwargs)

        # Stroke passes line_width and line_dash to its children; but those two are also
        # valid arguments for stroke, so if a stroke context is a child of stroke
        # context, there's an argument collision. Duplicate the kwargs and explicitly
        # overwrite to avoid the collision
        draw_kwargs = kwargs.copy()
        draw_kwargs["line_width"] = self.line_width
        draw_kwargs["line_dash"] = self.line_dash
        impl.stroke(self.color, **draw_kwargs)

        impl.pop_context(**kwargs)

    @property
    def color(self) -> Color:
        """The color of the stroke."""
        return self._color

    @color.setter
    def color(self, value: object) -> None:
        if value is None:
            self._color = parse_color(BLACK)
        else:
            self._color = parse_color(value)


#######################################################################################
# Events
#######################################################################################


class OnTouchHandler(Protocol):
    def __call__(self, widget: Canvas, x: int, y: int, /, **kwargs: Any) -> object:
        """A handler that will be invoked when a :any:`Canvas` is touched with a finger
        or mouse.

        :param widget: The canvas that was touched.
        :param x: X coordinate, relative to the left edge of the canvas.
        :param y: Y coordinate, relative to the top edge of the canvas.
        :param kwargs: Ensures compatibility with arguments added in future versions.
        """


class OnResizeHandler(Protocol):
    def __call__(
        self, widget: Canvas, /, width: int, height: int, **kwargs: Any
    ) -> object:
        """A handler that will be invoked when a :any:`Canvas` is resized.

        :param widget: The canvas that was resized.
        :param width: The new width.
        :param height: The new height.
        :param kwargs: Ensures compatibility with arguments added in future versions.
        """


#######################################################################################
# The Canvas Widget
#######################################################################################


class Canvas(Widget):
    _MIN_WIDTH = 0
    _MIN_HEIGHT = 0

    def __init__(
        self,
        id: str | None = None,
        style: StyleT | None = None,
        on_resize: OnResizeHandler | None = None,
        on_press: OnTouchHandler | None = None,
        on_activate: OnTouchHandler | None = None,
        on_release: OnTouchHandler | None = None,
        on_drag: OnTouchHandler | None = None,
        on_alt_press: OnTouchHandler | None = None,
        on_alt_release: OnTouchHandler | None = None,
        on_alt_drag: OnTouchHandler | None = None,
    ):
        """Create a new Canvas widget.

        Inherits from :class:`toga.Widget`.

        :param id: The ID for the widget.
        :param style: A style object. If no style is provided, a default style will be
            applied to the widget.
        :param on_resize: Initial :any:`on_resize` handler.
        :param on_press: Initial :any:`on_press` handler.
        :param on_activate: Initial :any:`on_activate` handler.
        :param on_release: Initial :any:`on_release` handler.
        :param on_drag: Initial :any:`on_drag` handler.
        :param on_alt_press: Initial :any:`on_alt_press` handler.
        :param on_alt_release: Initial :any:`on_alt_release` handler.
        :param on_alt_drag: Initial :any:`on_alt_drag` handler.
        """

        super().__init__(id=id, style=style)

        self._context = Context(canvas=self)

        # Create a platform specific implementation of Canvas
        self._impl = self.factory.Canvas(interface=self)

        # Set all the properties
        self.on_resize = on_resize
        self.on_press = on_press
        self.on_activate = on_activate
        self.on_release = on_release
        self.on_drag = on_drag
        self.on_alt_press = on_alt_press
        self.on_alt_release = on_alt_release
        self.on_alt_drag = on_alt_drag

    @property
    def enabled(self) -> Literal[True]:
        """Is the widget currently enabled? i.e., can the user interact with the widget?
        ScrollContainer widgets cannot be disabled; this property will always return
        True; any attempt to modify it will be ignored.
        """
        return True

    @enabled.setter
    def enabled(self, value: object) -> None:
        pass

    def focus(self) -> None:
        """No-op; ScrollContainer cannot accept input focus."""
        pass

    @property
    def context(self) -> Context:
        """The root context for the canvas."""
        return self._context

    def redraw(self) -> None:
        """Redraw the Canvas.

        The Canvas will be automatically redrawn after adding or removing a drawing
        object, or when the Canvas resizes. However, when you modify the properties of a
        drawing object, you must call ``redraw`` manually.
        """
        self._impl.redraw()

    def Context(self) -> ContextManager[Context]:
        """Construct and yield a new sub-:class:`~toga.widgets.canvas.Context` within
        the root context of this Canvas.

        :yields: The new :class:`~toga.widgets.canvas.Context` object.
        """
        return self.context.Context()

    def ClosedPath(
        self,
        x: float | None = None,
        y: float | None = None,
    ) -> ContextManager[ClosedPathContext]:
        """Construct and yield a new :class:`~toga.widgets.canvas.ClosedPathContext` context in
        the root context of this canvas.

        :param x: The x coordinate of the path's starting point.
        :param y: The y coordinate of the path's starting point.
        :yields: The new :class:`~toga.widgets.canvas.ClosedPathContext` context object.
        """
        return self.context.ClosedPath(x, y)

    def Fill(
        self,
        x: float | None = None,
        y: float | None = None,
        color: Color | str | None = BLACK,
        fill_rule: FillRule = FillRule.NONZERO,
    ) -> ContextManager[FillContext]:
        """Construct and yield a new :class:`~toga.widgets.canvas.FillContext` in the
        root context of this canvas.

        A drawing operator that fills the path constructed in the context according to
        the current fill rule.

        If both an x and y coordinate is provided, the drawing context will begin with
        a ``move_to`` operation in that context.

        :param x: The x coordinate of the path's starting point.
        :param y: The y coordinate of the path's starting point.
        :param fill_rule: `nonzero` is the non-zero winding rule; `evenodd` is the
            even-odd winding rule.
        :param color: The fill color.
        :yields: The new :class:`~toga.widgets.canvas.FillContext` context object.
        """
        return self.context.Fill(x, y, color, fill_rule)

    def Stroke(
        self,
        x: float | None = None,
        y: float | None = None,
        color: Color | str | None = BLACK,
        line_width: float = 2.0,
        line_dash: list[float] | None = None,
    ) -> ContextManager[StrokeContext]:
        """Construct and yield a new :class:`~toga.widgets.canvas.StrokeContext` in the
        root context of this canvas.

        If both an x and y coordinate is provided, the drawing context will begin with
        a ``move_to`` operation in that context.

        :param x: The x coordinate of the path's starting point.
        :param y: The y coordinate of the path's starting point.
        :param color: The color for the stroke.
        :param line_width: The width of the stroke.
        :param line_dash: The dash pattern to follow when drawing the line. Default is a
            solid line.
        :yields: The new :class:`~toga.widgets.canvas.StrokeContext` context object.
        """
        return self.context.Stroke(x, y, color, line_width, line_dash)

    @property
    def on_resize(self) -> OnResizeHandler:
        """The handler to invoke when the canvas is resized."""
        return self._on_resize

    @on_resize.setter
    def on_resize(self, handler: OnResizeHandler) -> None:
        self._on_resize = wrapped_handler(self, handler)

    @property
    def on_press(self) -> OnTouchHandler:
        """The handler invoked when the canvas is pressed. When a mouse is being used,
        this press will be with the primary (usually the left) mouse button."""
        return self._on_press

    @on_press.setter
    def on_press(self, handler: OnTouchHandler) -> None:
        self._on_press = wrapped_handler(self, handler)

    @property
    def on_activate(self) -> OnTouchHandler:
        """The handler invoked when the canvas is pressed in a way indicating the
        pressed object should be activated. When a mouse is in use, this will usually be
        a double click with the primary (usually the left) mouse button.

        This event is not supported on Android or iOS."""
        return self._on_activate

    @on_activate.setter
    def on_activate(self, handler: OnTouchHandler) -> None:
        self._on_activate = wrapped_handler(self, handler)

    @property
    def on_release(self) -> OnTouchHandler:
        """The handler invoked when a press on the canvas ends."""
        return self._on_release

    @on_release.setter
    def on_release(self, handler: OnTouchHandler) -> None:
        self._on_release = wrapped_handler(self, handler)

    @property
    def on_drag(self) -> OnTouchHandler:
        """The handler invoked when the location of a press changes."""
        return self._on_drag

    @on_drag.setter
    def on_drag(self, handler: OnTouchHandler) -> None:
        self._on_drag = wrapped_handler(self, handler)

    @property
    def on_alt_press(self) -> OnTouchHandler:
        """The handler to invoke when the canvas is pressed in an alternate
        manner. This will usually correspond to a secondary (usually the right) mouse
        button press.

        This event is not supported on Android or iOS.
        """
        return self._on_alt_press

    @on_alt_press.setter
    def on_alt_press(self, handler: OnTouchHandler) -> None:
        self._on_alt_press = wrapped_handler(self, handler)

    @property
    def on_alt_release(self) -> OnTouchHandler:
        """The handler to invoke when an alternate press is released.

        This event is not supported on Android or iOS.
        """
        return self._on_alt_release

    @on_alt_release.setter
    def on_alt_release(self, handler: OnTouchHandler) -> None:
        self._on_alt_release = wrapped_handler(self, handler)

    @property
    def on_alt_drag(self) -> OnTouchHandler:
        """The handler to invoke when the location of an alternate press changes.

        This event is not supported on Android or iOS.
        """
        return self._on_alt_drag

    @on_alt_drag.setter
    def on_alt_drag(self, handler: OnTouchHandler) -> None:
        self._on_alt_drag = wrapped_handler(self, handler)

    ###########################################################################
    # Text measurement
    ###########################################################################

    def measure_text(
        self,
        text: str,
        font: Font | None = None,
        tight: None = None,  # DEPRECATED
    ) -> tuple[float, float]:
        """Measure the size at which :meth:`~.Context.write_text` would render some text.

        :param text: The text to measure. Newlines will cause line breaks, but long
            lines will not be wrapped.
        :param font: The font in which to draw the text. The default is the system font.
        :param tight: **DEPRECATED**: this argument has no effect.
        :returns: A tuple of ``(width, height)``.
        """
        if tight is not None:
            warnings.warn(
                "The `tight` argument on Canvas.measure_text() has been deprecated.",
                DeprecationWarning,
            )
        if font is None:
            font = Font(family=SYSTEM, size=SYSTEM_DEFAULT_FONT_SIZE)

        return self._impl.measure_text(str(text), font._impl)

    ###########################################################################
    # As image
    ###########################################################################

    def as_image(self, format: type[ImageT] = toga.Image) -> ImageT:
        """Render the canvas as an image.

        :param format: Format to provide. Defaults to :class:`~toga.images.Image`; also
            supports :any:`PIL.Image.Image` if Pillow is installed, as well as any image
            types defined by installed :doc:`image format plugins
            </reference/plugins/image_formats>`
        :returns: The canvas as an image of the specified type.
        """
        return toga.Image(self._impl.get_image_data()).as_format(format)

    ###########################################################################
    # 2023-07 Backwards compatibility
    ###########################################################################

    def new_path(self):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.begin_path` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.begin_path()",
            DeprecationWarning,
        )
        return self.context.begin_path()

    def move_to(self, x, y):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.move_to` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.move_to()",
            DeprecationWarning,
        )
        return self.context.move_to(x, y)

    def line_to(self, x, y):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.line_to` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.line_to()",
            DeprecationWarning,
        )
        return self.context.line_to(x, y)

    def bezier_curve_to(self, cp1x, cp1y, cp2x, cp2y, x, y):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.bezier_curve_to` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.bezier_curve_to()",
            DeprecationWarning,
        )
        return self.context.bezier_curve_to(cp1x, cp1y, cp2x, cp2y, x, y)

    def quadratic_curve_to(self, cpx: float, cpy: float, x: float, y: float):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.quadratic_curve_to`
        on :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.quadratic_curve_to()",
            DeprecationWarning,
        )
        return self.context.quadratic_curve_to(cpx, cpy, x, y)

    def arc(self, x, y, radius, startangle=0.0, endangle=2 * pi, anticlockwise=False):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.arc` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.arc()",
            DeprecationWarning,
        )
        return self.context.arc(x, y, radius, startangle, endangle, anticlockwise)

    def ellipse(
        self,
        x: float,
        y: float,
        radiusx: float,
        radiusy: float,
        rotation: float = 0.0,
        startangle: float = 0.0,
        endangle: float = 2 * pi,
        anticlockwise: bool = False,
    ):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.ellipse` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.ellipse()",
            DeprecationWarning,
        )
        return self.context.ellipse(
            x,
            y,
            radiusx,
            radiusy,
            rotation,
            startangle,
            endangle,
            anticlockwise,
        )

    def rect(self, x: float, y: float, width: float, height: float):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.rect` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.rect()",
            DeprecationWarning,
        )
        return self.context.rect(x, y, width, height)

    def write_text(self, text: str, x=0, y=0, font=None):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.write_text` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.write_text()",
            DeprecationWarning,
        )
        return self.context.write_text(text, x, y, font)

    def rotate(self, radians: float):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.rotate` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.rotate()",
            DeprecationWarning,
        )
        return self.context.rotate(radians)

    def scale(self, sx: float, sy: float):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.scale` on :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.scale()",
            DeprecationWarning,
        )
        return self.context.scale(sx, sy)

    def translate(self, tx: float, ty: float):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.translate` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.translate()",
            DeprecationWarning,
        )
        return self.context.translate(tx, ty)

    def reset_transform(self):
        """**DEPRECATED** - Use :meth:`~toga.widgets.canvas.Context.reset_transform` on
        :attr:`context`"""
        warnings.warn(
            "Direct canvas operations have been deprecated; use context.reset_transform()",
            DeprecationWarning,
        )
        return self.context.reset_transform()

    def closed_path(self, x, y):
        """**DEPRECATED** - use :meth:`~toga.Canvas.ClosedPath`"""
        warnings.warn(
            "Canvas.closed_path() has been renamed Canvas.ClosedPath()",
            DeprecationWarning,
        )
        return self.ClosedPath(x, y)

    def fill(
        self,
        color: Color | str | None = BLACK,
        fill_rule: FillRule = FillRule.NONZERO,
        preserve=None,  # DEPRECATED
    ):
        """**DEPRECATED** - use :meth:`~toga.Canvas.Fill`"""
        warnings.warn(
            "Canvas.fill() has been renamed Canvas.Fill()",
            DeprecationWarning,
        )
        if preserve is not None:
            warnings.warn(
                "The `preserve` argument on fill() has been deprecated.",
                DeprecationWarning,
            )
        return self.Fill(color=color, fill_rule=fill_rule)

    def stroke(
        self,
        color: Color | str | None = BLACK,
        line_width: float = 2.0,
        line_dash: list[float] | None = None,
    ):
        """**DEPRECATED** - use :meth:`~toga.Canvas.Stroke`"""
        warnings.warn(
            "Canvas.stroke() has been renamed Canvas.Stroke().",
            DeprecationWarning,
        )
        return self.Stroke(color=color, line_width=line_width, line_dash=line_dash)


def sweepangle(startangle: float, endangle: float, anticlockwise: bool) -> float:
    """Returns an arc length in the range [-2 * pi, 2 * pi], where positive numbers are
    clockwise. Based on the "ellipse method steps" in the HTML spec."""

    if anticlockwise:
        if endangle - startangle <= -2 * pi:
            return -2 * pi
    else:
        if endangle - startangle >= 2 * pi:
            return 2 * pi

    startangle %= 2 * pi
    endangle %= 2 * pi
    sweepangle = endangle - startangle
    if anticlockwise:
        if sweepangle > 0:
            sweepangle -= 2 * pi
    else:
        if sweepangle < 0:
            sweepangle += 2 * pi

    return sweepangle


# Based on https://stackoverflow.com/a/30279817
def arc_to_bezier(sweepangle: float) -> list[tuple[float, float]]:
    """Approximates an arc of a unit circle as a sequence of Bezier segments.

    :param sweepangle: Length of the arc in radians, where positive numbers are
        clockwise.
    :returns: [(1, 0), (cp1x, cp1y), (cp2x, cp2y), (x, y), ...], where each group of 3
        points has the same meaning as in the bezier_curve_to method, and there are
        between 1 and 4 groups."""

    matrices = [
        [1, 0, 0, 1],  # 0 degrees
        [0, -1, 1, 0],  # 90
        [-1, 0, 0, -1],  # 180
        [0, 1, -1, 0],  # 270
    ]

    if sweepangle < 0:  # Anticlockwise
        sweepangle *= -1
        for matrix in matrices:
            matrix[2] *= -1
            matrix[3] *= -1

    result = [(1.0, 0.0)]
    for matrix in matrices:
        if sweepangle < 0:
            break

        phi = min(sweepangle, pi / 2)
        k = 4 / 3 * tan(phi / 4)
        result += [
            transform(x, y, matrix)
            for x, y in [
                (1, k),
                (cos(phi) + k * sin(phi), sin(phi) - k * cos(phi)),
                (cos(phi), sin(phi)),
            ]
        ]

        sweepangle -= pi / 2

    return result


def transform(x: float, y: float, matrix: list[int]) -> tuple[float, float]:
    return (
        x * matrix[0] + y * matrix[1],
        x * matrix[2] + y * matrix[3],
    )
