cursive icon indicating copy to clipboard operation
cursive copied to clipboard

Docs: explain how to use callbacks

Open njskalski opened this issue 5 years ago • 2 comments

Hi,

a huge part of Cursive I don't understand is Callbacks. What are they for? What is the advantage of returning a callback in response to consuming event?

Can you please explain it a little better in docs?

njskalski avatar Mar 18 '20 23:03 njskalski

Returning a callback is a bit strange but it's how you get access to the Cursive object while keeping the initial callback static.

danobi avatar Mar 31 '20 02:03 danobi

Here's a brief summary of the idea behind the design behind View::on_event and EventResult:

  • View::on_event takes a &mut self so it can modify the view reacting to the event.
  • As reaction to the event, we want to potentially update the view tree; this would need a &mut Cursive
  • But the root Cursive contains the view, so from &mut Cursive I could get another &mut my_view. This is impossible by rust standard. For example, what if I remove the view itself in the callback? The view cannot really be dropped since we're still in one of its methods.

The conclusion is that we cannot easily[1] take a &mut Cursive in View::on_event. Instead, we can prepare "what we would do if we had this &mut Cursive" - that's the EventResult. You split your event response in two parts: the first half (the body of View::on_event) that updates the current view, and the second part (the EventResult) that updates the Cursive root to modify the view tree.

Most of the times it's easy to split (one of the two halves may be simply empty). In more complex cases where you want to update the current view based on something in the view tree, you'll just need to somehow update the current view from the EventResult itself. This can be done by sharing a Rc with the callback, or by re-finding the current view from the root if it has an ID or something.

One drawback of retuning the callback is the need to box it every time it needs to be returned. Note though that if the callback is zero-sized (which happens if it's a closure that doesn't embed any state), then Box will not actually allocate. So running fixed actions in callbacks is still allocation-free. A possible improvement could be to add some small-value optimization a-la SmallVec to embed closures that fit in some pre-determined size (128 bits for example). But I'm not sure how easy it is to do with dyn Fn(), so for now it's probably not worth it.

Avoiding allocations in the general case might be possible, but I don't have a good solution so far - it's very likely to make the whole event processing even more complex, wich is not a trade-off I'd love right now.

In the future I want to add a "ViewPath" to the on_event method, which describes the path in the view tree at the moment the event is dispatched. It would then be easy to give this path to the EventResult to find the current view again, without the need for a NamedView.

Another long-term goal I had in mind was EventResult not meant for the Cursive root, but for one of the parent views. This would potentially allow "sub-root" views to sandbox children, or enable more inter-views communications. This is still a vague idea and will obviously need a lot more design to get anywhere near usable.

I'll update a bit the documentation for View::on_event to make some of this clearer.

[1]: One convoluted way to have both would be to wrap every single view in some sort of Rc + RefCell; though it would need a deep refactor of how the view tree is visited for events. Also it might add some (probably small) overhead to every single view, including wrapper views, which is something I'm trying to avoid (wrapper views are a good idea as long as they are really cheap).

gyscos avatar Mar 31 '20 18:03 gyscos