Class: Tuile::Component::Layout::Box
- Inherits:
-
Layout
- Object
- Layout
- Tuile::Component::Layout::Box
- Defined in:
- lib/tuile/component/layout/box.rb,
sig/tuile.rbs
Overview
Abstract base of the one-dimensional box layouts. Children are stacked along a main axis in the order they were added, each getting the extent its constraint asks for; across the cross axis they are sized one at a time, since nothing competes with them there. Vertical and Horizontal pick which axis is which.
class LoginForm < Tuile::Component::Layout::Vertical
def initialize
super(spacing: 1, padding: Insets[top: 1])
add(@prompt = Tuile::Component::Label.new, Fixed[4])
add(@user = Tuile::Component::TextField.new, Fixed[1], cross: Fixed[30])
add(@log = Tuile::Component::TextView.new, Expand[1])
end
end
The constraint names need no prefix inside a subclass — Ruby finds them on
Layout, an ancestor. Component classes are not on that chain and still do.
Children pack from the start edge, so with no Expand among them the
slack is simply left at the end: there is no filler component to add.
Nest boxes to vary the gap — a Vertical.new(spacing: 0) inside a
Vertical.new(spacing: 1) groups two rows tightly within a looser stack.
Implementation details
Every child-list mutation re-runs the whole pass, because in a box the children move: removing one shifts everything after it, and adding one shrinks every Expand share. (Absolute can skip this — there, siblings are independent.)
Main-axis resolution order, against
available = extent - padding - spacing * (children - 1):
- Fixed takes its cells, clamped to what is still unassigned.
- Percent takes its share of
available, likewise clamped. - Expand children split the residue by weight; the integer remainder goes to the earliest of them, one cell each.
So over-subscription starves in declaration order rather than raising: a child with nothing left gets an empty rect and paints nothing. Padding wider than the layout does the same to every child.
Direct Known Subclasses
Constant Summary collapse
- DEFAULT_PLACEMENT =
Constraints for a child wired in through
add_childinstead of #add. { main: Fixed[1], cross: Percent[100], align: :start }.freeze
- ALIGNMENTS =
Where a child narrower than the cross extent sits within it.
%i[start center end].freeze
Instance Attribute Summary collapse
-
#padding ⇒ Insets, Integer
@return — inset from this layout's own rect.
-
#spacing ⇒ Integer
@return — blank cells between adjacent children.
Instance Method Summary collapse
-
#add(child, main = Fixed[1], cross: Percent[100], align: :start) ⇒ Object
Adds a child — or every element of an Enumerable, all with the same constraints — and re-runs the layout.
-
#align_offset(align, slack) ⇒ Integer
@param
align— one of ALIGNMENTS. -
#build_rect(inner, main_offset, main_size, cross_offset, cross_size) ⇒ Object
@param
inner— #inner_rect, the origin both offsets are relative to. -
#cross_extent(rect) ⇒ Integer
@param
rect. -
#cross_placement(child, available) ⇒ [Integer, Integer]
@param
child. -
#distribute_expand(sizes, indices, slack) ⇒ void
Splits
slackbetween the Expand children by weight, writing the results intosizes. -
#initialize(spacing: 0, padding: 0) ⇒ Box
constructor
@param
spacing— blank cells between adjacent children;>= 0. - #inner_rect ⇒ Rect
-
#main_extent(rect) ⇒ Integer
@param
rect. -
#main_sizes(inner) ⇒ ::Array[Integer]
@param
inner— #inner_rect. -
#percent_of(extent, constraint) ⇒ Integer
@param
extent. -
#place_children(inner) ⇒ void
@param
inner— #inner_rect, known non-empty. -
#placement(child) ⇒ ::Hash[Symbol, Object]
@param
child. -
#rect=(new_rect) ⇒ void
@param
new_rect. -
#relayout ⇒ void
Recomputes and assigns every child's rect.
-
#remove(child) ⇒ void
Removes the child, forgets its constraints, and closes the gap it left by re-running the layout.
-
#validate_align(align) ⇒ void
@param
align. -
#validate_cross(constraint) ⇒ void
@param
constraint. -
#validate_main(constraint) ⇒ void
@param
constraint. -
#validate_spacing(cells) ⇒ Integer
@param
cells.
Constructor Details
#initialize(spacing: 0, padding: 0) ⇒ Box
@param spacing — blank cells between adjacent children; >= 0.
@param padding — inset from this layout's own rect; an Integer is coerced to a uniform Insets.
60 61 62 63 64 65 66 |
# File 'lib/tuile/component/layout/box.rb', line 60 def initialize(spacing: 0, padding: 0) super() @spacing = validate_spacing(spacing) @padding = Insets.coerce(padding) # Identity-keyed: two == children are still two distinct slots. @placements = {}.compare_by_identity end |
Instance Attribute Details
#padding ⇒ Insets, Integer
@return — inset from this layout's own rect.
72 73 74 |
# File 'lib/tuile/component/layout/box.rb', line 72 def padding @padding end |
#spacing ⇒ Integer
@return — blank cells between adjacent children.
69 70 71 |
# File 'lib/tuile/component/layout/box.rb', line 69 def spacing @spacing end |
Instance Method Details
#add(child, main = Fixed[1], cross: Percent[100], align: :start) ⇒ Object
Adds a child — or every element of an Enumerable, all with the same constraints — and re-runs the layout.
add(field, Fixed[1], cross: Fixed[30], align: :center)
add([ok, cancel], Fixed[1])
@param child
@param main — extent along the main axis.
@param cross — extent across it.
@param align — one of ALIGNMENTS — where a child narrower than the cross extent sits. Vertical / Horizontal say which edge :start is.
112 113 114 115 116 117 118 119 120 121 122 123 124 |
# File 'lib/tuile/component/layout/box.rb', line 112 def add(child, main = Fixed[1], cross: Percent[100], align: :start) if child.is_a? Enumerable child.each { add(_1, main, cross:, align:) } return end validate_main(main) validate_cross(cross) validate_align(align) add_child(child) @placements[child] = { main:, cross:, align: } relayout end |
#align_offset(align, slack) ⇒ Integer
@param align — one of ALIGNMENTS.
@param slack — unused cells across the axis.
237 238 239 240 241 242 243 |
# File 'lib/tuile/component/layout/box.rb', line 237 def align_offset(align, slack) case align when :center then slack / 2 when :end then slack else 0 end end |
#build_rect(inner, main_offset, main_size, cross_offset, cross_size) ⇒ Object
@param inner — #inner_rect, the origin both offsets are relative to.
@param main_offset — cells along the main axis.
@param main_size — extent along the main axis.
@param cross_offset — cells along the cross axis.
@param cross_size — extent along the cross axis.
@return — absolute screen rect for one child.
268 269 270 |
# File 'lib/tuile/component/layout/box.rb', line 268 def build_rect(inner, main_offset, main_size, cross_offset, cross_size) raise NotImplementedError, "#{self.class} must implement build_rect" end |
#cross_extent(rect) ⇒ Integer
@param rect
@return — the extent along the cross axis.
260 |
# File 'lib/tuile/component/layout/box.rb', line 260 def cross_extent(rect) = raise(NotImplementedError, "#{self.class} must implement cross_extent") |
#cross_placement(child, available) ⇒ [Integer, Integer]
@param child
@param available — cross extent of #inner_rect.
@return — offset from inner's start edge, and
extent, along the cross axis.
225 226 227 228 229 230 231 232 |
# File 'lib/tuile/component/layout/box.rb', line 225 def cross_placement(child, available) spec = placement(child) size = case (constraint = spec[:cross]) when Fixed then constraint.cells.clamp(0, available) else percent_of(available, constraint).clamp(0, available) end [align_offset(spec[:align], available - size), size] end |
#distribute_expand(sizes, indices, slack) ⇒ void
210 211 212 213 214 215 216 217 218 219 |
# File 'lib/tuile/component/layout/box.rb', line 210 def (sizes, indices, slack) slack = 0 if slack.negative? weights = indices.map { placement(children[_1])[:main].weight } total = weights.sum shares = weights.map { slack * _1 / total } # Under one cell is lost per floor, so the remainder can't outrun the # share count — the earliest Expand children each take one. (slack - shares.sum).times { |i| shares[i] += 1 } indices.each_with_index { |child_index, i| sizes[child_index] = shares[i] } end |
#inner_rect ⇒ Rect
163 164 165 166 |
# File 'lib/tuile/component/layout/box.rb', line 163 def inner_rect Rect.new(rect.left + padding.left, rect.top + padding.top, rect.width - padding.horizontal, rect.height - padding.vertical) end |
#main_extent(rect) ⇒ Integer
@param rect
@return — the extent along the main axis.
256 |
# File 'lib/tuile/component/layout/box.rb', line 256 def main_extent(rect) = raise(NotImplementedError, "#{self.class} must implement main_extent") |
#main_sizes(inner) ⇒ ::Array[Integer]
@param inner — #inner_rect.
@return — main-axis extent per child, in child order.
183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/tuile/component/layout/box.rb', line 183 def main_sizes(inner) count = children.size return [] if count.zero? available = [main_extent(inner) - (spacing * (count - 1)), 0].max sizes = Array.new(count, 0) = [] unassigned = available children.each_with_index do |child, i| case (constraint = placement(child)[:main]) when Expand then << i when Fixed then unassigned -= (sizes[i] = constraint.cells.clamp(0, unassigned)) else unassigned -= (sizes[i] = percent_of(available, constraint).clamp(0, unassigned)) end end (sizes, , unassigned) unless .empty? sizes end |
#percent_of(extent, constraint) ⇒ Integer
@param extent
@param constraint
248 |
# File 'lib/tuile/component/layout/box.rb', line 248 def percent_of(extent, constraint) = (extent * constraint.percent / 100.0).round |
#place_children(inner) ⇒ void
This method returns an undefined value.
@param inner — #inner_rect, known non-empty.
170 171 172 173 174 175 176 177 178 179 |
# File 'lib/tuile/component/layout/box.rb', line 170 def place_children(inner) sizes = main_sizes(inner) available = cross_extent(inner) offset = 0 children.each_with_index do |child, i| cross_offset, cross_size = cross_placement(child, available) child.rect = build_rect(inner, offset, sizes[i], cross_offset, cross_size) offset += sizes[i] + spacing end end |
#placement(child) ⇒ ::Hash[Symbol, Object]
@param child
@return — the child's main/cross/align.
252 |
# File 'lib/tuile/component/layout/box.rb', line 252 def placement(child) = @placements[child] || DEFAULT_PLACEMENT |
#rect=(new_rect) ⇒ void
This method returns an undefined value.
@param new_rect
138 139 140 141 |
# File 'lib/tuile/component/layout/box.rb', line 138 def rect=(new_rect) super relayout end |
#relayout ⇒ void
This method returns an undefined value.
Recomputes and assigns every child's rect. Silent until this layout has a rect of its own — #add runs during construction, long before a parent assigns one.
149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/tuile/component/layout/box.rb', line 149 def relayout return if rect.empty? inner = inner_rect if inner.empty? children.each { _1.rect = Rect.new(rect.left, rect.top, 0, 0) } else place_children(inner) end invalidate end |
#remove(child) ⇒ void
This method returns an undefined value.
Removes the child, forgets its constraints, and closes the gap it left by re-running the layout.
@param child
130 131 132 133 134 |
# File 'lib/tuile/component/layout/box.rb', line 130 def remove(child) super @placements.delete(child) relayout end |
#validate_align(align) ⇒ void
This method returns an undefined value.
@param align
308 309 310 311 312 |
# File 'lib/tuile/component/layout/box.rb', line 308 def validate_align(align) return if ALIGNMENTS.include?(align) raise ArgumentError, "expected one of #{ALIGNMENTS.inspect}, got #{align.inspect}" end |
#validate_cross(constraint) ⇒ void
This method returns an undefined value.
@param constraint
295 296 297 298 299 300 301 302 303 |
# File 'lib/tuile/component/layout/box.rb', line 295 def validate_cross(constraint) if constraint.is_a? Expand raise ArgumentError, "Expand is main-axis only — a child has no siblings competing " \ "across the axis; use Fixed or Percent for cross:" end return if constraint.is_a?(Fixed) || constraint.is_a?(Percent) raise ArgumentError, "expected Fixed or Percent for cross:, got #{constraint.inspect}" end |
#validate_main(constraint) ⇒ void
This method returns an undefined value.
@param constraint
286 287 288 289 290 |
# File 'lib/tuile/component/layout/box.rb', line 286 def validate_main(constraint) return if [Fixed, Percent, Expand].any? { constraint.is_a?(_1) } raise ArgumentError, "expected Fixed, Percent or Expand, got #{constraint.inspect}" end |
#validate_spacing(cells) ⇒ Integer
@param cells
@return — cells.
275 276 277 278 279 280 281 |
# File 'lib/tuile/component/layout/box.rb', line 275 def validate_spacing(cells) unless cells.is_a?(Integer) && !cells.negative? raise ArgumentError, "spacing expects a non-negative Integer, got #{cells.inspect}" end cells end |