2020-08-30

How to run an animation while waiting for a task to finish?

This shows how to run an animation in QML (say an animated gif) while running a task with unknown duration.

The trick here is to run the task in a thread, while running a local event loop, needed for the animation to keep running. Since closures are intuitive and simple in Lisp, the solution should also be quite easy to understand.

Say you have the following QML for your animation:


  // busy animation

  AnimatedImage {
      objectName: "busy"
      anchors.centerIn: parent
      width: 64
      height: width
      z: 10
      source: "img/busy.gif"
      visible: playing
      playing: false
  }
      
animation

For a practical example, let's assume we want to make a web client request to a remote server, using drakma as our web client:


  ;; original version, EQL5 only

  (defun server-request (url parameters)
    "Runs request in a thread, returns after thread finished."
    (q> |playing| ui:*busy* t) ; start animation
    (let (response)
      ;; local event loop is needed for above 'busy' animation
      (qlet ((ev-loop "QEventLoop"))
        ;; worker thread
        (mp:process-run-function
         :server-request
         (lambda ()
           (setf response
                 (drakma:http-request url
                                      :method :post
                                      :parameters parameters))
           (|exit| ev-loop)))
        ;; main thread
        (|exec| ev-loop |QEventLoop.ExcludeUserInputEvents|))
      (q> |playing| ui:*busy* nil) ; stop animation
      response))
      

  ;; simpler variant, which also works in LQML

  (defun request ()
    "Runs request in a thread, returns after thread finished."
    (q> |playing| ui:*busy* t) ; start animation
    (let (response)
      ;; worker thread
      (mp:process-run-function
       :request
       (lambda ()
         (sleep 3) ; working hard...
         (setf response :ok)
         (qexit)))
      ;; main thread
      (qexec (* 60 1000)) ; timeout (ms)
      (q> |playing| ui:*busy* nil) ; stop animation
      response))
      

So, the animation gets started, but would stop immediately, waiting for the request to finish. Therefore we run the request in a thread. But how to continue to process animation events while waiting for the thread to finish?
Simple: use a local event loop, which will keep the animation running!

So, right after starting the thread, the local event loop will also start running, and will exit only if we explicitly call |exit| on it.

As soon as the thread finishes, the response variable will be set, and the local event loop will be exited, see (|exit| ev-loop), and our function server-request will behave just like if it were synchronous (despite running in a thread), which is preferable in simple cases.