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.

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

Instance Method Summary collapse

Constructor Details

#initialize(screen) ⇒ Router

@param screen

Parameters:



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

#grabbedComponent? (readonly)

@return — the component whose Component#handle_mouse_down? claimed the press currently held.

Returns:



64
65
66
# File 'lib/tuile/mouse/router.rb', line 64

def grabbed
  @grabbed
end

#levelSymbol?

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)


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.

Parameters:

Returns:



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

Parameters:



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.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[Component]

@param point

@return — the shown components under point whose extent contains it, root first.

Parameters:

Returns:



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

Parameters:



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

#hoveredComponent?

@return — the innermost component under the pointer, as of the last move under :hover.

Returns:



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

def hovered = @hovered.last

#move(event) ⇒ void

This method returns an undefined value.

@param event

Parameters:



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

Parameters:



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.button == :left) do
    focus_innermost(root, path) if event.button == :left
    claimant = bubble(path.take_while { _1.extent_rect.contains?(point) }, :handle_mouse_down?, event)
    unless claimant.nil?
      @grabbed = claimant
      @grab_button = event.button
    end
  end
end

#reachable?(component) ⇒ Boolean

@param component

@return — whether component is attached and it and every ancestor are shown.

Parameters:

Returns:

  • (Boolean)


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.

Parameters:

Returns:



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.

Parameters:



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

Parameters:



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_grabvoid

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_hovervoid

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