Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide a per App event catcher #383

Closed
ccoupe opened this issue Nov 7, 2017 · 66 comments
Closed

Provide a per App event catcher #383

ccoupe opened this issue Nov 7, 2017 · 66 comments
Assignees
Milestone

Comments

@ccoupe
Copy link

ccoupe commented Nov 7, 2017

In issue #287 and others, @backorder asks for additional control of events delivered to a window (a shoes app) to write a Shoes GUI builder in Shoes. Instead of overriding canvas.click we could create an app.event = {proc} and modify all the shoes_app_event in app.c to to call it, if specified.

It would return true if its handled the event and false if it wants normal Shoes event handling. Or vice versa? Each of the shoes_app_ could build a hash {event: :click, x: y: key: } and its up to the that overseer method to do the right thing with the hash.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 7, 2017

This sounds like a good idea. I would like to suggest a simplification for the API: use event ! It would integrate better in Shoes DSL and still make it distinguishable from other slot events.

Take a look at the following code sample. Notice params could be "x, y", "direction", etc. but the sample below would do fine for a generic event handler. What do you think?

Shoes.app do
   event do |evt, params|
      case evt
         when :click
            para "click: x #{params.first}, y #{params.last}"
         when :wheel
            para "wheel: direction #{params}"
         else
            para "evt #{evt}, params #{params.inspect}"
      end
   end
end
Shoes.app do
   event do |evt, direction|
      if evt.eql? :wheel
         para "wheel is rolling #{direction}"
      end
   end
end

@ccoupe
Copy link
Author

ccoupe commented Nov 8, 2017

One thing I think would be interesting is to control the events of one window (shoes app) from another app - not just interception but feeding events to it . It needs more thought and some extra events like app_loaded and app_closed

@IanTrudel
Copy link
Collaborator

The way things work in Shoes is that a block should be enough. Shoes.app can be assigned to a variable and its members are readily available, which would include event. So it would be possible to write something like...

Shoes.app do
   w = Shoes.app {}
   w.event do |evt, p|
      # second window event handler here
   end
end

By the way, I am not against using a proc but hoping to keep the API clean and simple. Shoes API always favoured block instead of proc. Though perhaps you have sample case that would make sense to use a proc instead.

not just interception but feeding events to it

Sounds interesting. Would you care to elaborate on your thoughts?

It needs more thought and some extra events like app_loaded and app_closed

Isn't it like start and finish? aka #25 and #26

@ccoupe
Copy link
Author

ccoupe commented Nov 8, 2017

GUI testing was my thought. Collect the events generated by a human - serialize, Play back as often as you like.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 8, 2017

This would be an amazing feature. It would certainly elevate Shoes. I'm liking it. A lot. :)))

@ccoupe
Copy link
Author

ccoupe commented Nov 8, 2017

The event method is more Shoes like. There are issues. In the user supplied event handler above - we need to provide a way to tell shoes to distribute the event to the shoes_canvas methods . An example would help. shoes_app_click(shoes_app *app, ...) would build an 'event' (class or hash) and look in app->event_handler for something to call - the optional handler and pass the arg(s) for the event. That handler has to return a value to the handler invocation from back to shoes_app_click() - pass it on to shoes_ canvas_clicked or don't pass it and then shoe_app_click() has to return SHOES_OK

Shoes.app do
  def handler(time, evt, hash)
    if evt == :click
      # save click to to yaml
      return true # pass event to shoes
    end
    return false # DISABLE all other events
  end
  
  para "Collector";
  button "start" do 
    # open yaml file
    @watch = window do 
      para "you are being spied upon"
      edit_line "text entry"
    end
    @watch.events = handler
  end
  button "stop" do
    # close yaml file
    @watch.events = NIL
  end

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 8, 2017

This approach could even allow multiple event handlers if need to. By the way, it doesn't need to be plural because an event handler only handles one event at a time. I would also suggest to use Shoes approach to remove an event handler using @watch.event.remove such as any other Shoes element would behave.

Also, just to make things clear (as in writing specs), Shoes should assume any handler let events be passed to other handlers by default (return true even when not explicitly written).

@ccoupe
Copy link
Author

