Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
fbc73d5
Remove backreference, combine drawing actions, move methods
HalfWhitt Feb 3, 2026
3b5ef7f
Add change note
HalfWhitt Feb 3, 2026
45eecae
Fix coverage for 3.14
HalfWhitt Feb 3, 2026
592fbde
Switch to WeakSet
HalfWhitt Feb 6, 2026
502b41f
Switch to platform-specific images
HalfWhitt Feb 6, 2026
743ad6c
Merge branch 'main' into canvas-switcheroo
HalfWhitt Feb 6, 2026
defc23a
Correct platform-specific image name
HalfWhitt Feb 6, 2026
4b64f14
Update new tests
HalfWhitt Feb 6, 2026
ab679ef
Merge branch 'main' into canvas-switcheroo
HalfWhitt Feb 7, 2026
2fc60da
Update new test
HalfWhitt Feb 7, 2026
29b26fb
Add Android reference image
HalfWhitt Feb 7, 2026
b84617e
Update Qt image
HalfWhitt Feb 7, 2026
241a6cd
Use resized version:
HalfWhitt Feb 8, 2026
37869c9
Cleanups & fixes
HalfWhitt Feb 8, 2026
0029a84
Fix link
HalfWhitt Feb 8, 2026
2b5a10f
Wording fix
HalfWhitt Feb 11, 2026
afb54b2
Add more explanation of State
HalfWhitt Feb 11, 2026
bb6b359
Simplify active state lookup
HalfWhitt Feb 20, 2026
ac3bca8
Raise exception when states are re-entered
HalfWhitt Feb 20, 2026
ec73572
Close context manages when adding an action.
HalfWhitt Feb 24, 2026
55e4680
Add _can_be_entered attribute
HalfWhitt Feb 24, 2026
127f155
More comprehensive contains testing
HalfWhitt Feb 25, 2026
ac3a0e9
Deprecate x and y in context managers
HalfWhitt Feb 26, 2026
1b47c46
Actually suppress warnings
HalfWhitt Feb 27, 2026
73a1baa
Merge branch 'main' into canvas-switcheroo
HalfWhitt Apr 12, 2026
6a16ded
Update pre-commit hook
HalfWhitt Apr 12, 2026
860881a
Accommodate Togat Chart ClosedPath usage
HalfWhitt Apr 12, 2026
51098e2
Fix tutorials, drawing_objects references, and changenote clarification
HalfWhitt Apr 14, 2026
44f02f1
Mark static web tests as flaky...
freakboy3742 Apr 15, 2026
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
7 changes: 6 additions & 1 deletion android/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ class CanvasProbe(SimpleProbe):
native_class = DrawHandlerView

def reference_variant(self, reference):
if reference in {"multiline_text", "write_text", "write_text_and_path"}:
if reference in {
"multiline_text",
"write_text",
"write_text_and_path",
"deprecated_tutorial",
}:
return f"{reference}-android"
return reference

Expand Down
6 changes: 6 additions & 0 deletions changes/4159.removal.md
Comment thread
HalfWhitt marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
There are a number of changes to the Canvas widget's drawing API:
- Drawing methods can now be called directly on a Canvas. Calling them on States is now deprecated.
- The CamelCase context-manager drawing methods (`ClosedPath`, `Fill`, and `Stroke`) are deprecated; they are now unified with their standalone counterparts (`close_path`, `fill`, and `stroke`, respectively). For example, the `fill` method (lowercase) can be used as a normal method or as `with canvas.fill():`.
- List-like methods on States are deprecated; manipulate their `State.drawing_actions` lists directly.
- `State.redraw()` is deprecated; call `Canvas.redraw()` instead.
- States no longer hold a reference to their Canvas; the `State.canvas` attribute is deprecated. (This is largely an internal detail, and unlikely to affect user code.)
7 changes: 6 additions & 1 deletion cocoa/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ def background_color(self):
return TRANSPARENT

