Module: Ruby2D::Window::ObjectEventDispatch

Included in:
Ruby2D::Window
Defined in:
lib/ruby2d/window/object_events.rb

Overview

Per-object event dispatching for interactive renderables

Instance Method Summary collapse

Instance Method Details

#cleanup_interaction_state(object) ⇒ Object

Clear pressed/hover refs when an object is removed



33
34
35
36
37
# File 'lib/ruby2d/window/object_events.rb', line 33

def cleanup_interaction_state(object)
  @hovered_object = nil if @hovered_object == object

  @pressed_objects.delete_if { |_btn, info| info[:object] == object }
end

#dispatch_object_mouse_down(button, x, y) ⇒ Object

Dispatch mouse down to the topmost interactive object



94
95
96
97
98
99
100
# File 'lib/ruby2d/window/object_events.rb', line 94

def dispatch_object_mouse_down(button, x, y)
  obj = topmost_interactive_at(x, y)
  return unless obj

  @pressed_objects[button] = { object: obj, x: x, y: y }
  obj._fire_event(:mouse_down, MouseEvent.new(:down, button, nil, x, y, nil, nil))
end

#dispatch_object_mouse_held(button, x, y) ⇒ Object

Dispatch mouse held to the object originally pressed with this button. Stays on the originally pressed object even if the cursor drags off, mirroring :drag's "this interaction belongs to this object" semantics.



149
150
151
152
153
154
# File 'lib/ruby2d/window/object_events.rb', line 149

def dispatch_object_mouse_held(button, x, y)
  press = @pressed_objects[button]
  return unless press

  press[:object]._fire_event(:mouse_held, MouseEvent.new(:held, button, nil, x, y, nil, nil))
end

#dispatch_object_mouse_move(x, y, delta_x, delta_y) ⇒ Object

Dispatch mouse move: update hover state, handle drag



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/ruby2d/window/object_events.rb', line 123

def dispatch_object_mouse_move(x, y, delta_x, delta_y)
  obj = topmost_interactive_at(x, y)

  # Update hover state
  if obj != @hovered_object
    if @hovered_object
      @hovered_object._fire_event(:hover_out, MouseEvent.new(:hover_out, nil, nil, x, y, nil, nil))
    end
    if obj
      obj._fire_event(:hover, MouseEvent.new(:hover, nil, nil, x, y, nil, nil))
    end
    @hovered_object = obj
  end

  # Handle drag for each pressed button
  @pressed_objects.each do |button, info|
    drag_obj = info[:object]
    next unless drag_obj.interactive?(:drag)

    drag_obj._fire_event(:drag, MouseEvent.new(:drag, button, nil, x, y, delta_x, delta_y))
  end
end

#dispatch_object_mouse_scroll(x, y, direction, delta_x, delta_y) ⇒ Object

Dispatch scroll to the topmost interactive object under the cursor. Hit-tests fresh (like :mouse_down/:mouse_up) rather than reusing the hover state, which mouse_move updates only on movement — scrolling without first moving would otherwise target the wrong object or nothing.



160
161
162
163
164
165
# File 'lib/ruby2d/window/object_events.rb', line 160

def dispatch_object_mouse_scroll(x, y, direction, delta_x, delta_y)
  obj = topmost_interactive_at(x, y)
  return unless obj

  obj._fire_event(:mouse_scroll, MouseEvent.new(:scroll, nil, direction, x, y, delta_x, delta_y))
end

#dispatch_object_mouse_up(button, x, y) ⇒ Object

Dispatch mouse up. :mouse_up mirrors :mouse_down: it fires on the originally-pressed object regardless of release location, and also on the topmost interactive object under the cursor at release. When the press and release are on the same object, only one :mouse_up fires (and :click follows).



107
108
109
110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ruby2d/window/object_events.rb', line 107

def dispatch_object_mouse_up(button, x, y)
  obj = topmost_interactive_at(x, y)
  press = @pressed_objects.delete(button)
  event = MouseEvent.new(:up, button, nil, x, y, nil, nil)

  obj._fire_event(:mouse_up, event) if obj
  if press && press[:object] != obj
    press[:object]._fire_event(:mouse_up, event)
  end

  if press && obj == press[:object]
    obj._fire_event(:click, MouseEvent.new(:click, button, nil, x, y, nil, nil))
  end
end

#init_object_event_storesObject

Initialize stores for object-level interaction state



8
9
10
11
12
# File 'lib/ruby2d/window/object_events.rb', line 8

def init_object_event_stores
  @interactive_objects = []
  @hovered_object = nil
  @pressed_objects = {}
end

#register_interactive(object) ⇒ Object

Register an object as interactive (has event handlers)



15
16
17
18
19
20
21
22
23
24
# File 'lib/ruby2d/window/object_events.rb', line 15

def register_interactive(object)
  return if @interactive_objects.include?(object)

  index = @interactive_objects.index { |obj| obj.z > object.z }
  if index
    @interactive_objects.insert(index, object)
  else
    @interactive_objects.push(object)
  end
end

#topmost_interactive_at(x, y) ⇒ Object

Find the topmost interactive object at the given coordinates. A Renderable is only hit-tested while it's in the scene graph: a shape built with add: false (or one that's been removed) has handlers but is not drawn, so it must not silently swallow clicks or shadow visible objects beneath it. Hidden objects (visible = false) stay in the scene graph and keep receiving events, as documented. Button and other self-managing interactives aren't scene-graph members by design (their visual is) and register/unregister themselves, so they're exempt.



47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/ruby2d/window/object_events.rb', line 47

def topmost_interactive_at(x, y)
  # This runs on every mouse-move event, so the common cases must not
  # allocate: the enumerator + pair-array sort below costs ~12µs per
  # event on wasm mruby even with zero interactive objects.
  objs = @interactive_objects
  size = objs.size
  return nil if size.zero?

  # @interactive_objects is kept sorted by z at registration, but an
  # object's z (or its wrapped visual's z) can change at runtime. Verify
  # the order in one allocation-free pass; while it holds (nearly every
  # frame), hit-test top-down by iterating backwards in place — among
  # equal-z objects the highest index (most recently registered) is
  # checked first, matching the stable sort in the fallback.
  in_order = true
  i = 1
  while i < size
    if objs[i - 1].z > objs[i].z
      in_order = false
      break
    end
    i += 1
  end

  if in_order
    i = size - 1
    while i >= 0
      obj = objs[i]
      unless obj.is_a?(Renderable) && !@object_set.key?(obj)
        return obj if obj.contains?(x, y)
      end
      i -= 1
    end
    return nil
  end

  # A runtime z change broke the registration order: re-establish it with
  # a sort stable on the array's current index, preserving registration
  # order among equal-z objects (most recently registered stays topmost).
  objs.each_with_index.sort_by { |obj, idx| [obj.z, idx] }.reverse_each do |obj, _idx|
    next if obj.is_a?(Renderable) && !@object_set.key?(obj)
    return obj if obj.contains?(x, y)
  end
  nil
end

#unregister_interactive(object) ⇒ Object

Unregister an object (no more event handlers)



27
28
29
30
# File 'lib/ruby2d/window/object_events.rb', line 27

def unregister_interactive(object)
  @interactive_objects.delete(object)
  cleanup_interaction_state(object)
end