Module: Muxr::LayoutManager
- Defined in:
- lib/muxr/layout_manager.rb
Overview
Pure functions that turn (layout_name, pane_count, screen_rect) into an array of pane rectangles. No mutable state; safe to call repeatedly on every render. Following xmonad, layouts decide geometry — users never resize panes by hand.
Defined Under Namespace
Classes: Rect
Constant Summary collapse
- LAYOUTS =
%i[tall wide columns rows grid spiral centered stack monocle].freeze
Class Method Summary collapse
-
.centered(count, area, master_index = 0) ⇒ Object
Three-column master: master occupies the centre column full-height; the remaining panes are dealt alternately to a left and a right column and stacked within each.
-
.columns(count, area) ⇒ Object
Equal-width, full-height vertical strips, side by side.
- .compute(layout, count, area, focused_index: 0, master_index: 0) ⇒ Object
-
.grid(count, area) ⇒ Object
Roughly square grid.
-
.monocle(count, area, _focused_index = 0) ⇒ Object
All panes occupy the full area; the focused pane is the one drawn last (the Renderer is responsible for the z-order).
-
.neighbor(rects, focused_index, direction) ⇒ Object
Return the index of the closest pane in ‘direction` (:left/:right/:up/:down) from the focused pane.
- .overlap_extent(a_start, a_size, b_start, b_size) ⇒ Object
-
.rows(count, area) ⇒ Object
Equal-height, full-width horizontal strips, stacked.
-
.spiral(count, area) ⇒ Object
Fibonacci spiral: each pane takes half of the remaining region, splitting vertically then horizontally in alternation, so panes wind inward toward the bottom-right.
-
.stack(count, area, focused_index = 0) ⇒ Object
Accordion: the focused pane expands to fill the leftover height while the others collapse to short “title sliver” rows, all stacked vertically.
-
.stack_column(rects, indices, x, y, w, total_h) ⇒ Object
Stack the given pane indices vertically within a single column, dividing the height evenly (remainder to the topmost panes).
-
.tall(count, area, master_index = 0) ⇒ Object
Master pane on the left taking half the width; remaining panes stack vertically on the right, dividing the remaining height evenly.
-
.wide(count, area, master_index = 0) ⇒ Object
The transpose of ‘tall`: master pane spans the full width across the top half; remaining panes sit side-by-side in the bottom half, dividing the remaining width evenly.
Class Method Details
.centered(count, area, master_index = 0) ⇒ Object
Three-column master: master occupies the centre column full-height; the remaining panes are dealt alternately to a left and a right column and stacked within each. With a single slave there is no symmetry to keep, so it falls back to a simple master/slave vertical split (like ‘tall`).
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/muxr/layout_manager.rb', line 143 def centered(count, area, master_index = 0) master_index = master_index.clamp(0, count - 1) return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1 others = (0...count).to_a - [master_index] rects = Array.new(count) if others.length == 1 master_w = [area.w / 2, 1].max rects[master_index] = Rect.new(area.x, area.y, master_w, area.h) rects[others[0]] = Rect.new(area.x + master_w, area.y, [area.w - master_w, 1].max, area.h) return rects end master_w = [area.w / 2, 1].max side_w = area.w - master_w left_w = [side_w / 2, 1].max right_w = [side_w - left_w, 1].max rects[master_index] = Rect.new(area.x + left_w, area.y, master_w, area.h) left = others.select.with_index { |_, i| i.even? } right = others.select.with_index { |_, i| i.odd? } stack_column(rects, left, area.x, area.y, left_w, area.h) stack_column(rects, right, area.x + left_w + master_w, area.y, right_w, area.h) rects end |
.columns(count, area) ⇒ Object
Equal-width, full-height vertical strips, side by side. No master.
88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/muxr/layout_manager.rb', line 88 def columns(count, area) base_w = area.w / count rem = area.w - base_w * count rects = [] x = area.x count.times do |i| w = base_w + (i < rem ? 1 : 0) rects << Rect.new(x, area.y, w, area.h) x += w end rects end |
.compute(layout, count, area, focused_index: 0, master_index: 0) ⇒ Object
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# File 'lib/muxr/layout_manager.rb', line 17 def compute(layout, count, area, focused_index: 0, master_index: 0) return [] if count <= 0 master_index = master_index.clamp(0, count - 1) focused_index = focused_index.clamp(0, count - 1) case layout when :tall then tall(count, area, master_index) when :wide then wide(count, area, master_index) when :columns then columns(count, area) when :rows then rows(count, area) when :grid then grid(count, area) when :spiral then spiral(count, area) when :centered then centered(count, area, master_index) when :stack then stack(count, area, focused_index) when :monocle then monocle(count, area, focused_index) else raise ArgumentError, "Unknown layout: #{layout.inspect}" end end |
.grid(count, area) ⇒ Object
Roughly square grid. Each row stretches its panes to fill the full width so an underfull bottom row doesn’t leave gaps.
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/muxr/layout_manager.rb', line 210 def grid(count, area) cols_per_row = Math.sqrt(count).ceil rows = (count.to_f / cols_per_row).ceil base_h = area.h / rows h_rem = area.h - base_h * rows rects = [] idx = 0 y = area.y rows.times do |r| remaining = count - idx in_row = [cols_per_row, remaining].min row_h = base_h + (r < h_rem ? 1 : 0) col_w = area.w / in_row w_rem = area.w - col_w * in_row x = area.x in_row.times do |c| w = col_w + (c < w_rem ? 1 : 0) rects << Rect.new(x, y, w, row_h) x += w idx += 1 end y += row_h end rects end |
.monocle(count, area, _focused_index = 0) ⇒ Object
All panes occupy the full area; the focused pane is the one drawn last (the Renderer is responsible for the z-order).
240 241 242 |
# File 'lib/muxr/layout_manager.rb', line 240 def monocle(count, area, _focused_index = 0) Array.new(count) { Rect.new(area.x, area.y, area.w, area.h) } end |
.neighbor(rects, focused_index, direction) ⇒ Object
Return the index of the closest pane in ‘direction` (:left/:right/:up/:down) from the focused pane. Pure function over the rect list — does not know about the layout that produced the rects.
Selection rule: among panes strictly on the requested side, prefer the one with the largest perpendicular overlap with the focused pane; tie-break by smallest axis-distance, then by smallest center offset. Returns nil when nothing qualifies (e.g. focused is the rightmost pane and direction is :right, or monocle where every rect is identical).
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 |
# File 'lib/muxr/layout_manager.rb', line 253 def neighbor(rects, focused_index, direction) return nil if rects.nil? || rects.empty? return nil unless focused_index.is_a?(Integer) return nil unless focused_index.between?(0, rects.length - 1) focused = rects[focused_index] return nil unless focused best = nil rects.each_with_index do |rect, idx| next if idx == focused_index || rect.nil? case direction when :right next unless rect.x >= focused.x + focused.w axis_dist = rect.x - (focused.x + focused.w) overlap = overlap_extent(focused.y, focused.h, rect.y, rect.h) center = ((rect.y + rect.h / 2.0) - (focused.y + focused.h / 2.0)).abs when :left next unless rect.x + rect.w <= focused.x axis_dist = focused.x - (rect.x + rect.w) overlap = overlap_extent(focused.y, focused.h, rect.y, rect.h) center = ((rect.y + rect.h / 2.0) - (focused.y + focused.h / 2.0)).abs when :down next unless rect.y >= focused.y + focused.h axis_dist = rect.y - (focused.y + focused.h) overlap = overlap_extent(focused.x, focused.w, rect.x, rect.w) center = ((rect.x + rect.w / 2.0) - (focused.x + focused.w / 2.0)).abs when :up next unless rect.y + rect.h <= focused.y axis_dist = focused.y - (rect.y + rect.h) overlap = overlap_extent(focused.x, focused.w, rect.x, rect.w) center = ((rect.x + rect.w / 2.0) - (focused.x + focused.w / 2.0)).abs else return nil end score = [-overlap, axis_dist, center] if best.nil? || (score <=> best[0]) < 0 best = [score, idx] end end best && best[1] end |
.overlap_extent(a_start, a_size, b_start, b_size) ⇒ Object
297 298 299 300 301 |
# File 'lib/muxr/layout_manager.rb', line 297 def overlap_extent(a_start, a_size, b_start, b_size) finish = [a_start + a_size, b_start + b_size].min start = [a_start, b_start].max [finish - start, 0].max end |
.rows(count, area) ⇒ Object
Equal-height, full-width horizontal strips, stacked. The dual of columns.
102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/muxr/layout_manager.rb', line 102 def rows(count, area) base_h = area.h / count rem = area.h - base_h * count rects = [] y = area.y count.times do |i| h = base_h + (i < rem ? 1 : 0) rects << Rect.new(area.x, y, area.w, h) y += h end rects end |
.spiral(count, area) ⇒ Object
Fibonacci spiral: each pane takes half of the remaining region, splitting vertically then horizontally in alternation, so panes wind inward toward the bottom-right. The last pane fills whatever is left.
118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 |
# File 'lib/muxr/layout_manager.rb', line 118 def spiral(count, area) x, y, w, h = area.x, area.y, area.w, area.h rects = [] count.times do |i| if i == count - 1 rects << Rect.new(x, y, w, h) elsif i.even? left = [w / 2, 1].max rects << Rect.new(x, y, left, h) x += left w = [w - left, 1].max else top = [h / 2, 1].max rects << Rect.new(x, y, w, top) y += top h = [h - top, 1].max end end rects end |
.stack(count, area, focused_index = 0) ⇒ Object
Accordion: the focused pane expands to fill the leftover height while the others collapse to short “title sliver” rows, all stacked vertically. Like monocle but the other panes stay visible (and spatially reachable).
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 |
# File 'lib/muxr/layout_manager.rb', line 173 def stack(count, area, focused_index = 0) return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1 focused_index = focused_index.clamp(0, count - 1) others = count - 1 # Sliver is 3 rows so draw_box can still render the title; shrink it only # when the terminal is too short to give the focused pane its own 3 rows. sliver = [3, [area.h - 3, 0].max / others].min sliver = [sliver, 1].max focus_h = area.h - sliver * others rects = Array.new(count) y = area.y count.times do |i| h = (i == focused_index) ? focus_h : sliver rects[i] = Rect.new(area.x, y, area.w, h) y += h end rects end |
.stack_column(rects, indices, x, y, w, total_h) ⇒ Object
Stack the given pane indices vertically within a single column, dividing the height evenly (remainder to the topmost panes). Used by ‘centered`.
196 197 198 199 200 201 202 203 204 205 206 |
# File 'lib/muxr/layout_manager.rb', line 196 def stack_column(rects, indices, x, y, w, total_h) return if indices.empty? base_h = total_h / indices.length rem = total_h - base_h * indices.length cy = y indices.each_with_index do |idx, i| h = base_h + (i < rem ? 1 : 0) rects[idx] = Rect.new(x, cy, w, h) cy += h end end |
.tall(count, area, master_index = 0) ⇒ Object
Master pane on the left taking half the width; remaining panes stack vertically on the right, dividing the remaining height evenly.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/muxr/layout_manager.rb', line 38 def tall(count, area, master_index = 0) master_index = master_index.clamp(0, count - 1) return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1 master_w = [area.w / 2, 1].max stack_w = [area.w - master_w, 1].max others = (0...count).to_a - [master_index] slave_count = others.length base_h = area.h / slave_count remainder = area.h - base_h * slave_count rects = Array.new(count) rects[master_index] = Rect.new(area.x, area.y, master_w, area.h) y = area.y others.each_with_index do |idx, i| h = base_h + (i < remainder ? 1 : 0) rects[idx] = Rect.new(area.x + master_w, y, stack_w, h) y += h end rects end |
.wide(count, area, master_index = 0) ⇒ Object
The transpose of ‘tall`: master pane spans the full width across the top half; remaining panes sit side-by-side in the bottom half, dividing the remaining width evenly.
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/muxr/layout_manager.rb', line 64 def wide(count, area, master_index = 0) master_index = master_index.clamp(0, count - 1) return [Rect.new(area.x, area.y, area.w, area.h)] if count == 1 master_h = [area.h / 2, 1].max stack_h = [area.h - master_h, 1].max others = (0...count).to_a - [master_index] slave_count = others.length base_w = area.w / slave_count remainder = area.w - base_w * slave_count rects = Array.new(count) rects[master_index] = Rect.new(area.x, area.y, area.w, master_h) x = area.x others.each_with_index do |idx, i| w = base_w + (i < remainder ? 1 : 0) rects[idx] = Rect.new(x, area.y + master_h, w, stack_h) x += w end rects end |