TimelineEvents | A Coroutine-Based Event System

Showcase your libraries, tools and other projects that help your fellow love users.
User avatar
Posts: 5
Joined: Sun Nov 03, 2019 5:50 pm

Re: TimelineEvents | A Coroutine-Based Event System

Post by babulous » Wed Nov 06, 2019 12:49 pm

yetneverdone wrote:
Mon Nov 04, 2019 11:19 pm
The example seems complex, could be simplified i guess with more explanation?
I edited the example to include some comments for clarification.
pgimeno wrote:
Tue Nov 05, 2019 11:30 pm
I've just run into one. I'm writing a very straightforward input function that lets you enter characters and delete them. In your example, you create a branch in order to read TextInput, but to me it would be much clearer if I could poll both TextInput and KeyPress with a non-blocking function, in a loop where I would also call E.Step(). As things are, I have to repeat the display code both for the deletion and for the typing branches.
it's cases like these why i created certain events like E.PollMouseActivity. An E.PollKeyboardActivity could probably be useful for something simple like that. I've done some testing with text boxes that have more functionality and my solution always tends to be more branching. Here's my example from GitHub modified, admittedly it gets quite a bit more complex, but can easily be scaled up to include more functionality like; cursor movement, delete key, copy, paste, all without increasing branching complexity much further.

Code: Select all

local E = require "tlevent"


  print("What's your name?")
  local name = ""
  local enter_name = E.Branch(function() -- wrapping branch
    local old_name = ""
    E.Branch("loop", function() -- display
      print("Enter your name: " .. name)
      old_name = name
      repeat E.Step() until name ~= old_name
    E.Branch("loop", function() -- typing
      local text = E.PollTextInput()
      name = name .. text
    E.Branch("loop", function() -- deleting
      name = name:sub(1, -2)
  E.G.Kill(enter_name) -- will kill all the child branches too
  print("Hello " .. name .. ", nice to meet you!")
  print("[Press any key to exit]")
Also, I think I just realized what you're meaning when you say "non-blocking events." There are undocumented functions for "Passive" events. It's used internally for E.PollMouseActivity. I didn't document them because most scenarios can also be done with branching, but also I planned on changing how passives work by allowing the creation of timelines with E() inside of timelines instead of using E.Branch. This allows them to be manually updated automatically inside of the timeline. Although I've decided to give the whole thing an overhaul so I guess that doesn't matter much now. In the new version I already have it working to create non branching timelines inside of other timelines.

Here's the source for E.PollMouseActivity.

Code: Select all

function E.PollMouseActivity()
  local pmm = E.Passive(E.PollMouseMove)
  local pmp = E.Passive(E.PollMousePress)
  local pmr = E.Passive(E.PollMouseRelease)
  local pmw = E.Passive(E.PollMouseWheel)
  while true do
    E.PassiveStep(pmm, pmp, pmr, pmw)
    if E.IsPassiveDone(pmm) then return "MouseMoved",    E.GetPassiveResults(pmm) end
    if E.IsPassiveDone(pmp) then return "MousePressed",  E.GetPassiveResults(pmp) end
    if E.IsPassiveDone(pmr) then return "MouseReleased", E.GetPassiveResults(pmr) end
    if E.IsPassiveDone(pmw) then return "MouseWheel",    E.GetPassiveResults(pmw) end
Here's an equivalent in the new version.

Code: Select all

function TL.Event.MouseActivity()
  local pmm = TL(TL.Event.MouseMoved)
  local pmp = TL(TL.Event.MousePressed)
  local pmr = TL(TL.Event.MouseReleased)
  local pmw = TL(TL.Event.WheelMoved)
  while true do
    pmm:Step(); if pmm:IsDone() then return "MouseMoved",    pmm:GetResults() end
    pmp:Step(); if pmp:IsDone() then return "MousePressed",  pmp:GetResults() end
    pmr:Step(); if pmr:IsDone() then return "MouseReleased", pmr:GetResults() end
    pmw:Step(); if pmw:IsDone() then return "WheelMoved",    pmw:GetResults() end
This isn't possible in the current version because of some old optimizations in place that prevent garbage collection of timelines unless they explicitly die or are killed. But in the new one they'll clear up even if they just fall out of scope.

User avatar
Party member
Posts: 1905
Joined: Sun Oct 18, 2015 2:58 pm
Location: Valencia, ES

Re: TimelineEvents | A Coroutine-Based Event System

Post by pgimeno » Wed Nov 06, 2019 2:13 pm

To be clear, what I meant by non-blocking is similar to the difference between Channel:demand (blocking) and Channel:pop (non-blocking). The former waits until there's a message; the latter returns nil if there are no messages.

At first I figured I could poll a non-blocking TextInput first, and if there was no text (it returned nil), then poll a non-blocking KeyPressed in order to know if Return or Backspace was pressed. But getting the logic behind that right, is more complicated than it seems, when order is taken into account and multiple keys are allowed per frame. You could, for example, get a keypress for a control key after a textinput key, and think it was not pressed in the textinput event, when in fact it was.

One idea would be to return both keypressed and textinput with the same poll, but it doesn't seem easy. Sadly, keypressed comes before textinput; if it was the other way around, you could store the textinput without passing it down, and on keypressed, send both keypressed and textinput (or nil or false if the key did not generate textinput). Maybe a hack can be done by peeking the LÖVE event queue on keypressed, to see if the next event is the textinput event generated by this key press. Dirty as hell, but that's what we have. And complicated by the fact that on Android, according to my tests, keyreleased comes before textinput too.

With the version still on GitHub, and using the hack I explained in an earlier post, this is a simplified version of how I implemented the input for my template; note drawing is persistent between frames:

Code: Select all

  local str = ''
    erase_str_background(x, y, maxlen + 1)
    love.graphics.print(str .. "_", x, y)
    local k = TL.PollTextInput()
    if k == '\b' and str ~= '' then
      str = str:sub(1, -2)
    elseif k >= '0' and k <= '9' then
      str = str .. k
      if #str > maxlen then
        str = str:sub(1, maxlen)
  until k == '\r' or k == '\n'
  return str
That's clearer to me than using multiple timelines.

Post Reply

Who is online

Users browsing this forum: No registered users and 4 guests