ccoupe commented Nov 8, 2017

The other side is replay or sending events to an app. We'll need a new method in app.c, call it 'play_event' for now.

Shoes.app do
  para "tester"
  button "Chose App & yaml" do
    # open yaml
    # load script in new @window
  end
  button "start"
    yaml.each_line do |args|
       # build event
       @window.play_event event
       sleep 0.1
    end
  end
end

This is not well thought out - just illustration.

@IanTrudel
Copy link
Collaborator

What do you think about playback instead of play_event ?

It is noteworthy that sleep had some troubles on Windows in the past and may still be in need of fixing.

@ccoupe
Copy link
Author

ccoupe commented Nov 8, 2017

This approach could even allow multiple event handlers if need.

Exactly! A gui builder might want several handlers it can switch to depending on the state of the built app.

Also, just to make things clear (as in writing specs), Shoes should assume any handler let events be passed to other handlers by default (return true even when not explicitly written).

Examples drag in assumptions :-) There is no chain of handlers - there is the default and the one set in app->event_handler. It has to deal with all event types. Writing handlers would be a very difficult task but this proposal is just a cut out in the event flow for clever people to exploit. Even describing it is a problem.

ccoupe pushed a commit that referenced this issue Nov 8, 2017
ccoupe pushed a commit that referenced this issue Nov 8, 2017
@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 8, 2017

I am getting cold feet with the proc approach after looking at your work in progress. Shoes should really focus on blocks. The good news is that a proc can in fact be passed as block. Here is a sample to illustrate how one would do. So we don't need to sacrifice the simplicity of Shoes for flexibility. We get both by using a block approach.

Shoes.app do
    def event(&block)
        yield if block_given?
    end

    event do
        para "block\n"
    end

    p = proc { para "proc\n" }

    event &p
end

@ccoupe
Copy link
Author

ccoupe commented Nov 9, 2017

Yeah, there is a lot going on and proc may not be correct - 'event do ..end` is essentially a canvas method - I want one app.event method (could be a block, perhaps) - one issue with blocks is returning true so the event cutout will call the canvas functions normally. Probably not a big issue but I'm not writing event handlers in ruby so what do I know?

The big issue with a block is all the C app event functions need to know that there is a cutout to call (or not). I need to find the internal C for obj.respond_to? I haven't found it yet. One way forward would be to have a app.event_handler = t/f (that solves the tricky remove issues so that's good). So

Shoes.app do
   @w = window "controlled" do
   end
   @w.event do |time, event, args_hash| 
      # return true for shoes to process this event normally
      # return false if we don't want normal
   end
   para "Controller" 
   button "start" do @w.event_handler = true end
   button "stop"  do @w.event_handler = false end
end

I'm OK with that two step process. I kind of like it.

I 'm warming up, a lot to having an Event class object being passed to the block/proc

   @w.event do |evt| 
      if evt.type == :keypress
        evt.status = false # ignore keyevent
     if evt,type == :click 
       evt.args["top"] = 200; evt.args["left"]= 300; 
       evt.status = true
     end
  end

That solves the return value issue - when the Event obj is created in C app__code the status field will be set true (pass it on to canvas methods) and the event handler can change change the event. - a lot. In the C code for app_shoes_clicked after calling the handler with the event it can use the status and contents of the Event object sent/returns instead of the args on the initial call. That would allow uber-control of the event system.

@IanTrudel
Copy link
Collaborator

RE: return value

blocks can have a return value but it won't use return keyword. It's no different from, say, array methods with blocks (collect, select, etc).

Shoes.app do
	def event(&block)
		retval = yield if block_given?
		para "retval: #{retval}\n"
	end

	event do
		true
	end

	event do
		false
	end
end

RE: app.event_handler = t/f

This is way too C-ish here. Perhaps we should consider supporting event.enable and event.disable (or start/stop in a timer/animate parlance).

Now just remember that it should be modelled based on other Shoes events (click, release, keydown, etc) where they don't really get to be turned on/off. And it seems that remove doesn't exist either. Not sure why I thought there was but we cannot remove those events (we may override them though).

