Class: Tuile::Listeners

Inherits:
Object
  • Object
show all
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:

button.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

Constructor Details

#initialize(name:, &claim_changed) ⇒ Listeners

@param name — the slot's name (:on_click), used in error messages.

Parameters:

  • name: (Symbol)


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.

Parameters:

  • callable (Object)

Returns:

  • (self)


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.

Parameters:

  • callable (Object)

Returns:

  • (Object)


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.

Returns:

  • (Boolean)


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.

Parameters:



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.

Parameters:

  • callable (Object)

Returns:

  • (Boolean)


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.

Parameters:

  • callable (Object)

Returns:

  • (Boolean)


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.

Returns:

  • (Integer)


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.

Parameters:

  • callable (Object)

Returns:

  • (Boolean)


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