def reference_variant(self, reference):
if reference in {"multiline_text", "write_text", "write_text_and_path"}:
if reference in {
"multiline_text",
"write_text",
"write_text_and_path",
"deprecated_tutorial",
}:
# System font and default size is platform dependent.
return f"{reference}-macOS"
return reference
Expand Down
2 changes: 1 addition & 1 deletion core/src/toga/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def __init__(
"""Construct a reference to a font.

This class should be used when an API requires an explicit font reference (e.g.
[`State.write_text`][toga.widgets.canvas.State.write_text]). In all other
[`Canvas.write_text`][toga.Canvas.write_text]). In all other
cases, fonts in Toga are controlled
using the style properties linked below.

Expand Down
26 changes: 14 additions & 12 deletions core/src/toga/widgets/canvas/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@
Arc,
BeginPath,
BezierCurveTo,
ClosePath,
DrawImage,
DrawingAction,
Ellipse,
Fill,
LineTo,
MoveTo,
QuadraticCurveTo,
Rect,
ResetTransform,
Rotate,
RoundRect,
Scale,
Stroke,
Translate,
WriteText,
)
from .geometry import arc_to_bezier, sweepangle
from .state import ClosedPathContext, FillContext, State, StrokeContext
from .state import ClosePath, Fill, State, Stroke

# Make sure deprecation warnings are shown by default
warnings.filterwarnings("default", category=DeprecationWarning)

_deprecated_names = {
# Jan 2026: DrawingAction was named DrawingObject, and State was named Context, in
# Toga 0.5.3 and earlier.
# 2026-02: The following have different names than they did in Toga 0.5.3 and
# earlier.
"DrawingObject": DrawingAction,
"Context": State,
# No one should be using these directly anyway, but just in case...
"ClosedPathContext": ClosePath,
"FillContext": Fill,
"StrokeContext": Stroke,
}


Expand All @@ -54,24 +57,23 @@ def __getattr__(name):
"Arc",
"BeginPath",
"BezierCurveTo",
"ClosePath",
"DrawImage",
"Ellipse",
"Fill",
"LineTo",
"MoveTo",
"QuadraticCurveTo",
"Rect",
"ResetTransform",
"Rotate",
"RoundRect",
"Scale",
"Stroke",
"Translate",
"WriteText",
# States
"ClosedPathContext",
"State",
"FillContext",
"StrokeContext",
"Fill",
"Stroke",
"ClosePath",
# Geometry
"arc_to_bezier",
"sweepangle",
Expand Down
118 changes: 27 additions & 91 deletions core/src/toga/widgets/canvas/canvas.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
from __future__ import annotations

import warnings
from contextlib import AbstractContextManager as ContextManager
from typing import (
TYPE_CHECKING,
Any,
Literal,
Protocol,
)
from weakref import WeakSet

import toga
from toga.constants import FillRule
from toga.fonts import (
SYSTEM,
SYSTEM_DEFAULT_FONT_SIZE,
Expand All @@ -19,10 +18,9 @@
from toga.handlers import wrapped_handler

from ..base import StyleT, Widget
from .state import ClosedPathContext, FillContext, State, StrokeContext
from .state import DrawingActionDispatch, State

if TYPE_CHECKING:
from toga.colors import ColorT
from toga.images import ImageT

# Make sure deprecation warnings are shown by default
Expand Down Expand Up @@ -52,10 +50,13 @@ def __call__(self, widget: Canvas, width: int, height: int, **kwargs: Any) -> No
"""


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

# 2026-02: Backwards compatibility for <= 0.5.3
_instances: WeakSet = WeakSet()

def __init__(
self,
id: str | None = None,
Expand Down Expand Up @@ -88,7 +89,7 @@ def __init__(
:param on_alt_drag: Initial [`on_alt_drag`][toga.Canvas.on_alt_drag] handler.
:param kwargs: Initial style properties.
"""
self._state = State(canvas=self)
self._state = State()

super().__init__(id, style, **kwargs)

Expand All @@ -102,6 +103,9 @@ def __init__(
self.on_alt_release = on_alt_release
self.on_alt_drag = on_alt_drag

# 2026-02: Backwards compatibility for <= 0.5.3
self._instances.add(self)

def _create(self) -> Any:
return self.factory.Canvas(interface=self)

Expand All @@ -126,6 +130,10 @@ def root_state(self) -> State:
"""The root state for the canvas."""
return self._state

######################################################################
# 2026-02: Backwards compatibility for <= 0.5.3
######################################################################

@property
def context(self) -> State:
warnings.warn(
Expand All @@ -135,96 +143,24 @@ def context(self) -> State:
)
return self._state

######################################################################
# End backwards compatibility
######################################################################

@property
def _action_target(self):
"""Return the currently active state."""
return self.root_state._active_state

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.
The Canvas will be automatically redrawn after calling its drawing methods.
However, when you directly add, remove, or modify a drawing action, you must
call `redraw` manually.
"""
self._impl.redraw()

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

:return: Yields the new [`State`][toga.widgets.canvas.State] object.
"""
warnings.warn(
"Canvas.Context() is deprecated. Use Canvas.root_state.state() instead.",
DeprecationWarning,
stacklevel=2,
)
return self.root_state.state()

def ClosedPath(
self,
x: float | None = None,
y: float | None = None,
) -> ContextManager[ClosedPathContext]:
"""Construct and yield a new
[`ClosedPathContext`][toga.widgets.canvas.ClosedPathContext]
state in the root state 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.
:return: Yields the new
[`ClosedPathContext`][toga.widgets.canvas.ClosedPathContext] state object.
"""
return self.root_state.ClosedPath(x, y)

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

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

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

: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.
:return class: Yields the new [`FillContext`][toga.widgets.canvas.FillContext]
state object.
"""
return self.root_state.Fill(x, y, color, fill_rule)

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

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

: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.
:return: Yields the new
[`StrokeContext`][toga.widgets.canvas.StrokeContext] state object.
"""
return self.root_state.Stroke(x, y, color, line_width, line_dash)

@property
def on_resize(self) -> OnResizeHandler:
"""The handler to invoke when the canvas is resized."""
Expand Down Expand Up @@ -320,7 +256,7 @@ def measure_text(
line_height: float | None = None,
) -> tuple[float, float]:
"""Measure the size at which
[`State.write_text`][toga.widgets.canvas.State.write_text]
[`Canvas.write_text`][toga.Canvas.write_text]
would render some text.

:param text: The text to measure. Newlines will cause line breaks, but long
Expand Down
Loading