1use crate::{Constraint, Flex};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub struct TitleGroupPositions {
5 pub left_x: Option<u16>,
6 pub center_x: Option<u16>,
7 pub right_x: Option<u16>,
8}
9
10pub fn title_group_positions(
11 area_width: u16,
12 left_width: u16,
13 center_width: u16,
14 right_width: u16,
15) -> TitleGroupPositions {
16 if area_width == 0 {
17 return TitleGroupPositions {
18 left_x: None,
19 center_x: None,
20 right_x: None,
21 };
22 }
23
24 let left_x = (left_width > 0).then_some(0u16);
25 let right_x = (right_width > 0).then(|| area_width.saturating_sub(right_width.min(area_width)));
26
27 let center_x = if center_width > 0 {
28 let center_width = center_width.min(area_width);
29 let center_x = area_width.saturating_sub(center_width) / 2;
30 let center_end = center_x.saturating_add(center_width);
31 let left_end = left_x
32 .map(|x| x.saturating_add(left_width.min(area_width)))
33 .unwrap_or(0);
34 let right_start = right_x.unwrap_or(area_width);
35 (center_x >= left_end && center_end <= right_start).then_some(center_x)
36 } else {
37 None
38 };
39
40 TitleGroupPositions {
41 left_x,
42 center_x,
43 right_x,
44 }
45}
46
47pub fn table_span_width(
48 column_widths: &[u16],
49 column_positions: &[u16],
50 table_width: u16,
51 start: usize,
52 span: usize,
53) -> u16 {
54 if start >= column_positions.len() {
55 return 0;
56 }
57
58 let start_x = column_positions[start];
59 let last = start
60 .saturating_add(span.saturating_sub(1))
61 .min(column_positions.len().saturating_sub(1));
62 let end_x = column_positions[last]
63 .saturating_add(column_widths.get(last).copied().unwrap_or_default())
64 .min(table_width);
65
66 end_x.saturating_sub(start_x).max(
67 column_widths
68 .get(start)
69 .copied()
70 .unwrap_or_default()
71 .min(table_width.saturating_sub(start_x)),
72 )
73}
74
75pub fn table_column_layout(
76 total_width: u16,
77 columns: usize,
78 widths: &[Constraint],
79 column_spacing: u16,
80 flex: Flex,
81) -> (Vec<u16>, Vec<u16>) {
82 if columns == 0 {
83 return (Vec::new(), Vec::new());
84 }
85
86 let mut effective_spacing = column_spacing;
87 let separator_width = columns.saturating_sub(1) as u16 * effective_spacing;
88 let content_width = total_width.saturating_sub(separator_width);
89 if widths.is_empty() {
90 let base = content_width / columns as u16;
91 let remainder = content_width % columns as u16;
92 let widths: Vec<u16> = (0..columns)
93 .map(|index| base.saturating_add(u16::from(index < remainder as usize)))
94 .collect();
95 let positions =
96 table_column_positions(columns, &widths, effective_spacing, total_width, flex);
97 return (widths, positions);
98 }
99
100 let mut resolved = vec![0u16; columns];
101 let mut fixed_total = 0u16;
102 let mut fill_columns = Vec::new();
103 let mut max_columns = Vec::new();
104 let mut min_columns = Vec::new();
105
106 for index in 0..columns {
107 let constraint = widths.get(index).copied().unwrap_or(Constraint::Fill(1));
108 match constraint {
109 Constraint::Length(value) => {
110 resolved[index] = value;
111 fixed_total = fixed_total.saturating_add(value);
112 }
113 Constraint::Percentage(percent) => {
114 let value = ((u32::from(content_width) * u32::from(percent.min(100))) / 100) as u16;
115 resolved[index] = value;
116 fixed_total = fixed_total.saturating_add(value);
117 }
118 Constraint::Min(value) => {
119 resolved[index] = value;
120 fixed_total = fixed_total.saturating_add(value);
121 min_columns.push(index);
122 }
123 Constraint::Max(value) => max_columns.push((index, value)),
124 Constraint::Fill(weight) => fill_columns.push((index, weight.max(1))),
125 }
126 }
127
128 let mut remaining = content_width.saturating_sub(fixed_total);
129 if !fill_columns.is_empty() {
130 let total_weight: u16 = fill_columns.iter().map(|(_, weight)| *weight).sum();
131 let mut assigned = 0u16;
132 for (position, (index, weight)) in fill_columns.iter().enumerate() {
133 let share = if position == fill_columns.len() - 1 {
134 remaining.saturating_sub(assigned)
135 } else {
136 ((u32::from(remaining) * u32::from(*weight)) / u32::from(total_weight.max(1)))
137 as u16
138 };
139 resolved[*index] = share;
140 assigned = assigned.saturating_add(share);
141 }
142 fit_table_columns_to_width(total_width, &mut resolved, &mut effective_spacing);
143 let positions =
144 table_column_positions(columns, &resolved, effective_spacing, total_width, flex);
145 return (resolved, positions);
146 }
147
148 if !max_columns.is_empty() && remaining > 0 {
149 for (index, max_width) in max_columns {
150 let share = remaining.min(max_width);
151 resolved[index] = share;
152 remaining = remaining.saturating_sub(share);
153 if remaining == 0 {
154 break;
155 }
156 }
157 }
158
159 if matches!(flex, Flex::Legacy) {
160 let stretch_targets: Vec<usize> = if !min_columns.is_empty() {
161 min_columns
162 } else {
163 (0..columns).collect()
164 };
165 if remaining > 0 && !stretch_targets.is_empty() {
166 let base = remaining / stretch_targets.len() as u16;
167 let remainder = remaining % stretch_targets.len() as u16;
168 for (position, index) in stretch_targets.into_iter().enumerate() {
169 resolved[index] = resolved[index]
170 .saturating_add(base)
171 .saturating_add(u16::from(position < remainder as usize));
172 }
173 }
174 fit_table_columns_to_width(total_width, &mut resolved, &mut effective_spacing);
175 let positions =
176 table_column_positions(columns, &resolved, effective_spacing, total_width, flex);
177 return (resolved, positions);
178 }
179
180 if remaining > 0 && !min_columns.is_empty() {
181 let mut stretch_targets = min_columns;
182 stretch_targets.sort_by_key(|index| resolved[*index]);
183
184 let mut active = 1usize;
185 while active < stretch_targets.len() && remaining > 0 {
186 let current = resolved[stretch_targets[active - 1]];
187 let next = resolved[stretch_targets[active]];
188 let delta = next.saturating_sub(current);
189 if delta == 0 {
190 active += 1;
191 continue;
192 }
193
194 let needed = delta.saturating_mul(active as u16);
195 if remaining < needed {
196 break;
197 }
198
199 for index in &stretch_targets[..active] {
200 resolved[*index] = resolved[*index].saturating_add(delta);
201 }
202 remaining = remaining.saturating_sub(needed);
203 active += 1;
204 }
205
206 let base = remaining / active as u16;
207 let remainder = remaining % active as u16;
208 for (position, index) in stretch_targets[..active].iter().enumerate() {
209 resolved[*index] = resolved[*index]
210 .saturating_add(base)
211 .saturating_add(u16::from(position < remainder as usize));
212 }
213 }
214
215 fit_table_columns_to_width(total_width, &mut resolved, &mut effective_spacing);
216 let positions =
217 table_column_positions(columns, &resolved, effective_spacing, total_width, flex);
218
219 (resolved, positions)
220}
221
222fn fit_table_columns_to_width(total_width: u16, widths: &mut [u16], column_spacing: &mut u16) {
223 if widths.is_empty() {
224 *column_spacing = 0;
225 return;
226 }
227
228 let gaps = widths.len().saturating_sub(1) as u16;
229 let preferred_width = widths.iter().copied().fold(0u16, u16::saturating_add);
230 let preferred_total = preferred_width.saturating_add(gaps.saturating_mul(*column_spacing));
231 if preferred_total <= total_width {
232 return;
233 }
234
235 if gaps > 0 {
236 let overflow = preferred_total.saturating_sub(total_width);
237 let max_spacing_reduction = gaps.saturating_mul(*column_spacing);
238 let spacing_reduction = overflow.min(max_spacing_reduction);
239 let remaining_spacing = max_spacing_reduction.saturating_sub(spacing_reduction);
240 *column_spacing = remaining_spacing / gaps;
241 }
242
243 let available_for_columns = total_width.saturating_sub(gaps.saturating_mul(*column_spacing));
244 let width_sum = widths.iter().copied().fold(0u16, u16::saturating_add);
245 if width_sum <= available_for_columns {
246 return;
247 }
248
249 if available_for_columns == 0 {
250 widths.fill(0);
251 return;
252 }
253
254 let mut reassigned = vec![0u16; widths.len()];
255 let mut assigned = 0u16;
256 for (index, width) in widths.iter().copied().enumerate() {
257 let share = ((u32::from(available_for_columns) * u32::from(width))
258 / u32::from(width_sum.max(1))) as u16;
259 reassigned[index] = share;
260 assigned = assigned.saturating_add(share);
261 }
262 let mut remainder = available_for_columns.saturating_sub(assigned);
263 for width in &mut reassigned {
264 if remainder == 0 {
265 break;
266 }
267 *width = width.saturating_add(1);
268 remainder = remainder.saturating_sub(1);
269 }
270
271 widths.copy_from_slice(&reassigned);
272}
273
274fn table_column_positions(
275 columns: usize,
276 widths: &[u16],
277 column_spacing: u16,
278 total_width: u16,
279 flex: Flex,
280) -> Vec<u16> {
281 if columns == 0 {
282 return Vec::new();
283 }
284
285 let separator_width = columns.saturating_sub(1) as u16 * column_spacing;
286 let used_width = widths
287 .iter()
288 .copied()
289 .fold(0u16, u16::saturating_add)
290 .saturating_add(separator_width)
291 .min(total_width);
292 let extra = total_width.saturating_sub(used_width);
293
294 let (leading, between_extra) = match flex {
295 Flex::End => (extra, vec![0; columns.saturating_sub(1)]),
296 Flex::Center => (extra / 2, vec![0; columns.saturating_sub(1)]),
297 Flex::SpaceBetween if columns > 1 => {
298 let gaps = columns - 1;
299 let base = extra / gaps as u16;
300 let remainder = extra % gaps as u16;
301 let between = (0..gaps)
302 .map(|index| base.saturating_add(u16::from(index < remainder as usize)))
303 .collect();
304 (0, between)
305 }
306 Flex::SpaceAround if columns > 0 => distributed_space_around_gaps(extra, columns),
307 Flex::SpaceEvenly if columns > 0 => distributed_space_evenly_gaps(extra, columns),
308 _ => (0, vec![0; columns.saturating_sub(1)]),
309 };
310
311 let mut positions = Vec::with_capacity(columns);
312 let mut x = leading;
313 for index in 0..columns {
314 positions.push(x);
315 x = x.saturating_add(widths.get(index).copied().unwrap_or(0));
316 if index + 1 < columns {
317 x = x
318 .saturating_add(column_spacing)
319 .saturating_add(between_extra.get(index).copied().unwrap_or(0));
320 }
321 }
322 positions
323}
324
325fn distributed_space_evenly_gaps(extra: u16, columns: usize) -> (u16, Vec<u16>) {
326 if columns == 0 {
327 return (0, Vec::new());
328 }
329
330 let gap_count = columns + 1;
331 let base = extra / gap_count as u16;
332 let remainder = extra % gap_count as u16;
333 let mut gaps = Vec::with_capacity(gap_count);
334 for index in 0..gap_count {
335 gaps.push(base.saturating_add(u16::from(index < remainder as usize)));
336 }
337
338 let leading = gaps.first().copied().unwrap_or(0);
339 let between = if gaps.len() > 2 {
340 gaps[1..gaps.len() - 1].to_vec()
341 } else {
342 Vec::new()
343 };
344 (leading, between)
345}
346
347fn distributed_space_around_gaps(extra: u16, columns: usize) -> (u16, Vec<u16>) {
348 if columns == 0 {
349 return (0, Vec::new());
350 }
351
352 let edge_base = extra / (columns.saturating_mul(2) as u16);
353 let between_base = edge_base.saturating_mul(2);
354 let mut gaps = Vec::with_capacity(columns + 1);
355 gaps.push(edge_base);
356 for _ in 1..columns {
357 gaps.push(between_base);
358 }
359 gaps.push(edge_base);
360
361 let allocated = edge_base
362 .saturating_mul(2)
363 .saturating_add(between_base.saturating_mul(columns.saturating_sub(1) as u16));
364 let mut remainder = extra.saturating_sub(allocated);
365
366 let mut gap_order = Vec::with_capacity(gaps.len());
367 gap_order.extend(1..columns);
368 gap_order.push(0);
369 gap_order.push(columns);
370
371 while remainder > 0 {
372 for index in &gap_order {
373 if remainder == 0 {
374 break;
375 }
376 gaps[*index] = gaps[*index].saturating_add(1);
377 remainder = remainder.saturating_sub(1);
378 }
379 }
380
381 let leading = gaps.first().copied().unwrap_or(0);
382 let between = if gaps.len() > 2 {
383 gaps[1..gaps.len() - 1].to_vec()
384 } else {
385 Vec::new()
386 };
387 (leading, between)
388}