Skip to main content

ansiq_core/
render_math.rs

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}