Class: Tuile::Mouse::Router

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

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

Instance Method Summary collapse

Constructor Details

#initialize(screen) ⇒ Router

@param screen

Parameters:



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.

Returns:



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.

Returns:

  • (Symbol, nil)


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.

Parameters:

Returns:



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

Parameters:



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.button}, #{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.

Parameters:

Returns:

  • (::Array[Hit])


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

Parameters:



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.

Returns:



79
# File 'lib/tuile/mouse/router.rb', line 79

def hovered = @hovered.last

#move(event) ⇒ void

This method returns an undefined value.

@param event

Parameters:



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

Parameters:



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.button == :left) do
    focus_innermost(root, path) if event.button == :left
    claimant = bubble(within_extent(path), :handle_mouse_down?, event)
    unless claimant.nil?
      @grabbed = claimant
      @grab_button = event.button
    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.

Parameters:

Returns:

  • (::Array[Hit])


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.

Parameters:



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

Parameters:



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

Parameters:

  • path (::Array[Hit])

Returns:

  • (::Array[Hit])


204
# File 'lib/tuile/mouse/router.rb', line 204

def within_extent(path) = path.take_while { _1.component.local_extent_rect.contains?(_1.point) }