Skip to main content

egui_table_kit/layout/
columns.rs

1//! Logic for constrained column auto-sizing.
2
3use egui::Rangef;
4
5bitflags::bitflags! {
6    /// Configuration flags for a column's layout behavior.
7    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
8    #[serde(transparent)]
9    pub struct ColumnFlags: u8 {
10        /// Can the user resize this column?
11        const RESIZABLE = 1 << 0;
12        /// If set, the column will be automatically sized based on the content this frame.
13        const AUTO_SIZE_THIS_FRAME = 1 << 1;
14        /// Set whether the column should auto-fit to content width on first load.
15        const AUTO_FIT = 1 << 2;
16        /// Set whether the column should absorb remaining horizontal space on layout passes.
17        const REMAINDER = 1 << 3;
18    }
19}
20
21#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
22pub struct Column {
23    pub current: f32,
24    pub range: Rangef,
25    pub id: Option<egui::Id>,
26    pub flags: ColumnFlags,
27}
28
29impl Default for Column {
30    fn default() -> Self {
31        Self {
32            current: 100.0,
33            range: Rangef::new(4.0, f32::INFINITY),
34            id: None,
35            flags: ColumnFlags::RESIZABLE,
36        }
37    }
38}
39
40impl Column {
41    /// Current and/or initial column width.
42    #[must_use]
43    #[inline]
44    pub fn new(current: f32) -> Self {
45        Self {
46            current,
47            ..Default::default()
48        }
49    }
50
51    /// Allowed width range.
52    #[must_use]
53    #[inline]
54    pub fn range(mut self, range: impl Into<Rangef>) -> Self {
55        self.range = range.into();
56        self
57    }
58
59    /// Optional unique id within the parent table.
60    #[must_use]
61    #[inline]
62    pub const fn id(mut self, id: egui::Id) -> Self {
63        self.id = Some(id);
64        self
65    }
66
67    /// Can the user resize this column?
68    #[must_use]
69    #[inline]
70    pub fn resizable(mut self, resizable: bool) -> Self {
71        self.flags.set(ColumnFlags::RESIZABLE, resizable);
72        self
73    }
74
75    /// If set, the column will be automatically sized based on the content this frame.
76    #[must_use]
77    #[inline]
78    pub fn auto_size_this_frame(mut self, auto_size_this_frame: bool) -> Self {
79        self.flags
80            .set(ColumnFlags::AUTO_SIZE_THIS_FRAME, auto_size_this_frame);
81        self
82    }
83
84    /// Set whether the column should auto-fit to content width on first load.
85    #[must_use]
86    #[inline]
87    pub fn auto_fit(mut self, auto_fit: bool) -> Self {
88        self.flags.set(ColumnFlags::AUTO_FIT, auto_fit);
89        self
90    }
91
92    /// Set whether the column should absorb remaining horizontal space on layout passes.
93    #[must_use]
94    #[inline]
95    pub fn remainder(mut self, remainder: bool) -> Self {
96        self.flags.set(ColumnFlags::REMAINDER, remainder);
97        self
98    }
99
100    /// Getter for whether the column is resizable.
101    #[inline]
102    #[must_use]
103    pub const fn is_resizable(&self) -> bool {
104        self.flags.contains(ColumnFlags::RESIZABLE)
105    }
106
107    /// Getter for whether the column should auto-size this frame.
108    #[inline]
109    #[must_use]
110    pub const fn is_auto_size_this_frame(&self) -> bool {
111        self.flags.contains(ColumnFlags::AUTO_SIZE_THIS_FRAME)
112    }
113
114    /// Getter for whether the column should auto-fit.
115    #[inline]
116    #[must_use]
117    pub const fn is_auto_fit(&self) -> bool {
118        self.flags.contains(ColumnFlags::AUTO_FIT)
119    }
120
121    /// Getter for whether the column acts as a remainder.
122    #[inline]
123    #[must_use]
124    pub const fn is_remainder(&self) -> bool {
125        self.flags.contains(ColumnFlags::REMAINDER)
126    }
127
128    #[must_use]
129    #[inline]
130    pub fn id_for(&self, col_idx: usize) -> egui::Id {
131        self.id.unwrap_or_else(|| egui::Id::new(col_idx))
132    }
133
134    /// Resize columns to fit the total width.
135    pub fn auto_size(columns: &mut [Self], target_width: f32) {
136        if columns.is_empty() {
137            return;
138        }
139
140        // Make sure all columns have a valid range.
141        let mut max_width = 0.0;
142        let mut current_width = 0.0;
143        for column in columns.iter_mut() {
144            column.current = column.range.clamp(column.current);
145            max_width += column.range.max;
146            current_width += column.current;
147        }
148
149        // If the total width of all columns already meets or exceeds the target parent width,
150        // we should not attempt to shrink them (as horizontal scrolling handles the overflow).
151        if current_width >= target_width {
152            return;
153        }
154
155        let sign = 1.0;
156
157        if max_width <= current_width {
158            return; // Can't grow
159        }
160
161        // Only grow columns that are explicitly configured as remainder columns (matching egui_extras)
162        let mut can_change: Vec<(f32, usize)> = columns
163            .iter()
164            .enumerate()
165            .filter_map(|(i, c)| {
166                if c.flags.contains(ColumnFlags::REMAINDER) && c.current < c.range.max {
167                    let room = if c.range.max == f32::INFINITY {
168                        10000.0 - c.current // Cap infinity comparisons at a large finite limit
169                    } else {
170                        c.range.max - c.current
171                    };
172                    return Some((room, i));
173                }
174                None
175            })
176            .collect();
177
178        if can_change.is_empty() {
179            return; // No remainder columns to grow, keep everything compact
180        }
181
182        // Put the columns that has the most room to change first:
183        can_change.sort_by(|a, b| b.0.partial_cmp(&a.0).expect("NaN or Inf in column code"));
184        debug_assert!(
185            can_change[0].0 >= can_change.last().expect("Can't be empty").0,
186            "The sort is broken"
187        );
188
189        let mut remaining_abs = (target_width - current_width).abs();
190
191        while let Some((room_in_least, least_idx)) = can_change.pop() {
192            #[allow(clippy::cast_precision_loss)]
193            let evenly_distributed = remaining_abs / (can_change.len() as f32 + 1.0);
194
195            if evenly_distributed <= room_in_least {
196                columns[least_idx].current += sign * evenly_distributed;
197                for (_, i) in can_change {
198                    columns[i].current += sign * evenly_distributed;
199                }
200                return;
201            }
202
203            columns[least_idx].current = columns[least_idx].range.max;
204            remaining_abs -= room_in_least;
205        }
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    fn col(c: i32, range: std::ops::RangeInclusive<i32>) -> Column {
214        #[allow(clippy::cast_precision_loss)]
215        Column::new(c as f32).range(Rangef::new(*range.start() as f32, *range.end() as f32))
216    }
217
218    fn widths(columns: &[Column]) -> Vec<f32> {
219        columns.iter().map(|c| c.current).collect()
220    }
221
222    #[test]
223    fn test_single_column() {
224        let mut columns = [col(0, 100..=200).remainder(true)];
225        Column::auto_size(&mut columns, 50.0);
226        assert_eq!(widths(&columns), [100.0]);
227        Column::auto_size(&mut columns, 132.0);
228        assert_eq!(widths(&columns), [132.0]);
229        Column::auto_size(&mut columns, 500.0);
230        assert_eq!(widths(&columns), [200.0]);
231    }
232
233    #[test]
234    fn test_many_columns() {
235        let mut columns = [
236            col(15, 10..=20).remainder(true),
237            col(25, 10..=100).remainder(true),
238            col(150, 100..=200).remainder(true),
239        ];
240
241        Column::auto_size(&mut columns, 190.0);
242        assert_eq!(
243            widths(&columns),
244            [15.0, 25.0, 150.0],
245            "They have no need to grow"
246        );
247
248        Column::auto_size(&mut columns, 207.0);
249        assert_eq!(
250            widths(&columns),
251            [20.0, 31.0, 156.0],
252            "They should grow equally"
253        );
254    }
255}