adamdruppe / arsd

This is a collection of modules that I've released over the years. Most of them stand alone, or have just one or two dependencies in here, so you don't have to download this whole repo.
http://arsd-official.dpldocs.info/arsd.html
530 stars 125 forks source link

minigui: event loop: process messages #311

Closed andre2007 closed 2 years ago

andre2007 commented 2 years ago

While pressing a button on the UI, there is some heavy calculations ongoing, demonstrated in the example using Thread.sleep. The UI freezes until the loop is over. To keep the UI usuable and provide e.g. a cancel button to stop the heavy calculation if needed, I would like to call on the Event Loop object a method processMessages. Therefore I want to give on each loop iteration the Event Loop the chance to process the messages in the stack (e.g. UI clicks,...) before continuing the heavy calculations.

I searched the documentation and the source code, but was not able to find s.th. Does the EventLoop provide such a feature?

/+ dub.sdl:
    name "application"
    dependency "arsd-official:minigui" version="10.3.10"
+/
import core.thread;
import arsd.minigui;

void main()
{      
    new MyWindow().loop();
}

class MyWindow: MainWindow
{
    this()
    {
        super("sample", 161*4, 372);
        new Button("Test", this).addEventListener("triggered", () {
            foreach(i; 1..10)
            {
                Thread.sleep(dur!("seconds")(1));
                // eventLoop.processMessages();
            }
        });
    }
}
adamdruppe commented 2 years ago

You'll want to invert things a bit: don't call the event loop from the function (though you actually can do that, it is simpledisplay's EventLoop.get.run() function, you have to pass it a delegate to tell it when to stop processing... but don't do that), but instead call the function from the event loop by sending events. You can do this most easily with a Timer object: http://arsd-official.dpldocs.info/arsd.simpledisplay.Timer.html

import core.thread;
import arsd.minigui;

void main()
{
    new MyWindow().loop();
}

class MyWindow: MainWindow
{
    this()
    {
        super("sample", 161*4, 372);

        new Button("Test", this).addEventListener("triggered", () {
            int counter;
            Timer timer; // declare separate so it can destroy itself when done
            timer = new Timer(1, {
                Thread.sleep(500.msecs);
                counter++;
                import std.stdio; writeln("counter ", counter);
                if(counter == 10) {
                        timer.destroy(); // declare outside so this call possible
                        messageBox("All done!");
                }
            });
        });
    }
}

(timer is in simpledisplay, but minigui public imports simpledisplay, so it works)

The interval of 1 millisecond is small, but the implementation ensures it never gets backed up; if you miss a time, it just calls as soon as possible so you don't have to worry about getting into an impossible-to-finish backlog loop.

The problem though is the event loop only runs in between those timer pulses still, so everything there would lag by a half second. You'd want to do fairly little work and it is hard to get it right.

Alternatively, you might run the result in a worker thread and send an event back to the window when it is finished. This is something I'd like to make more convenient, but it still isn't too bad to do now, but being in a separate thread means you do need to be very careful about not calling gui methods or otherwise getting into thread data problems. I'd recommend passing a copy of everything the worker needs to the thread when it is constructed, then pass a message back to the gui thread when it is finished. Here's how you might do that:

import core.thread;
import arsd.minigui;

void main()
{
    new MyWindow().loop();
}

class MyWindow: MainWindow
{
    this()
    {
        super("sample", 161*4, 372);

        new Button("Test", this).addEventListener("triggered", () {
                auto thread = new Thread({
                        // do your work
                        foreach(i; 0 .. 10)
                                Thread.sleep(500.msecs);
                        // then when finished, use simpledisplay's runInGuiThread
                        // helper to pass the message back. This is actually run
                        // synchronously; the worker thread is paused until the
                        // gui thread finishes running this function.
                        runInGuiThread( {
                                messageBox("All done!");
                        });
                        // just to prove that it waits until you hit OK to finish here
                        import std.stdio; writeln("worked exiting...");
                });
                thread.isDaemon = true; // make it clean up automatically instead of needing explicit join
               // the daemon thing also means if you close the window, the program will exit without waiting for the worker thread to finish
                thread.start();
            });
    }
}
adamdruppe commented 2 years ago

I didn't put it in the example but yeah make sure you copy data into that thread so it isn't racing with the gui. You'd probably be ok with most things since the events tend to be user generated... but still. And other threads must not call methods on gui objects (unless the documentation specifically makes an exception) or you're liable to crash. So if the helper thread ever needs any from gui objects, you'll want to use that runInGuiThread helper to do the work in there and pass the data back out.

The nice thing about runInguiThread blocking is you actually can use data from the helper thread in the gui thread there, since the helper thread is on hold while that runs.

adamdruppe commented 2 years ago

BTW there is a third option that's very similar to the first option - it runs basically the same way as the timer - that I thought about demoing but it actually is liable to crash in certain cases too.... and one of those is messageBox. You can't call messageBox from inside a custom sdpy handler since that part of the event loop is not reenterant. (It works on Windows though...)

Just just completition I'll show it here:

import core.thread;
import arsd.minigui;

void main()
{
    new MyWindow().loop();
}

// You can make your own class and send it to a Simpledisplay window
class WorkEvent {
        MyWindow window;
        this(MyWindow window) {
                this.window = window;
        }

        int workLeft = 10;

        void doWork() {
                // pretending this is working
                workLeft--;
                Thread.sleep(500.msecs);

                if(workLeft)
                        window.win.postEvent(this); // add it back to the event queue
                else
                        {} // we're done! can't messageBox here though or we'll hit an assert(0)
        }
}

class MyWindow: MainWindow
{
    this()
    {
        super("sample", 161*4, 372);

       // register a handler for the custom event with the window
     // notice the use of `this.win`, which is the SimpleWindow that minigui builds on
        this.win.addEventListener((WorkEvent we) {
                we.doWork();
        });

        new Button("Test", this).addEventListener("triggered", () {
          // and post the custom event to the SimpleWnidow to kick things off
            this.win.postEvent(new WorkEvent(this));
        });
    }
}

minigui is built on simpledisplay which sometimes means a bit of duplication: SimpleWindow.addEventListener is similar to, but different than minigui Widget.addEventListener. minigui events are normally synchronous, simpledisplay events are generally async.

Sync events are nice in that you can actually preventDefault. But async events can do cross-thread magic and adding multiple copies can collapse into one. I use them fairly heavily internally (widget.redraw(), for example, actually does a simplewindow.postEvent so if you call it several times in a row, it still only actually happens once!).

So I don't think it is the right thing to do here, but being aware that it exists might be helpful to you later. As long as you don't confuse the differences between the similarly named functions....

andre2007 commented 2 years ago

Thanks a lot Adam for the different suggestions. This is fantastic and solves my problem.