Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions android/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

class CanvasProbe(SimpleProbe):
native_class = DrawHandlerView
screenshot_reset_transform = False

def reference_variant(self, reference):
if reference in {"multiline_text", "write_text", "write_text_and_path"}:
Expand Down
1 change: 1 addition & 0 deletions changes/4044.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Canvas's `reset_transform` method now correctly resets the transformation on Mac and iOS, instead of clearing the current state.
20 changes: 19 additions & 1 deletion cocoa/src/toga_cocoa/libs/core_graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,26 @@ class CGAffineTransform(Structure):
("ty", CGFloat),
]

def __eq__(self, other):
try:
return (
self.a == other.a
and self.b == other.b
and self.c == other.c
and self.d == other.d
and self.tx == other.tx
and self.ty == other.ty
)
except AttributeError:
return False


CGAffineTransformIdentity = CGAffineTransform.in_dll(
core_graphics, "CGAffineTransformIdentity"
)

core_graphics.CGAffineTransformIdentity = CGAffineTransform
core_graphics.CGAffineTransformConcat.argtypes = [CGAffineTransform, CGAffineTransform]
core_graphics.CGAffineTransformConcat.restype = CGAffineTransform
core_graphics.CGAffineTransformInvert.restype = CGAffineTransform
core_graphics.CGAffineTransformInvert.argtypes = [CGAffineTransform]
core_graphics.CGAffineTransformMakeScale.restype = CGAffineTransform
Expand Down
52 changes: 46 additions & 6 deletions cocoa/src/toga_cocoa/widgets/canvas.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from math import ceil
from warnings import warn

from rubicon.objc import CGSize, objc_method, objc_property
from travertino.size import at_least
Expand All @@ -7,6 +8,7 @@
from toga.constants import Baseline, FillRule
from toga_cocoa.colors import native_color
from toga_cocoa.libs import (
CGAffineTransformIdentity,
CGFloat,
CGPathDrawingMode,
CGRectMake,
Expand Down Expand Up @@ -38,6 +40,17 @@ class TogaCanvas(NSView):
@objc_method
def drawRect_(self, rect: NSRect) -> None:
context = NSGraphicsContext.currentContext.CGContext

# Store this so we can restore the original if needed.
self.original_matrix = core_graphics.CGContextGetCTM(context)
self.using_standard_coords = self.original_matrix == CGAffineTransformIdentity
# If we're not in the normal coordinate space (presumably because we're
# rendering to a cache), save the inverse as well.
if not self.using_standard_coords:
self.inverse_original = core_graphics.CGAffineTransformInvert(
core_graphics.CGContextGetCTM(context)
)

self.interface.context._draw(self.impl, draw_context=context)

@objc_method
Expand Down Expand Up @@ -244,12 +257,39 @@ def translate(self, tx, ty, draw_context, **kwargs):
core_graphics.CGContextTranslateCTM(draw_context, tx, ty)

def reset_transform(self, draw_context, **kwargs):
# Restore the "clean" state of the graphics context.
core_graphics.CGContextRestoreGState(draw_context)
# CoreGraphics has a stack-based state representation,
# so ensure that there is a new, clean version of the "clean"
# state on the stack.
core_graphics.CGContextSaveGState(draw_context)
# Core Graphics has no built-in ability to assign to or reset the transformation
# matrix.
current = core_graphics.CGContextGetCTM(draw_context)

if current == self.native.original_matrix:
# No resetting necessary.
return

if self.native.using_standard_coords:
transform = current
else:
# The *original* transform matrix isn't the standard identity; this is
# probably because we're rendering to a cache. So we need to know the
# transform from what we started with.
transform = core_graphics.CGAffineTransformConcat(
current,
self.native.inverse_original,
)

inverse_transform = core_graphics.CGAffineTransformInvert(transform)
if inverse_transform == transform:
Copy link
Copy Markdown
Contributor

@corranwebster corranwebster Jan 1, 2026

Choose a reason for hiding this comment

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

This is also true for the identity transform and a few others which are their own inverses (like (-1, 0, 0, -1, 0, 0) and (-1, 0, 0, 1, 0, 0)).

Edit: to be constructive, I think it suffices to check if the determinant a * d - b * c == 0.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm. As I understand it, if the transform (relative to the original) is the identity matrix, then the current would equal the original and we would've already exited. But I hadn't actually realized other matrices are their own inverse as well, thanks! Math is wild.

# When a matrix has no inverse, TransformInvert returns it unchanged.
warn(
(
"No way to reset transform on macOS if current transform has no "
"inverse. Did you scale to 0, perhaps?"
),
RuntimeWarning,
stacklevel=2,
)
return

core_graphics.CGContextConcatCTM(draw_context, inverse_transform)

# Text
def _render_string(self, text, font, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions cocoa/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

class CanvasProbe(SimpleProbe):
native_class = NSView
screenshot_reset_transform = True

@property
def background_color(self):
Expand Down
1 change: 1 addition & 0 deletions gtk/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

class CanvasProbe(SimpleProbe):
native_class = Gtk.DrawingArea
screenshot_reset_transform = False

def reference_variant(self, reference):
if reference == "multiline_text":
Expand Down
1 change: 1 addition & 0 deletions iOS/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def locationInView(self, view) -> NSPoint:

class CanvasProbe(SimpleProbe):
native_class = UIView
screenshot_reset_transform = True

def reference_variant(self, reference):
# System fonts and sizes are platform specific
Expand Down
1 change: 1 addition & 0 deletions qt/tests_backend/widgets/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

class CanvasProbe(SimpleProbe):
native_class = TogaCanvas
screenshot_reset_transform = False

def reference_variant(self, reference):
if reference in {"multiline_text", "write_text", "write_text_and_path"}:
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading