Class: Tuile::Listeners
- Inherits:
-
Object
- Object
- Tuile::Listeners
- Defined in:
- lib/tuile/listeners.rb,
sig/tuile.rbs
Overview
A listener slot: the callables registered on one on_foo, fired with one
Event. The reader is the registrar:
.on_click { save } # a block
field.on_value_change << method(:preview) # anything callable
field.on_value_change.remove(method(:preview)) # …removed, holding nothing
field.on_value_change.empty? # => true
Method#== compares receiver and name, so a widget unsubscribes with the
expression it subscribed with and holds nothing. A Proc equals only itself,
which is why the block form returns the Proc it registered rather than the
list.
Declare one with Declare, never by hand. It is not a collection: #each, #size and #include? are the whole surface.
There is no setter, and no clear
The semantics are append, and remove your own, and the absent on_foo= is
the point. A replaceable slot made every claim a contention: wherever the
gem wires a listener onto a child it also exposes for tuning
(DateTimeField#date_field, RadioGroup#list, TabSheet#strip), an app
reaching for that slot silently broke the widget. With no replace operation
that failure cannot be written.
An empty list is meaningful, and each slot's rdoc says what its empty means
Nothing here reads empty as "nothing to do": while empty, a key-claiming slot declines the key so it keeps bubbling and Screen#on_error re-raises. Declare's transition block is for the widget that must install something when the slot stops being empty.
Duplicates are allowed: two adds fire twice, and one #remove balances one #add.
Defined Under Namespace
Modules: Declare
Instance Method Summary collapse
-
#<<(callable) ⇒ self
sord duck - #call looks like a duck type, replacing with untyped Appends
callableand returns self, so registrations chain. -
#add(callable) ⇒ Object
sord duck - #call looks like a duck type, replacing with untyped sord duck - #call looks like a duck type, replacing with untyped Appends
callableand returns it, so a lambda can be held for removal. -
#each ⇒ void
sord duck - #call looks like a duck type, replacing with untyped Yields each listener in registration order.
-
#empty? ⇒ Boolean
@return — whether nothing is registered — a state each slot gives its own meaning.
-
#fire(event) ⇒ void
Calls every listener in registration order — so the gem's own listener runs before any app's, a widget having wired itself in its constructor.
-
#include?(callable) ⇒ Boolean
sord duck - #call looks like a duck type, replacing with untyped @param
callable. -
#initialize(name:, &claim_changed) ⇒ Listeners
constructor
@param
name— the slot's name (:on_click), used in error messages. -
#remove(callable) ⇒ Boolean
sord duck - #call looks like a duck type, replacing with untyped Removes the first occurrence of
callable. -
#size ⇒ Integer
@return — how many listeners are registered, duplicates counted.
-
#takes_event?(callable) ⇒ Boolean
sord duck - #call looks like a duck type, replacing with untyped @param
callable.
Constructor Details
#initialize(name:, &claim_changed) ⇒ Listeners
@param name — the slot's name (:on_click), used in error messages.
47 48 49 50 51 |
# File 'lib/tuile/listeners.rb', line 47 def initialize(name:, &claim_changed) @name = name @claim_changed = claim_changed @entries = [] end |
Instance Method Details
#<<(callable) ⇒ self
sord duck - #call looks like a duck type, replacing with untyped
Appends callable and returns self, so registrations chain.
field.on_value_change << method(:preview) << method(:log)
@param callable — the listener.
85 86 87 88 |
# File 'lib/tuile/listeners.rb', line 85 def <<(callable) add(callable) self end |
#add(callable) ⇒ Object
sord duck - #call looks like a duck type, replacing with untyped
sord duck - #call looks like a duck type, replacing with untyped
Appends callable and returns it, so a lambda can be held for removal.
cb = field.on_value_change.add(->(e) { preview(e.value) })
field.on_value_change.remove(cb)
Arity is settled here rather than per fire, so a listener that cannot take the event raises at registration instead of later inside a repaint on the loop thread.
Deliberately does not call Screen#check_locked, alone among the gem's
mutations: that would mean holding an owner, hence a Screen reach inside
Component::HasValue and Component::HasValidation, plain mixins with
none.
@param callable — the listener.
@return — callable.
71 72 73 74 75 76 77 |
# File 'lib/tuile/listeners.rb', line 71 def add(callable) entry = Entry.new(callable, takes_event?(callable)) was_empty = @entries.empty? @entries << entry @claim_changed&.call(true) if was_empty callable end |
#each ⇒ void
This method returns an undefined value.
sord duck - #call looks like a duck type, replacing with untyped Yields each listener in registration order.
121 122 123 |
# File 'lib/tuile/listeners.rb', line 121 def each @entries.each { yield _1.callable } end |
#empty? ⇒ Boolean
@return — whether nothing is registered — a state each slot gives its own meaning.
113 |
# File 'lib/tuile/listeners.rb', line 113 def empty? = @entries.empty? |
#fire(event) ⇒ void
This method returns an undefined value.
Calls every listener in registration order — so the gem's own listener runs before any app's, a widget having wired itself in its constructor.
A listener that raises aborts the fire: the ones behind it do not run and the exception propagates, as a single slot did. Isolating each listener would turn a bug into a partial fire that nothing reports.
@param event — passed to every listener that declared a parameter.
134 135 136 137 138 |
# File 'lib/tuile/listeners.rb', line 134 def fire(event) # Snapshot: a listener may add or remove during the fire, and the # newcomer is meant to run on the *next* one. @entries.dup.each { _1.takes_event ? _1.callable.call(event) : _1.callable.call } end |
#include?(callable) ⇒ Boolean
sord duck - #call looks like a duck type, replacing with untyped
@param callable
@return — whether callable is registered.
109 |
# File 'lib/tuile/listeners.rb', line 109 def include?(callable) = @entries.any? { _1.callable == callable } |
#remove(callable) ⇒ Boolean
sord duck - #call looks like a duck type, replacing with untyped
Removes the first occurrence of callable.
Not Array#delete, which drops every occurrence: one remove balances
one #add, the only rule that composes when a widget and an app happen to
register the same method(:x).
@param callable — the listener to remove.
@return — whether it was there.
98 99 100 101 102 103 104 105 |
# File 'lib/tuile/listeners.rb', line 98 def remove(callable) index = @entries.index { _1.callable == callable } return false if index.nil? @entries.delete_at(index) @claim_changed&.call(false) if @entries.empty? true end |
#size ⇒ Integer
@return — how many listeners are registered, duplicates counted.
116 |
# File 'lib/tuile/listeners.rb', line 116 def size = @entries.size |
#takes_event?(callable) ⇒ Boolean
sord duck - #call looks like a duck type, replacing with untyped
@param callable
@return — whether #fire passes it the event.
145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
# File 'lib/tuile/listeners.rb', line 145 def takes_event?(callable) raise ArgumentError, "#{@name}: expected a callable, got #{callable.inspect}" unless callable.respond_to?(:call) arity = callable.is_a?(Proc) || callable.is_a?(Method) ? callable.arity : callable.method(:call).arity required = arity.negative? ? -arity - 1 : arity if required > 1 raise ArgumentError, "#{@name}: a listener takes the event or nothing, but #{callable.inspect} requires " \ "#{required} arguments" end # A negative arity means optional or splat parameters, which can absorb # the event; only an exact zero declares it wants none. !arity.zero? end |