Class: Tuile::Mouse::Router
- Inherits:
-
Object
- Object
- Tuile::Mouse::Router
- Defined in:
- lib/tuile/mouse/router.rb,
sig/tuile.rbs
Overview
Delivers Events to the component tree. Screen owns one and hands it every event the terminal reports; a component only answers the handlers it calls, and walks nothing itself.
screen.handle_mouse(Mouse::DownEvent.new(:left, 5, 2)) # what the loop does
Every event resolves against one path: the topmost popup containing the
point, else the tiled content unless a modal popup is open
(ScreenPane#mouse_root_at), then down through the shown children whose
Component#rect contains it. A rect is parent-relative, so the walk
converts the point as it descends and each component is handed the event
in its own coordinates — the same ones it paints in (D_relative_rect).
- DownEvent — focuses the innermost Component#focusable? on that
path, then offers Component#handle_mouse_down? innermost-first until one
component answers
true. That claimant becomes #grabbed. - ScrollEvent, MoveEvent — bubble the same way through
Component#handle_mouse_scroll? / Component#handle_mouse_move?, and grab
nothing. A move fires the enter/exit hooks first, and arrives only at
:hover. - DragEvent, UpEvent — skip the path entirely: they go to #grabbed, wherever the pointer is, and an up ends the grab.
The two geometries are deliberately different. Focus follows rect, so a
press on the dead tail a widget does not paint still focuses it; the
handlers bubble only along the prefix whose
Component#local_extent_rect contains the point, so that same press
activates nothing (D_extent).
UI-thread-confined.
Implementation details
The grab has three releases and no relinquishing: the up, the next
down (the up was lost — ssh and tmux do lose them), and any key
(#release_grab). None of the last two tells the grabbed component. Hiding
or detaching it does not end the grab either; the router just stops
delivering to it until the release (D_mouse_dispatch).
The hovered chain is synced, not toggled: a move re-resolves it and diffs, and #sync_hover — run by Screen#repaint — drops members that were detached, hidden or reparented since, firing their exits.
Instance Attribute Summary collapse
-
#grabbed ⇒ Component?
readonly
@return — the component whose Component#handle_mouse_down? claimed the press currently held.
-
#level ⇒ Symbol?
The tracking level Screen#run_event_loop asked the terminal for, one of LEVELS;
:hoverwhile no loop is running, so a spec may post anything.
Instance Method Summary collapse
-
#bubble(path, handler, event) ⇒ Component?
@param
path— root first. -
#dispatch(event) ⇒ void
@param
event. -
#extent_path(point) ⇒ ::Array[Hit]
@param
point— in screen coordinates. -
#focus_innermost(root, path) ⇒ void
A non-modal overlay is never focused into: it sits outside the key scope, so focus there would make every keystroke go dead (
D_overlay). -
#hovered ⇒ Component?
@return — the innermost component under the pointer, as of the last move under
:hover. -
#initialize(screen) ⇒ Router
constructor
@param
screen. -
#move(event) ⇒ void
@param
event. -
#press(event) ⇒ void
@param
event. -
#rect_path(root, point) ⇒ ::Array[Hit]
@param
root. -
#rehover(chain) ⇒ void
@param
chain— the new hovered chain, root first. -
#release(event) ⇒ void
@param
event. -
#release_grab ⇒ void
Ends the grab without telling the grabbed component.
-
#sync_hover ⇒ void
Drops the hovered-chain members that are no longer attached, shown, or children of the member before them, firing Component#handle_mouse_exit innermost first.
-
#within_extent(path) ⇒ ::Array[Hit]
@param
path.
Constructor Details
#initialize(screen) ⇒ Router
@param screen
57 58 59 60 61 62 63 |
# File 'lib/tuile/mouse/router.rb', line 57 def initialize(screen) @screen = screen @level = :hover @hovered = [] @grabbed = nil @grab_button = nil end |
Instance Attribute Details
#grabbed ⇒ Component? (readonly)
@return — the component whose Component#handle_mouse_down? claimed the press currently held.
75 76 77 |
# File 'lib/tuile/mouse/router.rb', line 75 def grabbed @grabbed end |
#level ⇒ Symbol?
The tracking level Screen#run_event_loop asked the terminal for, one of
LEVELS; :hover while no loop is running, so a spec may post
anything. An ungrabbed move is dropped below :hover — under :drag the
terminal reports one only while a button nobody claimed is held, and
enter/exit that fire sometimes are worse than none.
71 72 73 |
# File 'lib/tuile/mouse/router.rb', line 71 def level @level end |
Instance Method Details
#bubble(path, handler, event) ⇒ Component?
@param path — root first.
@param handler — a routed handle_mouse_…?.
@param event — in screen coordinates; each component is handed it converted to its own.
@return — the component that answered true.
185 186 187 188 189 190 191 192 193 |
# File 'lib/tuile/mouse/router.rb', line 185 def bubble(path, handler, event) # A handler may detach what is below it on the path (a click that swaps # a slot's occupant), so re-check before each delivery. hit = path.reverse_each.find do |h| h.component.attached? && h.component.__send__(handler, event.with(x: h.point.x, y: h.point.y)) end hit&.component end |
#dispatch(event) ⇒ void
This method returns an undefined value.
@param event
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
# File 'lib/tuile/mouse/router.rb', line 83 def dispatch(event) case event when DownEvent then press(event) when UpEvent then release(event) when ScrollEvent then bubble(extent_path(event.point), :handle_mouse_scroll?, event) when MoveEvent then move(event) when DragEvent # No route of its own: it is manufactured here from a move while # something holds the grab, so a posted one can only be a mistake. raise Tuile::Error, "a Mouse::DragEvent is the router's own; post " \ "Mouse::MoveEvent.new(:#{event.}, #{event.x}, #{event.y}) " \ "— or FakeScreen#drag — to drive a grabbed component" else raise TypeError, "not a routable Mouse::Event: #{event.inspect}" end end |
#extent_path(point) ⇒ ::Array[Hit]
@param point — in screen coordinates.
@return — the shown components under point whose extent
contains it, root first.
198 |
# File 'lib/tuile/mouse/router.rb', line 198 def extent_path(point) = within_extent(rect_path(@screen.pane.mouse_root_at(point), point)) |
#focus_innermost(root, path) ⇒ void
This method returns an undefined value.
A non-modal overlay is never focused into: it sits outside the key scope,
so focus there would make every keystroke go dead (D_overlay).
@param root
@param path
173 174 175 176 177 178 |
# File 'lib/tuile/mouse/router.rb', line 173 def focus_innermost(root, path) return if root.is_a?(Component::Overlay) && !root.modal? target = path.reverse_each.map(&:component).find(&:focusable?) @screen.focused = target unless target.nil? || target.active? end |
#hovered ⇒ Component?
@return — the innermost component under the pointer, as of
the last move under :hover.
79 |
# File 'lib/tuile/mouse/router.rb', line 79 def hovered = @hovered.last |
#move(event) ⇒ void
This method returns an undefined value.
@param event
153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/tuile/mouse/router.rb', line 153 def move(event) unless @grabbed.nil? if ComponentUtil.effectively_visible?(@grabbed) local = @grabbed.to_local(event.point) @grabbed.__send__(:handle_mouse_drag, DragEvent.new(@grab_button, local.x, local.y)) end return end return unless @level == :hover path = extent_path(event.point) rehover(path.map(&:component)) bubble(path, :handle_mouse_move?, event) end |
#press(event) ⇒ void
This method returns an undefined value.
@param event
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 |
# File 'lib/tuile/mouse/router.rb', line 124 def press(event) release_grab point = event.point pane = @screen.pane root = pane.mouse_root_at(point) path = rect_path(root, point) pane.dismissing_popups_outside(point, left: event. == :left) do focus_innermost(root, path) if event. == :left claimant = bubble(within_extent(path), :handle_mouse_down?, event) unless claimant.nil? @grabbed = claimant @grab_button = event. end end end |
#rect_path(root, point) ⇒ ::Array[Hit]
@param root
@param point — in root's parent's coordinates — screen coordinates, since every mouse root is a ScreenPane child.
@return — the shown components whose rect contains point,
root first, each paired with the point in its own coordinates. Tiled
siblings never overlap, so at most one child qualifies at each level.
212 213 214 215 216 217 218 219 220 221 |
# File 'lib/tuile/mouse/router.rb', line 212 def rect_path(root, point) path = [] component = root while component&.visible? && component.rect.contains?(point) point = Point.new(point.x - component.rect.left, point.y - component.rect.top) path << Hit.new(component:, point:) component = component.children.find { _1.visible? && _1.rect.contains?(point) } end path end |
#rehover(chain) ⇒ void
This method returns an undefined value.
@param chain — the new hovered chain, root first.
225 226 227 228 229 230 |
# File 'lib/tuile/mouse/router.rb', line 225 def rehover(chain) old = @hovered @hovered = chain (old - chain).reverse_each { _1.__send__(:handle_mouse_exit) } (chain - old).each { _1.__send__(:handle_mouse_enter) } end |
#release(event) ⇒ void
This method returns an undefined value.
@param event
142 143 144 145 146 147 148 149 |
# File 'lib/tuile/mouse/router.rb', line 142 def release(event) grabbed = @grabbed release_grab return if grabbed.nil? || !ComponentUtil.effectively_visible?(grabbed) local = grabbed.to_local(event.point) grabbed.__send__(:handle_mouse_up, event.with(x: local.x, y: local.y)) end |
#release_grab ⇒ void
This method returns an undefined value.
Ends the grab without telling the grabbed component.
102 103 104 105 |
# File 'lib/tuile/mouse/router.rb', line 102 def release_grab @grabbed = nil @grab_button = nil end |
#sync_hover ⇒ void
This method returns an undefined value.
Drops the hovered-chain members that are no longer attached, shown, or children of the member before them, firing Component#handle_mouse_exit innermost first. Idempotent.
111 112 113 114 115 116 117 118 |
# File 'lib/tuile/mouse/router.rb', line 111 def sync_hover valid = @hovered.each_with_index.take_while do |c, i| c.attached? && c.visible? && (i.zero? || c.parent.equal?(@hovered[i - 1])) end.size return if valid == @hovered.size rehover(@hovered.take(valid)) end |
#within_extent(path) ⇒ ::Array[Hit]
@param path
@return — the prefix whose extent contains the point — the
test each component answers in its own coordinates, so a widget that
paints less than its rect has a dead tail (D_extent).
204 |
# File 'lib/tuile/mouse/router.rb', line 204 def within_extent(path) = path.take_while { _1.component.local_extent_rect.contains?(_1.point) } |