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
-
#cleanup_interaction_state(object) ⇒ Object
Clear pressed/hover refs when an object is removed.
-
#dispatch_object_mouse_down(button, x, y) ⇒ Object
Dispatch mouse down to the topmost interactive object.
-
#dispatch_object_mouse_held(button, x, y) ⇒ Object
Dispatch mouse held to the object originally pressed with this button.
-
#dispatch_object_mouse_move(x, y, delta_x, delta_y) ⇒ Object
Dispatch mouse move: update hover state, handle drag.
-
#dispatch_object_mouse_scroll(x, y, direction, delta_x, delta_y) ⇒ Object
Dispatch scroll to the topmost interactive object under the cursor.
-
#dispatch_object_mouse_up(button, x, y) ⇒ Object
Dispatch mouse up.
-
#init_object_event_stores ⇒ Object
Initialize stores for object-level interaction state.
-
#register_interactive(object) ⇒ Object
Register an object as interactive (has event handlers).
-
#topmost_interactive_at(x, y) ⇒ Object
Find the topmost interactive object at the given coordinates.
-
#unregister_interactive(object) ⇒ Object
Unregister an object (no more event handlers).
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(, x, y) obj = topmost_interactive_at(x, y) return unless obj @pressed_objects[] = { object: obj, x: x, y: y } obj._fire_event(:mouse_down, MouseEvent.new(:down, , 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(, x, y) press = @pressed_objects[] return unless press press[:object]._fire_event(:mouse_held, MouseEvent.new(:held, , 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 |, info| drag_obj = info[:object] next unless drag_obj.interactive?(:drag) drag_obj._fire_event(:drag, MouseEvent.new(:drag, , 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(, x, y) obj = topmost_interactive_at(x, y) press = @pressed_objects.delete() event = MouseEvent.new(:up, , 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, , nil, x, y, nil, nil)) end end |
#init_object_event_stores ⇒ Object
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 |