My experience with Shoes events so far is that I always handled the logic by myself. If something had to be momentarily unavailable, I had instance variables to that effect, e.g. colours sample and curve control point sample. So we should keep things in the spirit of Shoes. Or get new shiny feature for all the events. :)

I 'm warming up, a lot to having an Event class object being passed to the block/proc

It would be a good idea but we need to make sure it makes sense. Right now everything is growing in complexity very fast. It's easy to get over engineered. There is actually no return value issue (see above). The evt.args would be a pain to program with compared to block parameters.

However, having an Event class would make it easy to implement things like the hash {event: :click, x: y: key: } you mentioned earlier because we could have an instance of Event do evt.to_yaml.

@ccoupe
Copy link
Author

ccoupe commented Nov 9, 2017

consider supporting event.enable and event.disable

It's not a canvas thing with multiple user handlers for each and every event type and for each slot - that would be over engineering and a lot of code and name pollution . It's an app thing! app.events = t/f works for me because the handler has to deal with all events so plural is appropriate.

It would be a good idea but we need to make sure it makes sense. Right now everything is growing in complexity very fast. It's easy to get over engineered. There is actually no return value issue (see above).

Perhaps a simple request is not so simple? I'm writing the code and I don't want to write any more than I have to considering only one to two people will ever use this feature. But when I do, it ought to be useful for things you and I haven't thought enough about.

The evt.args would be a pain to program with compared to block parameters.

It's only an pseudo code example - I happen to like |time, event, x, y {event specific stuff in hash} but I haven't begun to see whats common and whats to be in the hash. case type is not a big improvement over writing case evt.type - remember no body is going to use it - it's so deep in the wood we probably don't need to document it.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 9, 2017

It's an app thing! app.events = t/f works for me because the handler has to deal with all events so plural is appropriate.

We both agree it's on app level. Let's say we would deal with event as any other Shoes events. Most apps that will use a global event handler will use it all the time but let's say you absolutely need a way to turn on and off events. It would look like this:

Shoes.app do
   event do
      if @status
         # perform event selection and action
      end
   end

   button "start" do @status = true end
   button "stop"  do @status = false end

   start { @status = true }
end

RE: events vs event

We both have been around for a long time. Plural form is a rare thing in programming and applies to things like arrays, databases, etc. Events tend to be pooled and sent one at a time. For example, you have GetMessage, TranslateMessage and DispatchMessage in a Windows application. The messages can be just about anything. Still no plural.

Perhaps a simple request is not so simple? I'm writing the code and I don't want to write any more than I have to considering only one to two people will ever use this feature. But when I do, it ought to be useful for things you and I haven't thought enough about.

It ought to be useful. That's why we have a conversation about it!

RE: Event class

There is no compelling reason for an Event class at this time. It creates an additional unnecessary layer of complexity for very little gain.

The mandatory hash in the block might come as a problem because it is not a necessity for most use cases. In fact, we may just have an array instead and do just fine, e.g. |time, evt, x, y| and subsequently do [time, evt, x, y].to_yaml to save for playback.

@IanTrudel
Copy link
Collaborator

Normally you are the voice of reason when it comes to Shoes legacy. It kinda feels the other way around right now.

The truth is that a simple event handler would do. It matches Shoes philosophy and how things work in Shoes and I can see all my event dreams be feasible with it. Why do you want to make it so complicated?

@IanTrudel
Copy link
Collaborator

Here is something that would embody all the features you want but without the complexity. Perhaps you would better understand what I am talking about if you see the code.

Shoes.app do
   event do |e, t, args|
      case e
      when :click
         x, y = args
         para "click at #{t} with coordinates #{x}@#{y}\n"
      when :wheel
         para "wheel direction #{args}\n" 
         app.event.next # prevent slot wheel events and move to next event
      when :motion
         left, top = args
         para "motion at #{left}@#{top}\n"
      else
         # saving for playback
         [e, t, args].to_yaml
         # ...
      end if @status
   end

   button "start" do @status = true end
   button "stop"  do @status = false end

   button "playback" do
      # load some yaml file
      yaml.each_line do |e, t, args|
         # time can be passed to replicate the execution timeline
         # ...or use a number such as 0.1 for equivalent to sleep 0.1
         app.event.playback e, t, args
      end
   end

   button "clear" do
      handler = proc { |e, t, args|
         para "proc: #{e}, #{t}, #{args.inspect}\n"
         app.event.next
      }

      event &handler
   end

   start { @status = true }
