Over a few days of thinking about life cycle events, we have come around to the idea that there are a number of issues which make them a poor match for the current 'event' system.
We propose adding a new widget method, with a signature that looks something like,
pub trait Widget<T> {
// ...
fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) {}
}
As a consequence of this, we will be able to change the signature of update, so that the type of the previous data goes from being Option<T> to being just T; the case where there is no data will be handled by a life cycle event.
In functionality, lifecycle is much closer to update than to event; it is principally an opportunity for the widget to do internal setup and to prepare for drawing.
There are a number of motivations for this change.
The first of these issues is conceptual, and relates to when we should be mutating application state. It is currently only possible to mutate the app's data during the event method on Widget. This is enforced by the type system; while the data is provided to the other widget methods (update, layout, and paint) it is only provided behind an immutable reference.
Are all events expected to mutate application state? We have come to the conclusion that no, they are not: certain events are intended as hooks for widgets to update internal state or to prepare for drawing. These include some events that are currently part of the Event enum, such as AnimFrame, HotChanged, and FocusChanged; and it also includes most of the events we've been thinking about as 'lifeycle' events, such as the current WindowConnected event, as well as things like WidgetCreated, WidgetHidden, and WidgetShown.
With normal events, propogation begins at the root of the tree and then flows down the branches. This is not necessarily the case for certain life cycle events. Some examples are WidgetCreated, which is sent when a widget is added to the tree, as well as WidgetHidden which is sent when a widget is made inactive but not destroyed, such as when it is the inactive branch of an Either widget. In both cases, the event should start at some non-root node in the widget graph, and propagate from there.
In current druid, there are certain guarantees around event delivery: during any pass of the runloop, we will deliver some events, and then we will call update, then layout, then paint. Life cycle events are more flexible; in the case of WidgetCreated for instance, we would expect this to be sent during the parents update method; the parent would create the widget, add it as a child, and then call WidgetCreated. The newly created child could then handle that event in order to do any initial setup; it's layout and paint methods would be called after this update cycle had completed.
updateThis was touched on earlier, but it's worth addressing separately: currently the signature for update is,
fn update(&mut self, ctx: &mut UpdateCtx, old_data: Option<&T>, data: &T, env: &Env);
Of note is that old_data has type Option<&T>. This is because currently we need to call update even if this widget has not seen data before, such as after handling the first event after startup. By introducing separate life cycle events, we can designate those events as the place to do initial setup, and then ensure that update is only called when old data exists; update is now more clearly about updating, and is no longer implicitly responsible for setup, so we can change that signature to,
fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env);
This is a minor improvement, but one which should hopefully reduce confusion.
AnimFrameThe AnimFrame event has always been a funny case; it is the only event that can be sent after update, and there is a bunch of special logic to accommodate it. Some of this may be unavoidable, but much of it can hopefully be clarified under this new approach.
It's possible that there are some subtleties here that will become obvious while doing the work, but I suspect this will overall lead to a cleaner abstraction that makes it easier to write widgets that behave as expected.
_should this method be optional?_ It is worth considering whether we want to have a default, noop impl of this method in the widget trait. The upside is that we would break less existing code, and also possibly that lifecycle events are not necessary in many cases; the downside would be that it might be easier for widget authors to avoid implementing life cycle events, or to overlook their existence.
_is there a better name than 'lifecycle'?_ It feels like life cycle might be a bit narrow to describe exactly the events we're trying to characterize; ultimately these are more 'bookkeeping' or 'internal' events, and I'm open to a more descriptive name.
I don't know about either of these naming ideas, but the fact that the lifecycle events don't happen during a specific time or point during the runloop reminds me of both _notifications_ and _signals_.
None of the other widget trait methods are optional right now and there's plenty of empty update methods with no complaints. I think it's fine if the lifecycle method isn't optional either. It keeps things unsurprising.
Also, the reasons that I can see for making it optional can also apply to event and update which some widgets have no need for. Thus I think whichever the choice is, I would rather it be applied consistently.
Most helpful comment
None of the other widget trait methods are optional right now and there's plenty of empty
updatemethods with no complaints. I think it's fine if thelifecyclemethod isn't optional either. It keeps things unsurprising.Also, the reasons that I can see for making it optional can also apply to
eventandupdatewhich some widgets have no need for. Thus I think whichever the choice is, I would rather it be applied consistently.