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 grid monocle].freeze

Class Method Summary collapse

Class Method Details

.compute(layout, count, area, focused_index: 0, master_index: 0) ⇒ Object



17
18
19
20
21
22
23
24
25
26
27
28
# 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 :grid    then grid(count, area)
  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.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# File 'lib/muxr/layout_manager.rb', line 57

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



87
88
89
# File 'lib/muxr/layout_manager.rb', line 87

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



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/muxr/layout_manager.rb', line 100

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



144
145
146
147
148
# File 'lib/muxr/layout_manager.rb', line 144

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

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



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/muxr/layout_manager.rb', line 32

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