aboutsummaryrefslogtreecommitdiff
path: root/modules/cj-window-geometry-lib.el
blob: 047fe7c45cef60a4b57112f2972ab9ba378fa752 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
;;; cj-window-geometry-lib.el --- Pure window-geometry helpers -*- lexical-binding: t; -*-

;; Author: Craig Jennings <c@cjennings.net>

;;; Commentary:

;; Pure helpers for classifying a window's position in its frame and
;; computing body sizes.  Shared between `ai-term.el' (F9 dispatch)
;; and `term-config.el' (F12 dispatch); the geometry-
;; preservation pattern in both modules captures direction + body
;; size at toggle-off and replays them on the next toggle-on.
;;
;; All functions are pure: they read window/frame edges and return
;; classifications.  No side effects, no state.  Consumers wrap them
;; with consumer-specific state vars and display logic.

;;; Code:

(defun cj/window-direction (window &optional default)
  "Return the side WINDOW occupies in its frame.

Returns one of right, below, left, above.  Falls back to DEFAULT
(or right when DEFAULT is nil) when WINDOW fills its frame's
root area.  Comparison uses `frame-root-window' edges so the
minibuffer doesn't make every full-area window look like it
fails to span the full height."
  (let* ((root (frame-root-window (window-frame window)))
         (edges (window-edges window))
         (root-edges (window-edges root))
         (left (nth 0 edges))
         (top (nth 1 edges))
         (right (nth 2 edges))
         (bottom (nth 3 edges))
         (root-left (nth 0 root-edges))
         (root-top (nth 1 root-edges))
         (root-right (nth 2 root-edges))
         (root-bottom (nth 3 root-edges))
         (spans-full-width (and (= left root-left) (= right root-right)))
         (spans-full-height (and (= top root-top) (= bottom root-bottom))))
    (cond
     ((not spans-full-width) (if (= left root-left) 'left 'right))
     ((not spans-full-height) (if (= top root-top) 'above 'below))
     (t (or default 'right)))))

(defun cj/window-body-size (window direction)
  "Return WINDOW's body size on the axis matching DIRECTION.

Returns body-width (columns) when DIRECTION is right or left.
Returns body-height (lines) when DIRECTION is below or above.

Body size, not total size, is the right thing to capture for
geometry replay: total-width includes the right-side divider when
the window has a right sibling but excludes it at the frame edge,
so a captured rightmost window replayed into a middle position
would leave the body 1 col short.  Body size is divider-
independent and matches what the user actually sees."
  (if (memq direction '(right left))
      (window-body-width window)
    (window-body-height window)))

(defun cj/cardinal-to-edge-direction (direction)
  "Map cardinal DIRECTION to its `display-buffer-in-direction' edge variant.

Returns rightmost/leftmost/bottom/top for right/left/below/above
respectively.  Returns nil for any other input.

The edge variants route splits relative to the frame's main
window rather than the selected window, so a toggle-on lands at
the same frame edge regardless of which window is selected.  The
cardinal variants would split the selected window's tree branch
instead, putting the new window mid-frame in multi-window
layouts."
  (pcase direction
    ('right 'rightmost)
    ('left 'leftmost)
    ('below 'bottom)
    ('above 'top)
    (_ nil)))

(defun cj/window-at-edge (direction &optional frame)
  "Return the window forming FRAME's half on the side named by DIRECTION.

DIRECTION is one of right, left, below, above.  A window qualifies as
the half when it sits flush against that frame edge and spans the full
perpendicular extent: for right or left it is a full-height side column
(nothing above, below, or further toward the edge, but a sibling on the
far side); for below or above it is a full-width row.  FRAME defaults to
the selected frame.

Existence is tested with `window-in-direction' rather than raw edge
arithmetic -- frame-internal geometry makes the root window's edges
disagree with its children's by a row, so an `=' comparison against the
root edges never matches.  Asking \"is there a window on each side?\"
sidesteps that.

Returns nil when no window qualifies: a single-window frame (no sibling
on the far side, so not a distinct half), an axis mismatch (a top/bottom
split when DIRECTION is right has no full-height right column), or a
nested edge no one window fills.  The caller then falls back to splitting
a fresh half."
  (when (memq direction '(right left below above))
    (let ((far (pcase direction
                 ('right 'left) ('left 'right) ('below 'above) ('above 'below)))
          (perp (pcase direction
                  ((or 'right 'left) '(above below))
                  ((or 'below 'above) '(left right)))))
      (seq-find
       (lambda (w)
         (and (window-in-direction far w)
              (not (window-in-direction direction w))
              (not (window-in-direction (nth 0 perp) w))
              (not (window-in-direction (nth 1 perp) w))))
       (window-list (or frame (selected-frame)) 'never)))))

(defun cj/window-size-fraction (window-size frame-size &optional min-frac max-frac)
  "Return WINDOW-SIZE as a fraction of FRAME-SIZE, clamped to [MIN-FRAC, MAX-FRAC].

WINDOW-SIZE and FRAME-SIZE are line or column counts.  MIN-FRAC and
MAX-FRAC default to 0.05 and 0.95: a side window pinned to either extreme
is almost certainly a mistake, and a 0.0 fraction makes
`display-buffer-in-side-window' unusable.  Returns nil when FRAME-SIZE is
not a positive number, or WINDOW-SIZE is not a number, so a caller can
fall back to its own default instead of dividing by zero.

This is the kernel for remembering a side window's user-resized size: capture
the fraction at toggle-off, replay it on the next toggle-on."
  (when (and (numberp window-size) (numberp frame-size) (> frame-size 0))
    (let ((lo (or min-frac 0.05))
          (hi (or max-frac 0.95)))
      (max lo (min hi (/ (float window-size) frame-size))))))

(provide 'cj-window-geometry-lib)
;;; cj-window-geometry-lib.el ends here