module Cute
Overview
Easy to use event-oriented publisher/subscribe modelled after the Qt Framework.
Defined in:
cute.crcute/buffer_sink.cr
cute/interval_sink.cr
cute/signal.cr
cute/sink.cr
cute/timed_buffer_sink.cr
cute/version.cr
spec.cr
Constant Summary
-
VERSION =
"0.4.0"
Macro Summary
-
connect(signal, handler)
Connects a method as handler for a signal.
-
middleware(deff)
Creates a middleware.
-
signal(call, async = false)
Creates a signal.
-
spy(sender, signal)
Builds a signal spy listening on sender for signal.
Macro Detail
Connects a method as handler for a signal. The method must be given as prototype. The argument types are not important though, feel free to omit them. Returns the signal handler.
Example usage:
class Window
def initialize
@btn = Button.new
@btn_handler = Cute.connect @btn.clicked, on_button_clicked(x, y)
end
def on_button_clicked(x, y)
# ...
end
end
See samples/qt_connect.cr
for a runnable example.
Note: The handler may be called in a different Fiber.
Creates a middleware. A middleware offers the user of your class to extend its functionality by adding user-defined behaviour before calling the actualy middleware, giving the opportunity to modify arguments or the result value, or not call later stages at all.
The argument to the macro is a full method definition, whose code body will be called last. It's named the "final stage" because of that, and always exists.
Like Cute.signal
, the macro creates a class and makes the method return
it. You can then #add
middleware, get the #list
of added middleware
and modify it directly, or #call
it.
Example usage:
class Chat
Cute.middleware def send_message(body : String) : String
# `self` is the instance of `Chat` here.
puts "Sending #{body}"
"Sent #{body.size} bytes"
end
end
chat = Chat.new
chat.send_message.add { |body, yielder| yielder.call body.upcase }
chat.send_message.call("Hello") # => "Sent 5 bytes"
# Prints "Sending HELLO"
Calling behaviour
When called (Through #call
), the algorithm will run each middleware stage
in the order it was added (Or: As it appears in the #list
). That is, the
stage that was added first will be called first, the second one after that,
and finally the final stage.
It's possible to manually add or reorder the middleware stages by modifying
the #list
directly.
The final stage is called in the context of the host class instance. That means it behaves like a normal method, as if it was declared without the macro.
Caveats
For this to work you have to explicitly mark the argument and result types.
Also note that if your middleware decides to not call the next stage, you
still have to return something matching the return type. You can make it
easier by allowing nil
results.
This won't work:
# Swear filter
chat.send_message.add { |body, yielder| yielder.call(body) if body !~ /dang/ }
Possible solutions are these:
# Solution 1: Return something matching the return type
chat.send_message.add do |body, yielder|
if body !~ /dang/
yielder.call(body)
else
"Swearing is not allowed"
end
end
# Solution 2: Allow nil result
class Chat
Cute.middleware def send_message(body : String) : String?
# ...
end
end
# Now the previous example would work fine:
chat.send_message.add { |body, yielder| yielder.call(body) if body !~ /dang/ }
Creates a signal. A signal manages listeners (So called slots), which will be called when an event has been emitted. Emitting is the act of triggering a signal to announce an event to the listeners.
Creates the method name
, which returns a Signal
.
This method does not take any arguments. Instead, the call arguments are
type declarations for the signal arguments, and will be passed through
Signal#emit
to listeners as block arguments in the same order.
By default, the signal listeners are called in the fiber #emit
is
called from. You can override this by setting the optional argument
async to true
. In this case, the signal listeners will be called
in turn in a Fiber on their own.
Example usage:
class Button
Cute.signal clicked(x : Int32, y : Int32)
end
btn = Button.new
btn.clicked.on { |x, y| p x, y }
btn.clicked.emit 5, 4 # => Will print 5, 4
Note: You have to fully qualify all argument types to Cute.signal
.
If your signal does not require any arguments, you can omit the ()
:
class MyIO
Cute.signal data_received() # With empty parantheses
Cute.signal closed # Or without. Both are fine.
end
Signal listeners are called in the order they were connected. You can
disconnect them later by using Signal#disconnect
. Pass it the identifier
you got from #on
:
btn = Button.new
handle = btn.clicked.on { |x, y| p x, y }
btn.clicked.disconnect(handle) # Removes the connection again.
It's also possible to use a Channel
instead, if you want to wait for
an event in a blocking fashion. Use the #new_channel
method for this:
btn = Button.new
ch, handle = btn.clicked.new_channel # Create channel
x, y = ch.retrieve # Wait for event
btn.clicked.disconnect(handle) # Remove channel
The channel will transport the signal argument, or will use a Tuple
if the
signal uses multiple arguments. If the signal has no arguments, the channel
will be Channel(Nil)
instead.
Note: A channel is just like a handle. Make sure to disconnect it if you don't need it any longer.
You can also wait just once for an event using the #wait
method. This
method returns the signal arguments. Example:
btn = Button.new
x, y = btn.clicked.wait # Wait just once
This is especially useful if you're only interested in the event once or rarely. If you expect events regularly, you're better served using one of the other solutions above.
Look in samples/
for runnable examples.
Collecting signal emissions
Have a look at Cute::Sink
, and its documentation, on how you can collect
multiple emissions. This is also interesting to synchronize for
batch-processing, making it easy to manage asynchronous dependencies.
Naming convention
Signal names should be written in past-tense: The slots are usually called
when something already happened. So, a signal name like clicked
is good,
a signal name like click
not so much. This also helps avoiding name
clashes.
Testing
Using Cute.spy
, you can create signal spies, which will record all
emitted values. Please have a look at its documentation for further
information.
Builds a signal spy listening on sender for signal. signal is the
signal definition, like in Cute.signal
. Types must not be omitted.
Whenever a signal occurs, its value(s) are appended to the signal spy.
Full usage example:
require "cute/spec" # Or put it into your "spec_helper" file
class Button
Cute.signal clicked(x : Int32, y : Int32)
def click
clicked.emit(4, 5)
end
end
describe Button do
describe "#click" do
it "emits #clicked" do
btn = Button.new
clicked_spy = Cute.spy btn, clicked(x : Int32, y : Int32) # Create the spy
btn.click # Somehow emit the spied upon signal
clicked_spy.size.should eq 1 # Verify
clicked_spy[0].should eq({4, 5})
end
end
end
Note: Signals without arguments add nil
into the spy.