New (v0.5.4) Canvas API: why restrict context managers to one __enter__ only? #4397
Replies: 7 comments 13 replies
|
Repro: Pressing "Add", then "Clear", works as intended. A second press of "Add" raises an exception. |
|
Hi there! Glad you're liking (some of) the changes. I've had my nose so close to the API that it's good to have the opportunity to talk to a real user trying it out. Sorry it took me so long to see this. Conceptually, the idea is that the interface on the Canvas widget itself matches the HTML canvas API as closely as is feasible. It has optional Python extras, like the context managers, but they still map directly to "real" canvas commands being executed one after the other. HTML's canvas API is entirely linear. There's no way, in JavaScript, to go back and change or remove a prior action after the fact; you just have to draw it over again (potentially after clearing the area). So Canvas's methods and attributes work the same way — and if you do want to alter history, you can manipulate the drawing actions directly, as you've resorted to. That said, I don't quite recall if there was any technical reason a context manager can't be opened more than once — the main issue was opening one out of order. In other words, things start breaking (behaving unintuitively) if we allow this: fill = canvas.fill() # Adds a Fill object to the list of drawing actions.
# Call some more drawing methods here. These append more actions after the Fill.
with fill:
# Now, any actions added here happen "inside" the Fill, *before* the above
# instructions, e.g. something drawn here would get covered up by anything drawn
# above.But I don't recall if anything in particular breaks if we allowed this: with canvas.fill() as fill:
# Call some drawing methods
# Do some other stuff, but *not* drawing methods of the canvas.
with fill:
# More drawing methods, which get appended to the same Fill object.Conceptually, it's a little weird... the idea is supposed to be that the context manager is saving state when it enters, then filling and restoring state when it leaves. This looks like it should save/fill/restore a second time, but in reality you're retroactively inserting more actions before the fill-and-restore. Would you find this helpful? Now, one more thing: the code you've shown doesn't quite do what you think it does. Or at least, it does, but it also does something else. draw_action = canvas.draw_image(...)
draw_collection.drawing_actions.append(draw_action)That first method draws the image to the canvas, in whatever state it currently is. Then you also add the resulting drawing action to the state in question, resulting in the same drawing action occuring twice within the canvas's total set of drawing actions. What you probably want to do is this: from toga.widgets.canvas import DrawImage
draw_action = DrawImage(...)
draw_collection.drawing_actions.append(draw_action)Things can get quite surprising if you start mixing the two "levels" of interface; calling the canvas's methods in between adding and removing your own drawing actions is a little akin to modifying a list while iterating over it. The methods work under the assumption that things are progressing linearly, so they might do things you don't expect if you change the ground under their feet. |
|
Thanks for the reply, and the additional notes! While what I wanted is doable with list methods, as you suggest, I hope the context manager limitation gets relaxed at some point, as it is cleaner to write it that way. Thanks again! P.S.: personally I don't see any problem with the "out of order" added instructions; if you create a "fill", whatever you add under it will be drawn where the "fill" was inserted; this does not seem contradictory to me. You're adding to a tree, and things are painted as per the tree, as expected. |
|
Actually, wait... now that I think about it more, it's actually less intuitive than that. If you enter multiple context managers out of order (assuming the error is removed), the canvas doesn't have any way of knowing in what order you entered them. As currently implemented, it can only ever append to the "latest" spot, in terms of the tree — for example, D in the following hypothetical: So in my example above, it wouldn't be appending inside the Fill. It's tracking the tip. Doing otherwise — always adding to the last one opened — would be possible, I imagine. But it would probably require keeping a monolithic stack of open states, especially since the same one could be added to multiple canvases. I'd prefer it if the states could remain self-contained, and not have to maintain a global stack. The more I think about it, I wonder if, rather than altering how Canvas targets states, this might be an argument for un-deprecating states' drawing methods. Beforehand, this was the only way to put drawing actions inside them. This had a potential foot-gun: ctx = canvas.context
ctx.do_stuff()
with ctx.ClosedPath() as closed:
# What you need to do:
closed.do_stuff()
# What you could quite easily keep doing, but these operate outside
# this subcontext, ignoring any translations, rotations, etc.:
ctx.do_stuff()In other words, the actual context manager wasn't really doing much; it only mattered what you called the methods on. The drawing methods were added to canvas in order to make them auto-track the latest open context manager. So that's the preferred "normal" / linear API... but maybe it would be worth keeping them (undeprecated) on the states as well, for retroactive editing. So in your code, you could do: draw_collection.draw_image(...)And that would always add to that state, no matter where or when you call it. This works right now, in fact — you'll just get a deprecation warning. (Keep in mind — any time you're adding from anywhere other than the canvas itself, remember to manually call One wrinkle here is that the signatures of some of these methods changed in the revamp; that hasn't been too much of a problem, because we've been able to keep the old signature on the states, while implementing the new one on Canvas. If we undeprecated the state methods, we'd have to accommodate both signatures on states... |
|
Thanks again for the details! I have not followed the sources, but from the documentation, I was under the impression that the logic would be something like: Thus no global stack was needed. I tried the following code, and it outputs so clearly an action is being inserted inside red_fill. Unfortunately, I cannot test any difference from a "linear order", because adding any canvas action between (1) and (2) presently causes the exception "A Canvas context manager can only be entered once, and only before any subsequent drawing actions are added." Thanks once more. P.S.: I understand the need for the interface to be clear without recurring to implementation imaginations... but there must be some definition of what the Z-order should be; the grouping of actions into states will probably be very related to the Z-order definition. |
|
Thanks to all participants on the discussion. To me, it's not entirely a "context managers" issue. Conceptually, part of the issue is that "state" has 2 functions: It's the second part that feels a bit lacking. From the moment you expose and allow list methods (via the drawing_actions property), you are inviting the drawing order to be disrupted. (Not that I oppose to that disruption, on the contrary, it gives the developer control over the Z-order... just saying that concepts like a "linear order" or "appending actions" become now a bit blurry.) At the risk of seemingly repeating the obvious with the above... I think it sets the stage for a discussion about context managers. Are context managers intended to serve function (1) only, and using them for (2) stresses the design? And if context managers are intended to be so coupled with the lifetime of the state (as in the open/close analogy), what is the intended use of creating states "on their own", separate from context managers? |
|
Possible idea to play with: canvas.use(state) returns a context manager that holds the association between canvas and state, allowing stacking states upon nesting without the need for a state to store a canvas reference. Different names needed for (a) a method as above, that just wraps an EXISTING state in a context manager, and (b) a method that CREATES a state and stores it in the context manager, to be returned by enter ("with canvas.whatever() as state:"). In both cases the context manager holds also a reference to the canvas. |
Uh oh!
There was an error while loading. Please reload this page.
I have a small confusion with the new Canvas API. It's a great step in the right direction, but some corners don't make sense to me; example:
I'd like to create a state to hold some draw_image actions, so I can clear later that part of the canvas only. So:
draw_collection = canvas.state()(assume I preserved the state as self.draw_collection on some object; I'm trying to be brief here.)
Then, repeatedly at several moments of the app's lifetime:
And eventually, at some later moment,
draw_collection.drawing_actions.clear()in order to remove these draw_image actions but keep the rest of the canvas' content.
However, on the line "with draw_collection:" I get the exception
RuntimeError: A Canvas context manager can only be entered once, and only before any subsequent drawing actions are added.
My question is, what is the rationale for this limitation? It forces me to rewrite as
where the first statement presumably added draw_action to canvas.root_state.drawing_actions, so now I also need to remove it from there.
All reactions