Skip to content
Open
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 changes/319.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Canvas usage (and some Color usage) has been updated to reflect changes in Toga's API.
40 changes: 19 additions & 21 deletions src/toga_chart/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
from matplotlib.path import Path
from matplotlib.transforms import Affine2D
from toga import Canvas, Widget
from toga.colors import color as parse_color
from toga.colors import rgba
from toga.colors import Color, rgb
from toga.fonts import CURSIVE, FANTASY, MONOSPACE, SANS_SERIF, SERIF, Font
from toga.handlers import wrapped_handler
from travertino.size import at_least
Expand Down Expand Up @@ -80,7 +79,7 @@ def _draw(self, figure: Figure):
:param figure: The matplotlib figure to draw
"""
_, b, w, h = figure.bbox.bounds
self.canvas.context.clear()
self.canvas.root_state.drawing_actions.clear()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this indicate the need for a self.canvas.clear() (or self.canvas.root_state.clear()?) method? While this is legal, it seems like a common enough use case that it might warrant an entry point.

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.

That's fair... it's just a little odd in terms of comparing it to the HTML API. Since Context2d doesn't give any direct access to the "history" of drawing actions, there's no way to explicitly tell it to discard prior drawing operations. As far as I'm aware, the standard way to erase the whole canvas is by calling clearRect and telling it the entire canvas as the rectangle. We don't have a clear_rect method yet, but once we do, that would presumably just be one more drawing operation added to the list. The effect would be the same... except that, of course, you could always delete the ClearRect action later, and everything should still be there.

Add to that the fact that, at least as far as things are currently implemented, Canvas already inherits clear() from Widget. Granted, it just throws an error, and I don't think there's any risk Canvas will ever need to have children, so we could give it is own custom clear() method that handles this. But Barbara Liskov might send someone to break our kneecaps.

So... yes, I do think it's a common use case, and a good button to expose. I'm just not positive of the best way to spell it, in betwixt our various idioms.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Oh, the ironies... Now I remember: that the existence of clear() on the base Widget was the reason we moved all the drawing instructions to canvas.context...

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.

The good news is that sinceclear isn't part of the HTML API, we're not married to that name for such a method. (Although because it is a natural guess, we could overwrite Widget.clear just to add a more helpful error message that says what to use instead.)

renderer = ChartRenderer(self.canvas, w, h)

# Invoke the on_draw handler.
Expand Down Expand Up @@ -140,36 +139,36 @@ def draw_path(self, gc, path, transform, rgbFace=None):
else:
r, g, b, a = gc.get_rgb()

color = parse_color(rgba(r * 255, g * 255, b * 255, a))
color = Color.parse(rgb(r * 255, g * 255, b * 255, a))

if rgbFace is not None:
stroke_fill_context = self._canvas.context.Fill(color=color)
stroke_fill_context = self._canvas.fill(color=color)
else:
offset, sequence = gc.get_dashes()
stroke_fill_context = self._canvas.context.Stroke(
stroke_fill_context = self._canvas.stroke(
color=color,
line_width=gc.get_linewidth(),
line_dash=sequence,
)

transform = transform + Affine2D().scale(1.0, -1.0).translate(0.0, self.height)

with stroke_fill_context as context:
with context.Context() as path_segments:
with stroke_fill_context:
with self._canvas.state():
for points, code in path.iter_segments(transform):
if code == Path.MOVETO:
path_segments.move_to(points[0], points[1])
self._canvas.move_to(points[0], points[1])
elif code == Path.LINETO:
path_segments.line_to(points[0], points[1])
self._canvas.line_to(points[0], points[1])
elif code == Path.CURVE3:
path_segments.quadratic_curve_to(
self._canvas.quadratic_curve_to(
points[0],
points[1],
points[2],
points[3],
)
elif code == Path.CURVE4:
path_segments.bezier_curve_to(
self._canvas.bezier_curve_to(
points[0],
points[1],
points[2],
Expand All @@ -178,7 +177,8 @@ def draw_path(self, gc, path, transform, rgbFace=None):
points[5],
)
elif code == Path.CLOSEPOLY:
path_segments.ClosedPath(points[0], points[1])
self._canvas.move_to(points[0], points[1])
self._canvas.close_path()

def draw_image(self, gc, x, y, im):
pass
Expand All @@ -202,14 +202,12 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
if ismath:
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
else:
self._canvas.context.translate(x, y)
self._canvas.context.rotate(-math.radians(angle))
with self._canvas.context.Fill(
color=self.to_toga_color(*gc.get_rgb())
) as fill:
self._canvas.translate(x, y)
self._canvas.rotate(-math.radians(angle))
with self._canvas.fill(color=self.to_toga_color(*gc.get_rgb())):
font = self.get_font(prop)
fill.write_text(s, x=0, y=0, font=font)
self._canvas.context.reset_transform()
self._canvas.write_text(s, x=0, y=0, font=font)
self._canvas.reset_transform()

def flipy(self):
return True
Expand All @@ -235,4 +233,4 @@ def get_font(self, prop):
return Font(family=font_family, size=size)

def to_toga_color(self, r, g, b, a):
return parse_color(rgba(r * 255, g * 255, b * 255, a))
return Color.parse(rgb(r * 255, g * 255, b * 255, a))