end

@dredknight
Copy link
Contributor

code seems legit I like it. Simple and useful.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 9, 2017

A small revision for the suggested API. You could name app.events with an s if it is generally considered and behaving like a pool of events, only and only one instance per window and is preferably an internal class (cannot be instantiated). event block should however remain without an s. Making it a class attached to app (or window or so) will allow us to prevent polluting app.* namespace and make it easy to grow the event API as we go.

  • app.events
    • return an array of events
  • app.events.to_yaml
    • your friendly neighbourhood YAML output
  • app.events.record [filename] or app.events.recording [filename]
    • enable internal recording of events, in-memory by default
    • if a filename is provided, events will be dumped (YAML) in the file as they come up. Useful for apps that may randomly crash and need to be able to keep tracks of all events.
  • app.events.playback event, time, parameters
    • playback the given event
    • time/delay for the playback
    • additional parameters pertaining to the given event
  • app.events.next
    • stop dispatching the event to other handlers and move onto the next event

@ccoupe
Copy link
Author

ccoupe commented Nov 10, 2017

Wow. Lots to ideas to process - I should just wait another day and maybe the code will appear!

Shoes.app do
   event do |e, t, args|
      case e
      when :click
         x, y = args
         para "click at #{t} with coordinates #{x}@#{y}\n"
   ....
      end if @status
   end
   button "start" do @status = true end
   button "stop"  do @status = false end

the C code in app.c has no way to know that you have chosen @status as your variable. It has no way to know that you've have an event block in your code. Look at app.c - its not big or tricky - the event handlers are all together. It't can assume you have because that would be all existing scripts - it has to be told you have one.

I don't want to write an Events class unless I have to but there maybe some compelling reasons like getters and setters for all the different evt types - for example keypress doesn't have x, and y.. if event args was an array like some examples above then you have know (document) what the array contents are for each eventype.

app.events.playback event

would only work if app.events was an obj that respnded to 'playback_events' so app.events can't be an array. It could be object of class EventManager (or some other name) but would require you write
app.event = EventManager.new() and assign your block to some method of that object which is less shoes like than what I have know. However, it has some benefits - an initializer so It can inform shoea_app_clicked ... that there code to call into and since its an object of Shoes we can depend on the method name and data defined in there. Also a place to keep the list of 'events'

Yes, it just grew again and it's not shoes like.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 10, 2017

Wow. Lots to ideas to process - I should just wait another day and maybe the code will appear!

Are we grumpy again? Please, bear with me. I'm doing my best so we have a good understanding of the problem and its solutions.

RE: @status

The block is called from C but its execution is done by the Ruby interpreter. Basically, we don't need to worry about it. This technique has proven to be working over and over. This is what I used in colour and curve control point samples, Numinoes and several other Shoes programs. It's a not an issue. It's already working.

To make sure you totally understand: C side only needs to prepare what it will send to the block (evt, time, parameters) and then call the Ruby function to execute the block. Ruby execute the block and has access to all relevant global/class/instance variables.

would only work if app.events was an obj that respnded to 'playback_events' so app.events can't be an array. It could be object of class EventManager (or some other name) but would require you write app.event = EventManager.new() and assign your block to some method of that object which is less shoes like than what I have know.

Actually, we can subclass Array but it's not a necessity. I am suggesting a coherent API here but we can work out some details. EventManager would do. The user doesn't have direct access to EventManager (similar to Canvas). Shoes will internally instantiate it into app.events when a new Shoes.app/Window/Dialog is created.

How does it sound?

@ccoupe
Copy link
Author

ccoupe commented Nov 10, 2017

Lots of good ideas but C Ruby api and the existing code base (fricking macros will be the death of us all) will determine what is possible. It's always confusing at the C level with the junction/mixin of app vs canvas .

To make sure you totally understand: C side only needs to prepare what it will send to the block (evt, time, parameters) and then call the Ruby function to execute the block. Ruby execute the block and has access to all relevant global/class/instance variables.

