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.
- 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#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[Component]
@param
point. -
#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. -
#reachable?(component) ⇒ Boolean
@param
component. -
#rect_path(root, point) ⇒ ::Array[Component]
@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.
Constructor Details
#initialize(screen) ⇒ Router
@param screen
46 47 48 49 50 51 52 |
# File 'lib/tuile/mouse/router.rb', line 46 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.
64 65 66 |
# File 'lib/tuile/mouse/router.rb', line 64 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.
60 61 62 |
# File 'lib/tuile/mouse/router.rb', line 60 def level @level end |
Instance Method Details
#bubble(path, handler, event) ⇒ Component?
@param path — root first.
@param handler — a routed handle_mouse_…?.
@param event
@return — the component that answered true.
168 169 170 171 172 |
# File 'lib/tuile/mouse/router.rb', line 168 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. path.reverse_each.find { |c| c.attached? && c.__send__(handler, event) } end |
#dispatch(event) ⇒ void
This method returns an undefined value.
@param event
72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/tuile/mouse/router.rb', line 72 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[Component]
@param point
@return — the shown components under point whose
extent contains it, root first.
177 178 179 |
# File 'lib/tuile/mouse/router.rb', line 177 def extent_path(point) rect_path(@screen.pane.mouse_root_at(point), point).take_while { _1.extent_rect.contains?(point) } end |
#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
157 158 159 160 161 162 |
# File 'lib/tuile/mouse/router.rb', line 157 def focus_innermost(root, path) return if root.is_a?(Component::Overlay) && !root.modal? target = path.reverse_each.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.
68 |
# File 'lib/tuile/mouse/router.rb', line 68 def hovered = @hovered.last |
#move(event) ⇒ void
This method returns an undefined value.
@param event
139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/tuile/mouse/router.rb', line 139 def move(event) unless @grabbed.nil? drag = DragEvent.new(@grab_button, event.x, event.y) @grabbed.__send__(:handle_mouse_drag, drag) if reachable?(@grabbed) return end return unless @level == :hover path = extent_path(event.point) rehover(path) bubble(path, :handle_mouse_move?, event) end |
#press(event) ⇒ void
This method returns an undefined value.
@param event
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/tuile/mouse/router.rb', line 113 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(path.take_while { _1.extent_rect.contains?(point) }, :handle_mouse_down?, event) unless claimant.nil? @grabbed = claimant @grab_button = event. end end end |
#reachable?(component) ⇒ Boolean
@param component
@return — whether component is attached and it and every
ancestor are shown.
208 209 210 211 212 213 214 |
# File 'lib/tuile/mouse/router.rb', line 208 def reachable?(component) return false if component.nil? || !component.attached? cursor = component cursor = cursor.parent while cursor&.visible? cursor.nil? end |
#rect_path(root, point) ⇒ ::Array[Component]
@param root
@param point
@return — the shown components whose rect contains
point, root first. Tiled siblings never overlap, so at most one child
qualifies at each level.
186 187 188 189 190 191 192 193 194 |
# File 'lib/tuile/mouse/router.rb', line 186 def rect_path(root, point) path = [] component = root while component&.visible? && component.rect.contains?(point) path << component 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.
198 199 200 201 202 203 |
# File 'lib/tuile/mouse/router.rb', line 198 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
131 132 133 134 135 |
# File 'lib/tuile/mouse/router.rb', line 131 def release(event) grabbed = @grabbed release_grab grabbed.__send__(:handle_mouse_up, event) if reachable?(grabbed) end |
#release_grab ⇒ void
This method returns an undefined value.
Ends the grab without telling the grabbed component.
91 92 93 94 |
# File 'lib/tuile/mouse/router.rb', line 91 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.
100 101 102 103 104 105 106 107 |
# File 'lib/tuile/mouse/router.rb', line 100 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 |