Module: Tuile::LayoutPass Private

Defined in:
lib/tuile/layout_pass.rb,
sig/tuile.rbs

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Who may assign a rect right now. Component#rect= is refused outside the parent's Component#relayout, and this is the bracket that says when one is running:

LayoutPass.run(container) { relayout }    # inside the container

A pass also records which children it has placed so far. Before the first, Component#invalidate_layout drops a mark the container makes on itself — the pass that would answer it is the one running — and until a child's turn, Component#rect_stale? reports its rect as the previous pass's (LayoutPass.unplaced?).

Thread-local rather than an ivar, because a tree with no Screen places children too, so there is no one object to hang the state on.

Tuile-internal: a member may change or vanish with no migration note. The rule it enforces is Component#rect='s and is documented there.

Constant Summary collapse

PLACING =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

The thread-local naming what is placing children right now.

Returns:

  • (Symbol)
:tuile_placing
PLACED =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

The thread-local holding the children the current placer has assigned a rect so far — an identity Set, or nil before the first.

Returns:

  • (Symbol)
:tuile_placed
MAX_ROUNDS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Drain rounds a Screen#flush_layout or a detached Component#flush_layout runs before giving up. No container sizes itself from its children, so a tree settles in about its depth; one that is still marking past this is feeding a pass's output back into its input — a Component::Scroller whose content_rows the app derives from the content's width, say — and would otherwise hang the UI thread.

Returns:

  • (Integer)
50

Class Method Summary collapse

Class Method Details

.before_first_placement?(component) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether component is the running placer and has placed no child yet — the window in which a mark it makes on itself is redundant.

@param component

Parameters:

Returns:

  • (Boolean)


163
164
165
# File 'lib/tuile/layout_pass.rb', line 163

def before_first_placement?(component)
  Thread.current[PLACING].equal?(component) && Thread.current[PLACED].nil?
end

.check(component) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

@param component — the one whose rect is being assigned.

Parameters:



93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/tuile/layout_pass.rb', line 93

def check(component)
  current = Thread.current[PLACING]
  return if !current.nil? && current.equal?(placer(component))

  if component.parent.nil?
    raise Tuile::Error, "#{component} has no parent to place it; to size a detached tree, " \
                        "hold it in a Layout::Absolute (add(tree, rect), then flush_layout)"
  end
  raise Tuile::Error, "#{component}'s rect assigned outside #{component.parent}'s relayout; change " \
                      "what the parent places it by instead (Absolute#constrain, Box#constrain, " \
                      "Overlay#placement=)"
end

.drain(root, rounds = 0) ⇒ Integer

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Runs every Component#relayout root's tree owes, to a fixpoint — the one drain behind both Screen#flush_layout and a detached Component#flush_layout.

The dirty flags are the queue. Each round walks the tree pre-order and runs whatever is marked, so a parent lays out before the children whose rects it just wrote, and those children run in the same walk. A mark on something earlier in the order — a child's pass marking its parent — waits for the next round, and the drain ends on the first walk that finds nothing marked. A flag is cleared by the pass that answers it, so a container is never run twice for one mark, and a raise strands nothing.

@param root — the tree's root: the ScreenPane, or a detached root.

@param rounds — rounds already run in this flush, so a caller draining more than once — the screen, re-checking anchors — keeps one count.

@return — the rounds run in total.

Parameters:

  • root (Component)
  • rounds (Integer) (defaults to: 0)

Returns:

  • (Integer)


122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/tuile/layout_pass.rb', line 122

def drain(root, rounds = 0)
  while drain_round(root)
    next rounds += 1 if rounds < MAX_ROUNDS

    culprits = []
    root.walk_tree { culprits << _1 if _1.layout_dirty? }
    raise Tuile::Error, "layout did not settle after #{MAX_ROUNDS} rounds, still marking: " \
                        "#{culprits.first(5).map(&:inspect).join(", ")} — a relayout keeps changing its " \
                        "own input (content_rows derived from the content's width?)"
  end
  rounds
end

.note_placement(component) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Records that the running pass has assigned component its rect. Past the first, a mark on the placer is kept rather than dropped.

@param component

Parameters:



155
156
157
# File 'lib/tuile/layout_pass.rb', line 155

def note_placement(component)
  (Thread.current[PLACED] ||= Set.new.compare_by_identity) << component
end

.placer(component) ⇒ Component, ...

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

What may assign component's rect: its parent, whose Component#relayout does. The exception is the ScreenPane, which has no parent whose pass could place it, so the Screen does (D_tree_first keeps the screen out of the tree). Those are the only two cases — this is a closed rule rather than a hook a component may answer for itself.

@param component

Parameters:

Returns:



86
87
88
# File 'lib/tuile/layout_pass.rb', line 86

def placer(component)
  component.is_a?(ScreenPane) ? component.screen : component.parent
end

.refuse_nested ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

The guard at the top of both drains, Screen#flush_layout and a detached Component#flush_layout.



72
73
74
75
76
77
# File 'lib/tuile/layout_pass.rb', line 72

def refuse_nested
  return unless running?

  raise Tuile::Error, "flush_layout inside #{Thread.current[PLACING]}'s relayout: the running pass " \
                      "has not placed its children yet, so a nested drain would read stale rects"
end

.run(placer) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Runs the block with placer as the one thing whose children's rects may be assigned. Nests: the enclosing pass is restored on the way out.

@param placer

@return — the block's value.

Parameters:

Returns:

  • (Object)


50
51
52
53
54
55
56
57
58
59
# File 'lib/tuile/layout_pass.rb', line 50

def run(placer)
  outer = Thread.current[PLACING]
  outer_placed = Thread.current[PLACED]
  Thread.current[PLACING] = placer
  Thread.current[PLACED] = nil
  yield
ensure
  Thread.current[PLACING] = outer
  Thread.current[PLACED] = outer_placed
end

.running? ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether any pass is placing children right now — what a drain refuses to run inside (refuse_nested), and Screen#focused= defers its geometry under.

Returns:

  • (Boolean)


64
# File 'lib/tuile/layout_pass.rb', line 64

def running? = !Thread.current[PLACING].nil?

.unplaced?(component) ⇒ Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether component's placer is running its pass right now and has not reached it yet — so its rect is the previous pass's, though no flag says so: Component#perform_relayout clears the placer's before the body runs.

@param component

Parameters:

Returns:

  • (Boolean)


172
173
174
175
176
177
178
# File 'lib/tuile/layout_pass.rb', line 172

def unplaced?(component)
  placing = Thread.current[PLACING]
  return false if placing.nil? || !placing.equal?(placer(component))

  placed = Thread.current[PLACED]
  placed.nil? || !placed.include?(component)
end