For many if not most applications, the interface documented for Canvas is sufficient. However, it is also possible to directly manipulate a Canvas's stored instructions, thereby modifying the results in a non-linear fashion.
Each drawing operation that has been performed on a Canvas is represented by an object of class DrawingAction. Each drawing method has a corresponding DrawingAction subclass of the same name, except in CamelCase. For example, the line_to() method creates a LineTo object, and LineTo is a subclass of DrawingAction.
The current state of the drawing context is represented by a state object. All states are subclasses of the abstract BaseState; the simplest is State. Initially, a canvas has only one state; this initial state is always accessible via the canvas's root_state attribute.
When you save and then restore the state of the drawing context using a context manager (e.g., state(), stroke(), or fill()), a new state object is created and inserted into the currently active state's drawing_actions. This is possible because BaseState is itself a subclass of DrawingAction.
A state's drawing actions are a list, and can be accessed using list syntax. For example, if you wanted to access the Fill object in the previous example, you could use:
fill=canvas.root_state.drawing_actions[3]
However, this is not very practical, especially if the action of interest is nested within several states. A better way is to leverage the fact that each drawing method returns its drawing action. The line calling the fill method could be modified to:
fill=canvas.fill()
And now fill is a direct reference to the Fill object. This Fill object can then be modified as required.
The same is true even when a method is being used as a context manager, using with ... as ... syntax. For instance, the following code would bind fill, stroke, and move_to to the Fill, Stroke, and MoveTo drawing actions created by the methods called:
DrawingAction objects also have attributes corresponding to the equivalent method's parameters. If you modify these attributes, it will retroactively alter what is drawn on the canvas. For example, consider the following code and its output:
Since we've saved references, we can alter the parameters of the stroke and the first line segment. After altering attributes like this, the canvas's redraw() method must be called to ensure the results are rendered on screen.
DrawingAction objects can also be created directly, and the list of DrawingAction objects on a state can be manually altered. As with altering attributes, and direct modification of the lists of drawing actions should be followed by a call to the canvas's redraw method.
An extra point could be added to the above path like so:
An updated stroke with an additional line segment, after calling redraw() on the canvas.
This example uses insert, but drawing_actions is a list, with all of a list's normal methods, including append, remove, and extend. Remember to call redraw after any such alterations.
Every canvas drawing method creates a DrawingAction, adds it to the currently
active state, and returns it. Each argument passed to the method becomes a property
of the DrawingAction, which can be modified as shown in
Modifying attributes of Drawing actions.
A DrawingAction can also be
created manually. Their constructors take
the same arguments as the corresponding Canvas drawing method, and
their classes have the same names, but capitalized.
Source code in core/src/toga/widgets/canvas/drawingaction.py
classDrawingAction(ABC):"""A [`Canvas`][toga.Canvas] drawing operation. Every canvas drawing method creates a `DrawingAction`, adds it to the currently active state, and returns it. Each argument passed to the method becomes a property of the `DrawingAction`, which can be modified as shown in [Modifying attributes of Drawing actions][]. A `DrawingAction` can also be [created manually][creating-and-adding-new-drawing-actions]. Their constructors take the same arguments as the corresponding [`Canvas`][toga.Canvas] drawing method, and their classes have the same names, but capitalized. """def__repr__(self)->str:ifis_dataclass(self):str_fields=[]forfieldinfields(self):matchvalue:=getattr(self,field.name):casefloat():str_value=f"{value:.3f}"caseEnum():str_value=str(value)case_:str_value=repr(value)str_fields.append(f"{field.name}={str_value}")parenthetical=", ".join(str_fields)else:parenthetical=""returnf"{type(self).__name__}({parenthetical})"@abstractmethoddef_draw(self,context:Any)->None:"""Called by parent state to execute this drawing action."""def__contains__(self,other:DrawingAction):returnhasattr(self,"drawing_actions")andany(actionisotherorotherinactionforactioninself.drawing_actions)
classBaseState(DrawingAction,DrawingActionDispatch,ABC):"""A base class for all drawing actions that can function as state-saving context managers. """drawing_actions:list[DrawingAction]"""The list of all drawing actions contained by this state. If you add or remove drawing actions to this list, you'll need to call [`Canvas.redraw()`][toga.Canvas.redraw] for the changes to be rendered. """def__init__(self):self.drawing_actions=[]self._can_be_entered=True@abstractmethoddef_draw(self,context:Any)->None:...@propertydef_action_target(self):# State itself holds its drawing actions.returnself@propertydef_active_state(self):"""Return the currently active state, either this or a sub-state."""ifself.drawing_actions:# If a sub-state is active, it must be the last action in the list;# subsequent actions would be added to that sub-state (or a sub-state of# it).last=self.drawing_actions[-1]ifgetattr(last,"_is_open",False):returnlast._active_statereturnselfdef__enter__(self):ifnotself._can_be_entered:raiseRuntimeError("A Canvas context manager can only be entered once, and only before ""any subsequent drawing actions are added.")self._is_open=Trueself._can_be_entered=Falsereturnselfdef__exit__(self,exc_type,exc_val,exc_tb):self._is_open=False# Don't suppress any exceptionsreturnFalse########################################################################### 2026-04: Backwards compatibility for <= 0.5.3########################################################################### These preserve the old signature, and warn about the new one.deffill(self,color:ColorT|None|object=NOT_PROVIDED,fill_rule:FillRule=FillRule.NONZERO,)->AbstractContextManager[Fill]:fill=Fill(fill_rule=fill_rule,fill_style=color)self._add_to_target(fill)warnings.warn(("Calling drawing methods on a state is deprecated. To add actions ""to the currently active state, call drawing methods on the canvas. ""Additionally, the Canvas.fill() method's color parameter can only be ""provided via keyword. fill_rule is the only argument it accepts ""positionally."),DeprecationWarning,stacklevel=2,)self._redraw_without_warning()returnfilldefstroke(self,color:ColorT|None|NOT_PROVIDED=NOT_PROVIDED,line_width:float|None=None,line_dash:list[float]|None=None,)->AbstractContextManager[Stroke]:stroke=Stroke(stroke_style=color,line_width=line_width,line_dash=line_dash)self._add_to_target(stroke)warnings.warn(("Calling drawing methods on a state is deprecated. To add actions ""to the currently active state, call drawing methods on the canvas. ""Additionally, the Canvas.stroke() method's arguments can only be ""provided as keywords. It does not accept any positional arguments."),DeprecationWarning,stacklevel=2,)self._redraw_without_warning()returnstroke############################################################################ 2026-02: Backwards compatibility for Toga <= 0.5.3###########################################################################def__len__(self)->int:self._warn_list_methods()returnlen(self.drawing_actions)def__getitem__(self,index:int)->DrawingAction:self._warn_list_methods()returnself.drawing_actions[index]defappend(self,obj:DrawingAction)->None:self._warn_list_methods()self.drawing_actions.append(obj)self._redraw_without_warning()definsert(self,index:int,obj:DrawingAction)->None:self._warn_list_methods()self.drawing_actions.insert(index,obj)self._redraw_without_warning()defremove(self,obj:DrawingAction)->None:self._warn_list_methods()self.drawing_actions.remove(obj)self._redraw_without_warning()defclear(self)->None:self._warn_list_methods()self.drawing_actions.clear()self._redraw_without_warning()@propertydefcanvas(self)->Canvas:warnings.warn("States no longer hold a reference to their canvas.",DeprecationWarning,stacklevel=2,)from.canvasimportCanvas# Get the first that matches.forcanvasinCanvas._instances:ifselfiscanvas.root_stateorselfincanvas.root_state:returncanvasreturnNonedefredraw(self)->None:warnings.warn((f"{type(self).__name__}.redraw() is deprecated. Call the canvas's ""redraw() method instead."),DeprecationWarning,stacklevel=2,)from.canvasimportCanvas# Redraw any canvases that contain self; could be multiple.forcanvasinCanvas._instances:ifselfiscanvas.root_stateorselfincanvas.root_state:canvas.redraw()def_warn_list_methods(self)->None:warnings.warn(("A state's list-like methods (append, insert, remove, and clear), as ""well as implementing len() and indexing, are deprecated. Manipulate ""its drawing_actions directly, and then call redraw() on the canvas."),DeprecationWarning,stacklevel=3,)
Source code in core/src/toga/widgets/canvas/drawingaction.py
147148149150151152153154155156
@dataclass(repr=False)classSetFillStyle(DrawingAction):"""The [`DrawingAction`][toga.widgets.canvas.DrawingAction] representing assigning to the [fill_style][toga.Canvas.fill_style] context attribute. """fill_style:ColorT=color_property()def_draw(self,context:Any)->None:context.set_fill_style(self.fill_style)
Source code in core/src/toga/widgets/canvas/drawingaction.py
159160161162163164165166167168
@dataclass(repr=False)classSetStrokeStyle(DrawingAction):"""The [`DrawingAction`][toga.widgets.canvas.DrawingAction] representing assigning to the [stroke_style][toga.Canvas.stroke_style] context attribute. """stroke_style:ColorT=color_property()def_draw(self,context:Any)->None:context.set_stroke_style(self.stroke_style)
Source code in core/src/toga/widgets/canvas/drawingaction.py
183184185186187188189190191192
@dataclass(repr=False)classSetLineWidth(DrawingAction):"""The [`DrawingAction`][toga.widgets.canvas.DrawingAction] representing assigning to the [line_width][toga.Canvas.line_width] context attribute. """line_width:floatdef_draw(self,context:Any)->None:context.set_line_width(self.line_width)
Source code in core/src/toga/widgets/canvas/drawingaction.py
171172173174175176177178179180
@dataclass(repr=False)classSetLineDash(DrawingAction):"""The [`DrawingAction`][toga.widgets.canvas.DrawingAction] representing assigning to the [line_dash][toga.Canvas.line_dash] context attribute. """line_dash:list[float]def_draw(self,context:Any)->None:context.set_line_dash(self.line_dash)
Source code in core/src/toga/widgets/canvas/state.py
866867868869870871872873874875876877878879
@dataclass(repr=False)classState(BaseState):"""The [DrawingAction][toga.widgets.canvas.DrawingAction] representing the [stateh()][toga.Canvas.state] method. Functions as a context manager. """def__post_init__(self):super().__init__()def_draw(self,context:Any)->None:context.save()foractioninself.drawing_actions:action._draw(context)context.restore()
@dataclass(repr=False)classClosePath(BaseState):"""The [DrawingAction][toga.widgets.canvas.DrawingAction] representing the [close_path()][toga.Canvas.close_path] method. Can function as a context manager. """def__post_init__(self):super().__init__()# Backwards compatibility for Toga <= 0.5.4# See DrawingActionDispatch.ClosedPath for explanationdef__enter__(self):super().__enter__()ifhasattr(self,"x")andhasattr(self,"y"):self.drawing_actions.append(MoveTo(self.x,self.y))returnself# End backwards compatibilitydef_draw(self,context:Any)->None:ifnot(hasattr(self,"_is_open")orself.drawing_actions):# Wasn't used as a context manager, nor had drawing actions manually added# 4-2026: Backwards compatibility for Toga <= 0.5.4# See DrawingActionDispatch.ClosedPath for explanationifhasattr(self,"x")andhasattr(self,"y"):context.move_to(self.x,self.y)# End backwards compatibilitycontext.close_path()returncontext.save()context.begin_path()foractioninself.drawing_actions:action._draw(context)context.close_path()context.restore()
@dataclass(repr=False)classArc(DrawingAction):"""The [`DrawingAction`][toga.widgets.canvas.DrawingAction] representing the [arc()][toga.Canvas.arc] method. """x:floaty:floatradius:floatstartangle:float=0.0endangle:float=2*picounterclockwise:bool|None=Noneanticlockwise:InitVar[bool|None]=None# DEPRECATED####################################################################### 03-2025: Backwards compatibility for Toga <= 0.5.1######################################################################def__post_init__(self,anticlockwise):self.counterclockwise=_determine_counterclockwise(anticlockwise,self.counterclockwise)####################################################################### End backwards compatibility######################################################################def_draw(self,context:Any)->None:context.arc(self.x,self.y,self.radius,self.startangle,self.endangle,self.counterclockwise,)
@dataclass(repr=False)classEllipse(DrawingAction):"""The [`DrawingAction`][toga.widgets.canvas.DrawingAction] representing the [ellipse()][toga.Canvas.ellipse] method. """x:floaty:floatradiusx:floatradiusy:floatrotation:float=0.0startangle:float=0.0endangle:float=2*picounterclockwise:bool|None=Noneanticlockwise:InitVar[bool|None]=None# DEPRECATED####################################################################### 03-2025: Backwards compatibility for Toga <= 0.5.1######################################################################def__post_init__(self,anticlockwise):self.counterclockwise=_determine_counterclockwise(anticlockwise,self.counterclockwise,)####################################################################### End backwards compatibility######################################################################def_draw(self,context:Any)->None:context.ellipse(self.x,self.y,self.radiusx,self.radiusy,self.rotation,self.startangle,self.endangle,self.counterclockwise,)
@dataclass(repr=False)classFill(BaseState):"""The [DrawingAction][toga.widgets.canvas.DrawingAction] representing the [fill()][toga.Canvas.fill] method. Can function as a context manager. """# This will need to change to a pair of positional arguments in order to accommodate# (path), (fill_rule), or (path, fill_rule) usage as in JavaScript.fill_rule:FillRule=FillRule.NONZERO_:KW_ONLYfill_style:ColorT|None|object=color_property()color:InitVar[ColorT|None|object]=color_property()def__post_init__(self,color):super().__init__()self.fill_style=_assign_style(self,"fill",color)def_draw(self,context:Any)->None:context.save()ifself.fill_styleisnotNone:context.set_fill_style(self.fill_style)ifhasattr(self,"_is_open")orself.drawing_actions:# Was used as a context manager (or had drawing actions manually added)context.in_fill=True# 4-2026: Backwards compatibility for Toga <= 0.5.3context.begin_path()foractioninself.drawing_actions:action._draw(context)context.in_fill=False# 4-2026: Backwards compatibility for Toga <= 0.5.3context.fill(self.fill_rule)context.restore()
@dataclass(repr=False)classStroke(BaseState):"""The [DrawingAction][toga.widgets.canvas.DrawingAction] representing the [stroke()][toga.Canvas.stroke] method. Can function as a context manager. """# Path parameter (positional/keyword) will go here._:KW_ONLYstroke_style:ColorT|None|object=color_property()color:InitVar[ColorT|None|object]=color_property()line_width:float|None=Noneline_dash:list[float]|None=Nonedef__post_init__(self,color):super().__init__()self.stroke_style=_assign_style(self,"stroke",color)def_draw(self,context:Any)->None:context.save()ifself.stroke_styleisnotNone:context.set_stroke_style(self.stroke_style)ifself.line_widthisnotNone:context.set_line_width(self.line_width)ifself.line_dashisnotNone:context.set_line_dash(self.line_dash)ifhasattr(self,"_is_open")orself.drawing_actions:# Was used as a context manager (or had drawing actions manually added)context.in_stroke=True# Backwards compatibility for Toga <= 0.5.3context.begin_path()foractioninself.drawing_actions:action._draw(context)context.in_stroke=False# Backwards compatibility for Toga <= 0.5.3context.stroke()context.restore()