max / alloy_tui
| 1 | //! Spacing, as relationships rather than column counts. |
| 2 | //! |
| 3 | //! `makeover` resolves colour, which varies by theme. `makeover-geometry` |
| 4 | //! carries what does not, and this is the terminal's reading of it: a |
| 5 | //! [`Gap`] names *what is being separated* and this module answers how many |
| 6 | //! cells that is here. |
| 7 | //! |
| 8 | //! Why bother in a TUI, where the vocabulary collapses hard. A terminal cell is |
| 9 | //! coarse enough that four of the six gaps round to nothing horizontally and |
| 10 | //! five of six vertically, so most of what this module says is "no space". That |
| 11 | //! is the point rather than a disappointment: it says it *consistently*, and a |
| 12 | //! widget that asks for `Gap::Group` gets the same answer as every other widget |
| 13 | //! that asks, instead of one of them being written with a hardcoded 1 and its |
| 14 | //! neighbour with a 2. |
| 15 | //! |
| 16 | //! # Cells are not square |
| 17 | //! |
| 18 | //! A terminal cell is roughly 8x17 device pixels, so one row of space reads as |
| 19 | //! about twice the gap one column does. Treating the grid as one surface means |
| 20 | //! every gap is either visually thin horizontally or visually fat vertically. |
| 21 | //! |
| 22 | //! So there are two surfaces, one per axis, and they differ only in base. The |
| 23 | //! horizontal one is `Surface::terminal()` — a one-cell base and a one-cell |
| 24 | //! quantum, the crate's own definition. The vertical one keeps the one-cell |
| 25 | //! quantum (a row is still the smallest thing a terminal can draw) and halves |
| 26 | //! the base, which is what "a row is worth two columns" means expressed as a |
| 27 | //! measurement rather than as a special case. |
| 28 | //! |
| 29 | //! What falls out is the behaviour a terminal UI wants and would otherwise be |
| 30 | //! hand-tuned into every widget: containers get side padding and spend no rows |
| 31 | //! on it, and vertical space opens up only at pane scale, where a blank row is |
| 32 | //! carrying a real division rather than decorating one. |
| 33 | //! |
| 34 | //! | | bound | peer | group | section | pane | page | |
| 35 | //! |---|---|---|---|---|---|---| |
| 36 | //! | columns | 0 | 0 | 1 | 1 | 2 | 2 | |
| 37 | //! | rows | 0 | 0 | 0 | 0 | 1 | 1 | |
| 38 | //! |
| 39 | //! This answers the open question the crate's design note left for its first |
| 40 | //! terminal consumer. Nothing above is terminal-specific machinery: `Surface` |
| 41 | //! already takes a base and a quantum, and a second surface for the second axis |
| 42 | //! is a use of that, not an extension to it. |
| 43 | |
| 44 | use Surface; |
| 45 | use Padding; |
| 46 | |
| 47 | pub use ; |
| 48 | |
| 49 | /// The console is driven by a keyboard and read at desk distance. There is no |
| 50 | /// touch terminal to be the other case, so this is the only density in play — |
| 51 | /// named rather than assumed, so the call sites read the same as a webview's. |
| 52 | pub const DENSITY: Density = Pointer; |
| 53 | |
| 54 | /// Columns. One cell is the base and one cell is the quantum. |
| 55 | |
| 56 | terminal |
| 57 | |
| 58 | |
| 59 | /// Rows. Same quantum — a row is the smallest thing a terminal draws — against |
| 60 | /// half the base, because a row of space is worth about two columns of it. |
| 61 | |
| 62 | Surface |
| 63 | base: 0.5, |
| 64 | ..terminal |
| 65 | |
| 66 | |
| 67 | |
| 68 | /// How many columns of separation `gap` is worth. |
| 69 | |
| 70 | |
| 71 | clamp |
| 72 | |
| 73 | |
| 74 | /// How many rows of separation `gap` is worth. |
| 75 | |
| 76 | |
| 77 | clamp |
| 78 | |
| 79 | |
| 80 | /// `gap` as ratatui padding, per axis. |
| 81 | /// |
| 82 | /// What a bordered container wants inside its frame. [`Gap::Group`] is the |
| 83 | /// usual answer, being the crate's name for a container's inner margin. |
| 84 | |
| 85 | |
| 86 | symmetric |
| 87 | |
| 88 | |
| 89 | /// A terminal is addressed in `u16`, and no gap this vocabulary produces comes |
| 90 | /// near that. Saturating rather than `as` so a future base could not silently |
| 91 | /// wrap a layout. |
| 92 | |
| 93 | u16try_from.unwrap_or |
| 94 | |
| 95 | |
| 96 | |
| 97 | |
| 98 | use *; |
| 99 | |
| 100 | // The table in this module's own documentation. If these move, the doc is |
| 101 | // wrong and so is every widget that trusted it. |
| 102 | |
| 103 | |
| 104 | let want = |
| 105 | , |
| 106 | , |
| 107 | , |
| 108 | , |
| 109 | , |
| 110 | , |
| 111 | ]; |
| 112 | for in want |
| 113 | assert_eq!; |
| 114 | assert_eq!; |
| 115 | |
| 116 | |
| 117 | |
| 118 | // The invariant the crate's design note calls out as the one that matters: |
| 119 | // a looser relationship may collapse onto a tighter one, because a coarse |
| 120 | // surface has fewer distinctions available, but it must never resolve |
| 121 | // *tighter*. Checked per axis, since the two surfaces differ. |
| 122 | |
| 123 | |
| 124 | for axis in |
| 125 | , |
| 126 | , |
| 127 | ] |
| 128 | let = axis; |
| 129 | let ordered = all; |
| 130 | for pair in ordered.windows |
| 131 | let = ; |
| 132 | assert! |
| 133 | resolve >= resolve, |
| 134 | "{name}: {loose:?} ({}) resolved tighter than {tight:?} ({})", |
| 135 | resolve, |
| 136 | resolve |
| 137 | ; |
| 138 | |
| 139 | |
| 140 | |
| 141 | |
| 142 | // A row costs about twice what a column does, so no gap should ever spend |
| 143 | // more rows than columns. This is the whole reason there are two surfaces. |
| 144 | |
| 145 | |
| 146 | for gap in all |
| 147 | assert! |
| 148 | rows <= columns, |
| 149 | "{gap:?} spends {} rows against {} columns", |
| 150 | rows, |
| 151 | columns |
| 152 | ; |
| 153 | |
| 154 | |
| 155 | |
| 156 | |
| 157 | |
| 158 | assert_eq!; |
| 159 | |
| 160 | |
| 161 |