| 1 | module layout_mod |
| 2 | use pane_mod |
| 3 | implicit none |
| 4 | private |
| 5 | |
| 6 | public :: layout_split_vertical, layout_split_horizontal |
| 7 | public :: layout_remove_pane |
| 8 | public :: layout_recalculate |
| 9 | public :: layout_find_neighbor |
| 10 | |
| 11 | ! Direction constants for navigation |
| 12 | integer, parameter, public :: DIR_LEFT = 1 |
| 13 | integer, parameter, public :: DIR_RIGHT = 2 |
| 14 | integer, parameter, public :: DIR_UP = 3 |
| 15 | integer, parameter, public :: DIR_DOWN = 4 |
| 16 | |
| 17 | ! Split type constants |
| 18 | integer, parameter, public :: SPLIT_NONE = 0 |
| 19 | integer, parameter, public :: SPLIT_VERTICAL = 1 ! Side-by-side (left|right) |
| 20 | integer, parameter, public :: SPLIT_HORIZONTAL = 2 ! Stacked (top/bottom) |
| 21 | |
| 22 | contains |
| 23 | |
| 24 | ! Split the active pane vertically (side-by-side) |
| 25 | ! Returns the new pane index, or 0 on failure |
| 26 | function layout_split_vertical(panes, pane_count, active_idx, rows, cols) result(new_idx) |
| 27 | type(pane_t), intent(inout) :: panes(:) |
| 28 | integer, intent(inout) :: pane_count |
| 29 | integer, intent(in) :: active_idx |
| 30 | integer, intent(in) :: rows, cols |
| 31 | integer :: new_idx |
| 32 | |
| 33 | new_idx = 0 |
| 34 | if (pane_count >= MAX_PANES) return |
| 35 | if (active_idx < 1 .or. active_idx > pane_count) return |
| 36 | |
| 37 | ! Create new pane |
| 38 | pane_count = pane_count + 1 |
| 39 | new_idx = pane_count |
| 40 | |
| 41 | call pane_init(panes(new_idx), rows, cols) |
| 42 | |
| 43 | ! Mark the split relationship |
| 44 | panes(active_idx)%split_type = SPLIT_VERTICAL |
| 45 | panes(new_idx)%parent_idx = active_idx |
| 46 | panes(new_idx)%split_ratio = 0.5 ! Right half |
| 47 | end function layout_split_vertical |
| 48 | |
| 49 | ! Split the active pane horizontally (stacked top/bottom) |
| 50 | ! Returns the new pane index, or 0 on failure |
| 51 | function layout_split_horizontal(panes, pane_count, active_idx, rows, cols) result(new_idx) |
| 52 | type(pane_t), intent(inout) :: panes(:) |
| 53 | integer, intent(inout) :: pane_count |
| 54 | integer, intent(in) :: active_idx |
| 55 | integer, intent(in) :: rows, cols |
| 56 | integer :: new_idx |
| 57 | |
| 58 | new_idx = 0 |
| 59 | if (pane_count >= MAX_PANES) return |
| 60 | if (active_idx < 1 .or. active_idx > pane_count) return |
| 61 | |
| 62 | ! Create new pane |
| 63 | pane_count = pane_count + 1 |
| 64 | new_idx = pane_count |
| 65 | |
| 66 | call pane_init(panes(new_idx), rows, cols) |
| 67 | |
| 68 | ! Mark the split relationship |
| 69 | panes(active_idx)%split_type = SPLIT_HORIZONTAL |
| 70 | panes(new_idx)%parent_idx = active_idx |
| 71 | panes(new_idx)%split_ratio = 0.5 ! Bottom half |
| 72 | end function layout_split_horizontal |
| 73 | |
| 74 | ! Remove a pane and update layout |
| 75 | subroutine layout_remove_pane(panes, pane_count, idx) |
| 76 | type(pane_t), intent(inout) :: panes(:) |
| 77 | integer, intent(inout) :: pane_count |
| 78 | integer, intent(in) :: idx |
| 79 | integer :: i, parent_idx |
| 80 | |
| 81 | if (idx < 1 .or. idx > pane_count) return |
| 82 | |
| 83 | parent_idx = panes(idx)%parent_idx |
| 84 | |
| 85 | ! Destroy the pane |
| 86 | call pane_destroy(panes(idx)) |
| 87 | |
| 88 | ! Shift remaining panes down |
| 89 | do i = idx, pane_count - 1 |
| 90 | panes(i) = panes(i + 1) |
| 91 | end do |
| 92 | |
| 93 | pane_count = pane_count - 1 |
| 94 | |
| 95 | ! Update parent references for shifted panes |
| 96 | do i = 1, pane_count |
| 97 | if (panes(i)%parent_idx > idx) then |
| 98 | panes(i)%parent_idx = panes(i)%parent_idx - 1 |
| 99 | else if (panes(i)%parent_idx == idx) then |
| 100 | ! Orphaned - reparent to the removed pane's parent |
| 101 | panes(i)%parent_idx = parent_idx |
| 102 | end if |
| 103 | end do |
| 104 | |
| 105 | ! If we removed a pane that had children, clear the split type of sibling |
| 106 | if (parent_idx > 0 .and. parent_idx <= pane_count) then |
| 107 | panes(parent_idx)%split_type = SPLIT_NONE |
| 108 | end if |
| 109 | end subroutine layout_remove_pane |
| 110 | |
| 111 | ! Recalculate all pane viewports based on available space |
| 112 | subroutine layout_recalculate(panes, pane_count, x, y, w, h, cell_w, cell_h) |
| 113 | type(pane_t), intent(inout) :: panes(:) |
| 114 | integer, intent(in) :: pane_count |
| 115 | integer, intent(in) :: x, y, w, h |
| 116 | integer, intent(in) :: cell_w, cell_h |
| 117 | integer :: i |
| 118 | integer, allocatable :: viewport(:,:) ! (4, MAX_PANES) - x, y, w, h per pane |
| 119 | logical, allocatable :: processed(:) |
| 120 | integer :: px, py, pw, ph, split_pos |
| 121 | |
| 122 | if (pane_count < 1) return |
| 123 | |
| 124 | allocate(viewport(4, pane_count)) |
| 125 | allocate(processed(pane_count)) |
| 126 | processed = .false. |
| 127 | |
| 128 | ! Start by giving the first pane (root) the entire space |
| 129 | viewport(1, 1) = x |
| 130 | viewport(2, 1) = y |
| 131 | viewport(3, 1) = w |
| 132 | viewport(4, 1) = h |
| 133 | processed(1) = .true. |
| 134 | |
| 135 | ! Process panes in order, calculating child viewports from parents |
| 136 | do i = 2, pane_count |
| 137 | if (panes(i)%parent_idx > 0 .and. panes(i)%parent_idx <= pane_count) then |
| 138 | ! Get parent's viewport |
| 139 | px = viewport(1, panes(i)%parent_idx) |
| 140 | py = viewport(2, panes(i)%parent_idx) |
| 141 | pw = viewport(3, panes(i)%parent_idx) |
| 142 | ph = viewport(4, panes(i)%parent_idx) |
| 143 | |
| 144 | if (panes(panes(i)%parent_idx)%split_type == SPLIT_VERTICAL) then |
| 145 | ! Parent was split vertically - child gets right half |
| 146 | split_pos = pw / 2 |
| 147 | ! Shrink parent to left half |
| 148 | viewport(3, panes(i)%parent_idx) = split_pos - 1 |
| 149 | ! Child gets right half |
| 150 | viewport(1, i) = px + split_pos + 1 |
| 151 | viewport(2, i) = py |
| 152 | viewport(3, i) = pw - split_pos - 2 |
| 153 | viewport(4, i) = ph |
| 154 | else if (panes(panes(i)%parent_idx)%split_type == SPLIT_HORIZONTAL) then |
| 155 | ! Parent was split horizontally - child gets bottom half |
| 156 | split_pos = ph / 2 |
| 157 | ! Shrink parent to top half |
| 158 | viewport(4, panes(i)%parent_idx) = split_pos - 1 |
| 159 | ! Child gets bottom half |
| 160 | viewport(1, i) = px |
| 161 | viewport(2, i) = py + split_pos + 1 |
| 162 | viewport(3, i) = pw |
| 163 | viewport(4, i) = ph - split_pos - 2 |
| 164 | else |
| 165 | ! No split type - shouldn't happen, give equal share |
| 166 | viewport(1, i) = x |
| 167 | viewport(2, i) = y |
| 168 | viewport(3, i) = w |
| 169 | viewport(4, i) = h |
| 170 | end if |
| 171 | processed(i) = .true. |
| 172 | else |
| 173 | ! Orphan pane - give it the full space (shouldn't happen normally) |
| 174 | viewport(1, i) = x |
| 175 | viewport(2, i) = y |
| 176 | viewport(3, i) = w |
| 177 | viewport(4, i) = h |
| 178 | processed(i) = .true. |
| 179 | end if |
| 180 | end do |
| 181 | |
| 182 | ! Apply viewports to all panes |
| 183 | do i = 1, pane_count |
| 184 | call pane_set_viewport(panes(i), viewport(1, i), viewport(2, i), & |
| 185 | viewport(3, i), viewport(4, i), cell_w, cell_h) |
| 186 | end do |
| 187 | |
| 188 | deallocate(viewport) |
| 189 | deallocate(processed) |
| 190 | end subroutine layout_recalculate |
| 191 | |
| 192 | ! Find the nearest pane in the given direction |
| 193 | ! Returns 0 if no neighbor found |
| 194 | function layout_find_neighbor(panes, pane_count, active_idx, direction) result(neighbor_idx) |
| 195 | type(pane_t), intent(in) :: panes(:) |
| 196 | integer, intent(in) :: pane_count |
| 197 | integer, intent(in) :: active_idx |
| 198 | integer, intent(in) :: direction |
| 199 | integer :: neighbor_idx |
| 200 | |
| 201 | integer :: i |
| 202 | integer :: active_cx, active_cy |
| 203 | integer :: cx, cy |
| 204 | integer :: best_idx, best_dist, dist, perp_dist |
| 205 | |
| 206 | neighbor_idx = 0 |
| 207 | if (pane_count < 2) return |
| 208 | if (active_idx < 1 .or. active_idx > pane_count) return |
| 209 | |
| 210 | ! Get active pane's center point |
| 211 | active_cx = panes(active_idx)%x + panes(active_idx)%width / 2 |
| 212 | active_cy = panes(active_idx)%y + panes(active_idx)%height / 2 |
| 213 | |
| 214 | best_idx = 0 |
| 215 | best_dist = huge(best_dist) |
| 216 | |
| 217 | do i = 1, pane_count |
| 218 | if (i == active_idx) cycle |
| 219 | |
| 220 | cx = panes(i)%x + panes(i)%width / 2 |
| 221 | cy = panes(i)%y + panes(i)%height / 2 |
| 222 | |
| 223 | select case (direction) |
| 224 | case (DIR_LEFT) |
| 225 | if (cx < active_cx) then |
| 226 | dist = active_cx - cx |
| 227 | perp_dist = abs(cy - active_cy) |
| 228 | ! Prefer panes more directly to the left |
| 229 | dist = dist + perp_dist / 2 |
| 230 | if (dist < best_dist) then |
| 231 | best_dist = dist |
| 232 | best_idx = i |
| 233 | end if |
| 234 | end if |
| 235 | |
| 236 | case (DIR_RIGHT) |
| 237 | if (cx > active_cx) then |
| 238 | dist = cx - active_cx |
| 239 | perp_dist = abs(cy - active_cy) |
| 240 | dist = dist + perp_dist / 2 |
| 241 | if (dist < best_dist) then |
| 242 | best_dist = dist |
| 243 | best_idx = i |
| 244 | end if |
| 245 | end if |
| 246 | |
| 247 | case (DIR_UP) |
| 248 | if (cy < active_cy) then |
| 249 | dist = active_cy - cy |
| 250 | perp_dist = abs(cx - active_cx) |
| 251 | dist = dist + perp_dist / 2 |
| 252 | if (dist < best_dist) then |
| 253 | best_dist = dist |
| 254 | best_idx = i |
| 255 | end if |
| 256 | end if |
| 257 | |
| 258 | case (DIR_DOWN) |
| 259 | if (cy > active_cy) then |
| 260 | dist = cy - active_cy |
| 261 | perp_dist = abs(cx - active_cx) |
| 262 | dist = dist + perp_dist / 2 |
| 263 | if (dist < best_dist) then |
| 264 | best_dist = dist |
| 265 | best_idx = i |
| 266 | end if |
| 267 | end if |
| 268 | end select |
| 269 | end do |
| 270 | |
| 271 | neighbor_idx = best_idx |
| 272 | end function layout_find_neighbor |
| 273 | |
| 274 | end module layout_mod |
| 275 |