If only that were true. - event is not like click or keypress or a widget. Your canvas event {|args| block} preference is really annoying at the C level. Not sure why but it is. Deep in the woods. I'll figure it out.

@IanTrudel
Copy link
Collaborator

IanTrudel commented Nov 10, 2017

RE: Macroland and mixins

We both brought significative improvements to Shoes internals but there are still legacy code haunting us. We might open an issue related to this.

Macroland would need improvements and simplification, or a whole new approach. For example, macros for native widgets are using C convention but we would be able to completely move shoes/types/* to Ruby if Ruby convention would used instead. Shoes widgets are written in C but they are in fact Ruby calls. We are shortchanging ourselves here.

If only that were true. - event is not like click or keypress or a widget. Your canvas event {|args| block} preference is really annoying at the C level. Not sure why but it is. Deep in the woods. I'll figure it out.

I am not sure where the hurdles are. My latest suggestions are based on already existing things in Shoes. Some of the proof-of-concepts I wrote before included extending DSL with blocks.

It should be noted it's not necessary to mixin when it comes to app.events. Only instantiating EventManager in Shoes.app and write an app.events method that always return the said EventManager instance. That should do the trick.

By the way, I thought about how to return an array and we should just settle for app.events.to_a as it is the convention in Ruby.

@ccoupe
Copy link
Author

ccoupe commented Dec 9, 2017

Finally, some real progress on non-native widgets! Here is Tests/events/event4.rb
non-native-events
and the terminal report from the event handler

ccoupe@bronco:~/Projects/shoes3$ minlin/shoes Tests/events/event4.rb 
event called: click at 116,181 mods: control_shift
  for widget: (Shoes::Types::Image) width 200 height 200
event called: click at 276,192 mods: control
  for widget: (Shoes::Types::Svg) width 200 height 200
event called: click at 484,163 mods: shift
  for widget: (Shoes::Types::Plot) width 200 height 200
event called: click at 0,279 mods: 
  for widget: (Shoes::Types::Button) width 80 height 30
event called: click at 255,557 mods: control

More to do of course but seems like this is going to work.

ccoupe pushed a commit that referenced this issue Dec 9, 2017
ccoupe pushed a commit that referenced this issue Dec 9, 2017
* next up, textblocks and shapes
@IanTrudel
Copy link
Collaborator

The results speak for themselves, very good indeed. Thanks for sharing your progress.

Quick trick question: What happens with events when something is on top of something else?

image

Shoes.app(width: 200, height: 200) do
   @img1 = image "#{DIR}/static/shoes-icon-walkabout.png", left: 0, top: 0, width: 200, height: 200
   @img2 = image "#{DIR}/static/shoes-icon-blue.png", left: 50, top: 50, width: 100, height: 100
end

@ccoupe
Copy link
Author

ccoupe commented Dec 9, 2017

Why not write a a few lines of Shoes to find out?

Shoes.app(width: 200, height: 200) do
   @img1 = image "#{DIR}/static/shoes-icon-walkabout.png", left: 0, top: 0, width: 200, height: 200
   @img2 = image "#{DIR}/static/shoes-icon-blue.png", left: 50, top: 50, width: 100, height: 100
   event do |evt|
    $stderr.puts "event called: #{evt.type} at #{evt.x},#{evt.y} mods: #{evt.modifiers}"
    if evt.object 
      # Note: for Textblocks the evt.obj is the String of the text block
      $stderr.puts "  for widget: #{evt.object.class} width #{evt.width} height #{evt.height}"
    end
    evt.accept = true
   end
end

I notice a problem, perhaps, depending how how you think it should work. Speaking of which, maybe some one could try using this "feature" with better use cases.

ccoupe pushed a commit that referenced this issue Dec 9, 2017
ccoupe pushed a commit that referenced this issue Dec 9, 2017
ccoupe pushed a commit that referenced this issue Dec 10, 2017
* not working yet since there can be slot handlers and they need
  to be sent to non-native widget (svg,plot,image...)
  Much like click does
* Just checking in the code.
ccoupe pushed a commit that referenced this issue Dec 11, 2017
* can be sent to the event block (if there is one)
* clean up the C code a bit.
@ccoupe
Copy link
Author

ccoupe commented Dec 12, 2017

Preliminary - docs for events

ccoupe pushed a commit that referenced this issue Dec 15, 2017
* manual update - points to wiki
* Tests/events/capture.rb - example
  Time handling is wrong.
ccoupe pushed a commit that referenced this issue Dec 15, 2017
* capture/replay in simple/samples/chipmunk.rb
* work in progress
ccoupe pushed a commit that referenced this issue Dec 16, 2017
* save more info in capture
* need app.replay_event()
  assumes we have shoes_native_move_cursor
ccoupe pushed a commit that referenced this issue Dec 17, 2017
* replay wait times are wrong but, It Works!
ccoupe pushed a commit that referenced this issue Dec 18, 2017
* better replay timing
* a button press is now :btn_activate for event handler/blocks instead
  of :click. Updated Test/events/* to accomodate - it still calls
  the button proc/block if there is one.
* won't move the cursor on replay - really hard/impossible? to do and
  violates many GUI principles - only one mouse pointer/cursor
ccoupe pushed a commit that referenced this issue Dec 20, 2017
* replay effort will stall soon. It may not be possible.
@ccoupe
Copy link
Author

ccoupe commented Dec 20, 2017

Warning - I'm going to merge the event branch to master branch very soon. It is not feature complete but it does answer #287 and you get jky modifiers for click,wheel,motion (another issue to close). The event handling stuff only happens if the user asks for it - harmless to existing scripts. Without an active I need this developer to help, it can lay dormant in master.

I also need to update my box to something new (Mint 18.1 Cinnamon is my choice) and it's very likely my health status and attention will wane soon.

@dredknight
Copy link
Contributor

Great! What do you mean by "health status and attention" ? Mint does not support Shoes so you are stepping down from shoes? or just taking a break to check the new features ?

Please do not leave @ccoupe :)!

@ccoupe
Copy link
Author

ccoupe commented Dec 21, 2017

Mint is a derivative of Ubuntu. Shoes will run. Health means I need to get a biopsy and none of the outcomes it will show will be pleasant. That's all I know, but having been down the cancer path before I know what happens to 'attention' Leaving is not up to me in this case.

@ccoupe ccoupe added this to the 3.3.5 milestone Dec 21, 2017
@dredknight
Copy link
Contributor

I am so sorry to hear that. I am a coeliac but I found it out not before I almost died 2-3 years ago. If I can give any advice at all, it is the fish and buckwheat diet combined with mangnesium oil, it fixed my broken stomach and intestines almost completely since then.

@ccoupe I wish you all good! We will be sticking around!

@IanTrudel
Copy link
Collaborator

@ccoupe let's hope that you get good news on those biopsies. You are a generous and very capable man. I would be sad to see you go. We also accomplished so much together despite the frictions between us. Don't you worry about Shoes right now. You need to take good care of yourself first.

@ccoupe
Copy link
Author

ccoupe commented Dec 22, 2017

If you have ever been in the meat grinder of the health care system you'll remember that you had plenty of free time waiting for test results, scheduling delays for doctors and treatments and so on. Since I can't know the the future, I'm just going to continue on with Shoes maintenance because it pleases me to find and fix bugs and do the right thing for the code we have.

despite the frictions

Heart felt discussions about the Shoes 'vision' and future.

@IanTrudel
Copy link
Collaborator

RE: health care system

Welcome to Canada. :)

Since I can't know the the future, I'm just going to continue on with Shoes maintenance because it pleases me to find and fix bugs and do the right thing for the code we have.

May you be able to continue to enjoy it for many years to come. Hopefully you will continue to grant me the privilege to be your wing man.

Heart felt discussions about the Shoes 'vision' and future.

It sure is. Eventually everything will fall in place as it usually does.

@ccoupe
Copy link
Author

ccoupe commented Jan 13, 2018

Closing issue. If the 3.3.5 event mechanism doesn't work in the real world (for an actual app) then create new issues.

@ccoupe ccoupe closed this as completed Jan 13, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants