Remove State.canvas backreference, combine drawing actions, move drawing methods#4159
Remove State.canvas backreference, combine drawing actions, move drawing methods#4159HalfWhitt merged 29 commits intobeeware:mainfrom
State.canvas backreference, combine drawing actions, move drawing methods#4159Conversation
| # Use the font to write on a canvas. | ||
| canvas = toga.Canvas() | ||
| canvas.root_tate.write_text("Hello", font=my_font) | ||
| canvas.write_text("Hello", font=my_font) |
| _MIN_HEIGHT = 0 | ||
|
|
||
| # 2026-02: Backwards compatibility for <= 0.5.3 | ||
| _instances: list[ref] = [] |
There was a problem hiding this comment.
Order doesn't matter, so a weakref.WeakSet may provide a nicer API than a list of weakrefs?
There was a problem hiding this comment.
Also has the advantage of a faster keyed lookup, and no duplication of items.
There was a problem hiding this comment.
Thank you, that's not a thing I knew about! I don't think we'll ever have to worry about checking membership or about duplication, but the fact that it auto-prunes itself is great.
freakboy3742
left a comment
There was a problem hiding this comment.
I've given this a really cursory review - tl;dr is that it looks great. A couple of minor things in the docs that stood out, plus one naming issue (close_path vs closed_path) that we may need to resolve; but this looks to be on the right path in general.
| Any argument provided to a drawing operation (including context managers) becomes a property of that object. Those properties can be modified after creation, after which you should invoke [`Canvas.redraw`][toga.Canvas.redraw] to request a redraw of the canvas. | ||
|
|
||
| Drawing operations can also be added to or removed from a state using the `list` operations `append`, `insert`, `remove` and `clear`. In this case, [`Canvas.redraw`][toga.Canvas.redraw] will be called automatically. | ||
| A state stores a list of its associated drawing instructions as an attribute named [`drawing_actions`][toga.widgets.canvas.State.drawing_actions]. This can be modified like any other list (`append`, `insert`, `remove`, `clear`, etc.). As with modifying attributes, [`Canvas.redraw`][toga.Canvas.redraw] will need to be called to show the changes. |
There was a problem hiding this comment.
As of these edits, there's been no previous mention of state - some introductory material is needed here. That's probably also a good opportunity to talk about the relationship between context managers and the save/restore state calls.
There was a problem hiding this comment.
Is it okay if most of that waits until a separate docs PR? I was trying to keep the docs changes minimal while staying factually accurate, for now.
There was a problem hiding this comment.
In particular, I'd like to wait until we've implemented:
save()andrestore()methods- Setting basic fill / stroke attributes
- Separate
fill_text()andstroke_text()methods
Those are my next priorities, and should all be relatively straightforward. (Famous last words, I know.) With those in place, I think I'll be in a good place to reorganize the canvas docs.
Speaking of which, I don't know if you have a time in mind for publishing 0.5.4, and I'm sure this has already crossed your mind, but I'd definitely like to get this whole situation to a stable state beforehand. We don't need to have implemented everything, but I want to have the functional structure all in place, and to do a thorough double-check to make sure we don't release any public API components that are transitional or likely to be deprecated.
There was a problem hiding this comment.
I don't have an immediate timeline in mind for 0.5.4; it will definitely need to happen before PyCon US, but I'd hope we have at least one other release before then. I don't have any particular problem waiting until the Canvas stuff is all settled (or, at least, significantly settled) before 0.5.4.
I don't have a problem if a significant rework of docs is a future PR; but the docs shouldn't be wrong in any interim state. So a minor edit right now to give some context to what a "state" is, with a view to a longer term rewrite/rework would be fine.
There was a problem hiding this comment.
How's this? It's admittedly a little awkward, but I think it at least works, within the constraints of the existing order / structure of the page.
There was a problem hiding this comment.
Yup - this is more than enough for now.
|
The tests are failing partly because of platform-dependent font differences (fixable by adding reference image variants) and partly from mitering issues. I'm gonna try to resolve #4162 first, then come back to finish this up. |
A thought from a related problem in #4163 - we might want to deprecate |
That would be a pretty big difference... the current behavior just moves but doesn't shift the coordinate system at all. Part of me has already been wondering if the x and y coordinates are more complication / edge case than they're worth, when all they save is a single However, a transform parameter seems even further afield to me. For one thing, it would probably be used less often than x and y — one probably only sometimes wants to start a new path in a new coordinate system, but always needs to start at some coordinates. For another, having it potentially do two different things in standalone vs. context manager mode seems likely to be surprising. That said, I don't necessarily know quite what all you have in mind. |
|
Touching base on this PR - Is the toga-chart issue still current, or was that resolved by beeware/toga-chart#316? I'm mostly asking because we're overdue for a Toga release, but canvas is in a bit of a "mid API transformation" state, which I think we'd rather avoid releasing until we land at least this part of the work, and I'd like to get a feel for if/when we're likely to be able to schedule a release. |
As far as I know the problem still exists, but beeware/toga-chart#316 should get me unstuck on tracking it down, because now I have a working state against which to compare this PR. Many thanks to @johnzhou721, since that's what initially killed my momentum here.
My apologies for vanishing and not saying anything here. Holy crap, I can't believe it's been a month and a half. I've had a lot going on, but I think I should be able to take a look at this this week. Ideally I can fix it with minimal fuss, but if not I can at least update on the prognosis. |
|
The issue was that TogaChart uses the It takes some fiddling, but I added a way to check for and accommodate this (undocumented, but currently possible) usage. |
|
Something's been nagging at me.
I don't think this is ideal. It mixes concerns, and raises the question of what, if anything, those arguments should do when a context-capable command isn't entered. We could paper over this by giving them custom I think we'd be better off to nip this in the bud and tweak the class hierarchy. What's currently named It would be highly preferable to do this shuffle before releasing the new API and its documentation — but probably in a separate PR from this, just to avoid muddying things even further. Unless I'm missing something, it should be a straightforward change. * My first thought was |
Agreed.
Also agreed.
I think the solution you've proposed here makes sense. The fact that all the subclasses of State completely overwrite the definition of
Also agreed - and yes, it seems like it should be fairly straightforward, and mostly transparent to any existing uses (mostly closing off API edge cases that won't make sense once the kwarg handling is in place). |
|
Okay, will do. Anything left to do / fix / change on this particular PR, or can we merge the beast? |
FWIW, the high-level |
Hm, that's an interesting one. My first thought is that it might be worth splitting out some of the current As far as I'm aware, the only way to use a |
You can also add a path to another path in javascript, I think. But yes, #4163 has a |
freakboy3742
left a comment
There was a problem hiding this comment.
I think this is almost mergable. I'm happy with the basic mechanics of the reorg; the only two things that stood out are both in the examples.
- There's a lot of references to
close_path(x, y)that raise an error. I'm fairly certain these are all just "convert toclose_path()and add amove_to()", but I wanted a head-check on that. - Tutorial 4 raises a warning when it accesses
root_stateto determine if any drawing has happened. The example should all run clean as examples of best practice. It's easy enough to fix with ahasattr()call (or acatch AttributeError) onself.text_width; but I'm not sure if that's something where we need a better API on Canvas, or we should consider it an odd feature of the tutorial code.
johnzhou721
left a comment
There was a problem hiding this comment.
Apologies, hit the wrong button and prematurely sent my review. 2 more comments.
freakboy3742
left a comment
There was a problem hiding this comment.
Ok - at this point, I'm happy enough for this to be merged. You've flagged there's more work to do, but this works as a self-contained PR. The "clear/is clear" issue would be nice to resolve, but it doesn't need to stand in the way of this work landing.
I've pushed one update to mark the static content web tests as flaky since they seem to be causing issues on Qt; as soon as that's passing CI, I'm OK for this to land.
|
From discussion in Discord: Since when save and restore functions are added they'll both be DrawingActions on the root state rather than some other indicator, I'm personally convinced that clear/is_clear should be able to exist, since is_clear can be trivially implemented and saves user effort. |
I'm not against the idea, and I have various thoughts, but this PR is already massive enough. |

Part of #3994
I apologize for how monolithic this PR is; I came up against a circle of mutually dependent changes. It's possible I might have eventually worked out how to extricate one or more of thesm, but I'm pretty sure it would've required deprecating some things before implementing their replacements, or doing some very convoluted and temporary logic, so... here we are.
State.canvasandState.redraw()are both deprecated. State is now "canvas-agnostic".Since State can't redraw the canvas anymore, it loses the primary motivation for having its own list-like methods (which each called
redrawafter a change). I've deprecated them, and documented accessing theState.drawing_actionslist directly.In order to support legacy code that expects modifications to
drawing_actionslists to redraw the Canvas, we keep alist of weakrefs toweakSetof all Canvases. When a State's (deprecated) list-modifying methods are called, it looks through all extant Canvases and redraws any that contain the State. (State gains a recursive__contains__for this purpose.)Canvas now gains access to the drawing methods; calling them automatically adds to the "current" state (innermost context manager, or the root state if none). Calling drawing methods on State is deprecated.
The CamelCase-named context-manager drawing methods are deprecated, and context-manager functionality is merged into the lower-case drawing methods. That is, both of these are valid:
One side effect of this is that
fill,stroke, andclose_pathnow accept (optional)xandyparameters (at the end, rather than first, as in their predecessors). These do nothing if the DrawingAction is never entered, but we can't raise an error in the initializer if they're supplied extraneously (because we don't know at that point whether it's going to be entered or not).They're also in the
__repr__, which isn't entirely ideal. It should be possible to disable displaying them for instances that haven't been entered, but that seems like a matter for another day.(I could also see an argument for deprecating these parameters entirely, but I suppose they do still seem reasonably handy.)
[Edit: They're removed in the new API.]
As before, I've tried to keep the documentation changes as minimal as possible while staying technically accurate. The rewrite/reorganization is, of course, becoming increasingly necessary...
In addition to standard deprecation tests in core, I've also added a testbed test that runs a (slightly modified) version of the existing code from the Canvas tutorial, to make sure it still displays properly.
PR Checklist: