-
Notifications
You must be signed in to change notification settings - Fork 29
EventLoops.md
Every GUI library needs to figure out how it's going to handle event loops - how control moves in the program most of the time. Scarpe is limited by being display-service agnostic, so it has to support a wide variety of event loops.
When Scarpe uses a Webview local display service, for instance, background threads are basically disallowed. Webview will stop all background threads when it starts up, and a background thread making most Webview requests will crash Webview. So: no background threads.
As long as Scarpe primarily uses local Webview, it would be fair to say that Scarpe also forbids using background threads, because that makes it so easy to crash Webview. If we wanted to make Scarpe compatible with background threads, we'd want to require display services to handle that, as the Webview-relay display service does (for relay, by running Webview in a separate worker process and communicating over a socket.)
The Scarpe::App drawable initially receives control of the event loop, and sends a "run" event, which the Display service will normally receive and handle. With events you can't tell if anybody will receive them. But normally the Display service will never return from that event, at least until it's time to quit the app. If the Display service doesn't subscribe to that notification, or returns immediately from it, Scarpe::App will (currently) run its own event loop until the app receives a "destroy" call or event.
There are a few points in a Shoes file where you could take specific actions. They have tradeoffs. For instance:
You could display the window as soon as possible. When Shoes sees a method like "button", then add the button. When the window shows up it will be incomplete, and things may move around. But it will be as easy as reasonable to see what's going on. So this method trades simplicity and visibility (good) for inefficiency and seeing everything get into place (bad).
Or you could display the window at the end of the Shoes.app block. You'll need to make sure that modifying drawables that aren't yet shown (e.g. doing a para.replace() before that para is visible) is okay. But you can show everything in one big display event at the end of the Shoes block, with all the drawables in their final positions. So it loses simplicity (hidden objects are the rule, not the exception) and visibility (can't see what drawables are doing), and gains efficiency, plus the window looks good in how it shows up. One problem with losing visibility: if there's a time-consuming operation during the Shoes.app block, the window will simply not show up until it finishes.
You can even take it a step further: don't display the window until control reaches the end of the Shoes file and the Ruby interpreter would normally exit. That's what Shoes3 does. It's the same as the second option, but it also executes any code following the Shoes.app block before displaying the window. So Shoes permits "inline" Ruby code running during app setup, but only before the window is first displayed. After that, all Ruby code runs in callbacks.
Here's a fascinating Shoes3 app:
Shoes.app do
$p = para "Hello"
sleep 5
$p.replace("HRM")
end
sleep 5
$p.replace("YUP")
It waits about ten seconds before even the window shows up... And the just shows "YUP". So: it would appear that Shoes3 starts the loop and allows you to modify drawables for a significant amount of time before displaying the results. That's one very reasonable way to deal with the tradeoffs. It also means Shoes does not display the window the moment it finishes executing the Shoes.app block. Maybe it doesn't show it until it reaches the end of the source file, using at_exit or similar?
Of course, it's not entirely clear how control moves. For instance, here's an excerpt from the Shoes3 Git repo:
VALUE shoes_app_window(int argc, VALUE *argv, VALUE self, VALUE owner) {
// ... (Many lines cut) ...
if (shoes_world->mainloop)
shoes_app_open(app_t, url);
return app;
}
That would appear to be changing how opening a Shoes app works... depending on whether there's already a main loop running for a Shoes app (e.g. the Shoes splash app.) Shoes has a few tricks that really look like it's fine to run a Shoes app midway through a different Shoes app and it'll roll with it. It also looks like opening a new window may be implemented as "start a new Shoes app in the same process."
Here's how Shoes3 runs another event loop entry point:
shoes_code shoes_app_loop() {
if (shoes_world->mainloop)
return SHOES_OK;
shoes_world->mainloop = TRUE;
INFO("RUNNING LOOP.\n");
shoes_native_loop();
return SHOES_OK;
}
Basically: don't run a main loop if there's already one running, and then run a native (that is, GTK+-specific, Cocoa-specific, etc) main loop. Which, again, seems to "merge" conceptually-multiple main loops in some cases. Maybe not? It's hard to be sure.
The app-init logic in Shoes3 is beyond my ability to easily evaluate. I've tracked as far as "here's where the block passed to Shoes.app winds up" (the instance eval is in shoes_app_run). But it doesn't look designed to run with a "normal" init process where you run the program from the command line directly as a plain Ruby source file. It's not so much using at_exit as its being loaded into a different long-running process that's already doing its thing.