Skip to main content

dear_imgui_rs/
columns.rs

1//! Legacy columns API
2//!
3//! Thin wrappers for the old Columns layout system. New code should prefer
4//! the `table` API (`widget::table`) which supersedes Columns with more
5//! features and better user experience.
6//!
7#![allow(
8    clippy::cast_possible_truncation,
9    clippy::cast_sign_loss,
10    clippy::as_conversions,
11    clippy::unnecessary_cast
12)]
13use crate::Ui;
14use crate::sys;
15use bitflags::bitflags;
16
17fn current_columns() -> *mut sys::ImGuiOldColumns {
18    unsafe {
19        let window = sys::igGetCurrentWindowRead();
20        if window.is_null() {
21            std::ptr::null_mut()
22        } else {
23            (*window).DC.CurrentColumns
24        }
25    }
26}
27
28fn assert_no_current_columns(caller: &str) {
29    assert!(
30        current_columns().is_null(),
31        "{caller} cannot be called while another legacy columns layout is active"
32    );
33}
34
35fn assert_current_columns(caller: &str) -> *mut sys::ImGuiOldColumns {
36    let columns = current_columns();
37    assert!(
38        !columns.is_null(),
39        "{caller} must be called inside a legacy columns layout"
40    );
41    columns
42}
43
44fn assert_columns_count(count: i32, caller: &str) {
45    assert!(count >= 1, "{caller} count must be at least 1");
46}
47
48fn assert_finite_f32(caller: &str, name: &str, value: f32) {
49    assert!(value.is_finite(), "{caller} {name} must be finite");
50}
51
52fn assert_non_negative_f32(caller: &str, name: &str, value: f32) {
53    assert_finite_f32(caller, name, value);
54    assert!(value >= 0.0, "{caller} {name} must be non-negative");
55}
56
57fn validate_old_column_flags(caller: &str, flags: OldColumnFlags) {
58    let unsupported = flags.bits() & !OldColumnFlags::all().bits();
59    assert!(
60        unsupported == 0,
61        "{caller} received unsupported ImGuiOldColumnFlags bits: 0x{unsupported:X}"
62    );
63}
64
65fn resolve_column_index(column_index: i32, allow_trailing_offset: bool, caller: &str) -> i32 {
66    let columns = assert_current_columns(caller);
67    let column_index = if column_index < 0 {
68        unsafe { (*columns).Current }
69    } else {
70        column_index
71    };
72    let upper_bound = unsafe {
73        if allow_trailing_offset {
74            (*columns).Count
75        } else {
76            (*columns).Count - 1
77        }
78    };
79    assert!(
80        (0..=upper_bound).contains(&column_index),
81        "{caller} column index {column_index} is outside the allowed range 0..={upper_bound}"
82    );
83    column_index
84}
85
86bitflags! {
87    /// Flags for old columns system
88    #[repr(transparent)]
89    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90    pub struct OldColumnFlags: i32 {
91        /// No flags
92        const NONE = sys::ImGuiOldColumnFlags_None as i32;
93        /// Disable column dividers
94        const NO_BORDER = sys::ImGuiOldColumnFlags_NoBorder as i32;
95        /// Disable resizing columns by dragging dividers
96        const NO_RESIZE = sys::ImGuiOldColumnFlags_NoResize as i32;
97        /// Disable column width preservation when the total width changes
98        const NO_PRESERVE_WIDTHS = sys::ImGuiOldColumnFlags_NoPreserveWidths as i32;
99        /// Disable forcing columns to fit within window
100        const NO_FORCE_WITHIN_WINDOW = sys::ImGuiOldColumnFlags_NoForceWithinWindow as i32;
101        /// Restore pre-1.51 behavior of extending the parent window contents size
102        const GROW_PARENT_CONTENTS_SIZE = sys::ImGuiOldColumnFlags_GrowParentContentsSize as i32;
103    }
104}
105
106impl Default for OldColumnFlags {
107    fn default() -> Self {
108        OldColumnFlags::NONE
109    }
110}
111
112/// # Columns
113impl Ui {
114    /// Creates columns layout.
115    ///
116    /// # Arguments
117    /// * `count` - Number of columns (must be >= 1)
118    /// * `id` - Optional ID for the columns (can be empty string)
119    /// * `border` - Whether to draw borders between columns
120    #[doc(alias = "Columns")]
121    pub fn columns(&self, count: i32, id: impl AsRef<str>, border: bool) {
122        assert_columns_count(count, "Ui::columns()");
123        unsafe { sys::igColumns(count, self.scratch_txt(id), border) }
124    }
125
126    /// Begin columns layout with advanced flags.
127    ///
128    /// # Arguments
129    /// * `id` - ID for the columns
130    /// * `count` - Number of columns (must be >= 1)
131    /// * `flags` - Column flags
132    #[doc(alias = "BeginColumns")]
133    pub fn begin_columns(&self, id: impl AsRef<str>, count: i32, flags: OldColumnFlags) {
134        assert_columns_count(count, "Ui::begin_columns()");
135        validate_old_column_flags("Ui::begin_columns()", flags);
136        assert_no_current_columns("Ui::begin_columns()");
137        unsafe { sys::igBeginColumns(self.scratch_txt(id), count, flags.bits()) }
138    }
139
140    /// Begin columns layout with advanced flags and return a token that ends columns on drop.
141    #[doc(alias = "BeginColumns")]
142    pub fn begin_columns_token(
143        &self,
144        id: impl AsRef<str>,
145        count: i32,
146        flags: OldColumnFlags,
147    ) -> ColumnsToken<'_> {
148        self.begin_columns(id, count, flags);
149        ColumnsToken { ui: self }
150    }
151
152    /// End columns layout.
153    #[doc(alias = "EndColumns")]
154    pub fn end_columns(&self) {
155        assert_current_columns("Ui::end_columns()");
156        unsafe { sys::igEndColumns() }
157    }
158
159    /// Switches to the next column.
160    ///
161    /// If the current row is finished, switches to first column of the next row
162    #[doc(alias = "NextColumn")]
163    pub fn next_column(&self) {
164        unsafe { sys::igNextColumn() }
165    }
166
167    /// Returns the index of the current column
168    #[doc(alias = "GetColumnIndex")]
169    pub fn current_column_index(&self) -> i32 {
170        unsafe { sys::igGetColumnIndex() }
171    }
172
173    /// Returns the width of the current column (in pixels)
174    #[doc(alias = "GetColumnWidth")]
175    pub fn current_column_width(&self) -> f32 {
176        unsafe { sys::igGetColumnWidth(-1) }
177    }
178
179    /// Returns the width of the given column (in pixels)
180    #[doc(alias = "GetColumnWidth")]
181    pub fn column_width(&self, column_index: i32) -> f32 {
182        let column_index = if current_columns().is_null() {
183            column_index
184        } else {
185            resolve_column_index(column_index, false, "Ui::column_width()")
186        };
187        unsafe { sys::igGetColumnWidth(column_index) }
188    }
189
190    /// Sets the width of the current column (in pixels)
191    #[doc(alias = "SetColumnWidth")]
192    pub fn set_current_column_width(&self, width: f32) {
193        assert_non_negative_f32("Ui::set_current_column_width()", "width", width);
194        unsafe { sys::igSetColumnWidth(-1, width) };
195    }
196
197    /// Sets the width of the given column (in pixels)
198    #[doc(alias = "SetColumnWidth")]
199    pub fn set_column_width(&self, column_index: i32, width: f32) {
200        let column_index = resolve_column_index(column_index, false, "Ui::set_column_width()");
201        assert_non_negative_f32("Ui::set_column_width()", "width", width);
202        unsafe { sys::igSetColumnWidth(column_index, width) };
203    }
204
205    /// Returns the offset of the current column (in pixels from the left side of the content region)
206    #[doc(alias = "GetColumnOffset")]
207    pub fn current_column_offset(&self) -> f32 {
208        unsafe { sys::igGetColumnOffset(-1) }
209    }
210
211    /// Returns the offset of the given column (in pixels from the left side of the content region)
212    #[doc(alias = "GetColumnOffset")]
213    pub fn column_offset(&self, column_index: i32) -> f32 {
214        let column_index = if current_columns().is_null() {
215            column_index
216        } else {
217            resolve_column_index(column_index, true, "Ui::column_offset()")
218        };
219        unsafe { sys::igGetColumnOffset(column_index) }
220    }
221
222    /// Sets the offset of the current column (in pixels from the left side of the content region)
223    #[doc(alias = "SetColumnOffset")]
224    pub fn set_current_column_offset(&self, offset_x: f32) {
225        assert_non_negative_f32("Ui::set_current_column_offset()", "offset_x", offset_x);
226        unsafe { sys::igSetColumnOffset(-1, offset_x) };
227    }
228
229    /// Sets the offset of the given column (in pixels from the left side of the content region)
230    #[doc(alias = "SetColumnOffset")]
231    pub fn set_column_offset(&self, column_index: i32, offset_x: f32) {
232        let column_index = resolve_column_index(column_index, true, "Ui::set_column_offset()");
233        assert_non_negative_f32("Ui::set_column_offset()", "offset_x", offset_x);
234        unsafe { sys::igSetColumnOffset(column_index, offset_x) };
235    }
236
237    /// Returns the current amount of columns
238    #[doc(alias = "GetColumnsCount")]
239    pub fn column_count(&self) -> i32 {
240        unsafe { sys::igGetColumnsCount() }
241    }
242
243    // ============================================================================
244    // Advanced column utilities
245    // ============================================================================
246
247    /// Push column clip rect for the given column index.
248    /// This is useful for custom drawing within columns.
249    #[doc(alias = "PushColumnClipRect")]
250    pub fn push_column_clip_rect(&self, column_index: i32) {
251        let column_index = resolve_column_index(column_index, false, "Ui::push_column_clip_rect()");
252        unsafe { sys::igPushColumnClipRect(column_index) }
253    }
254
255    /// Push columns background for drawing.
256    #[doc(alias = "PushColumnsBackground")]
257    pub fn push_columns_background(&self) {
258        assert_current_columns("Ui::push_columns_background()");
259        unsafe { sys::igPushColumnsBackground() }
260    }
261
262    /// Pop columns background.
263    #[doc(alias = "PopColumnsBackground")]
264    pub fn pop_columns_background(&self) {
265        assert_current_columns("Ui::pop_columns_background()");
266        unsafe { sys::igPopColumnsBackground() }
267    }
268
269    /// Get columns ID for the given string ID and count.
270    #[doc(alias = "GetColumnsID")]
271    pub fn get_columns_id(&self, str_id: impl AsRef<str>, count: i32) -> u32 {
272        assert_columns_count(count, "Ui::get_columns_id()");
273        unsafe { sys::igGetColumnsID(self.scratch_txt(str_id), count) }
274    }
275
276    // ============================================================================
277    // Column state utilities
278    // ============================================================================
279
280    /// Check if any column in the current legacy columns set is being resized.
281    ///
282    /// Returns `false` when the current window is not inside a legacy columns set.
283    pub fn is_any_column_resizing(&self) -> bool {
284        unsafe {
285            let window = sys::igGetCurrentWindowRead();
286            if window.is_null() {
287                return false;
288            }
289
290            let columns = (*window).DC.CurrentColumns;
291            if columns.is_null() {
292                return false;
293            }
294
295            (*columns).IsBeingResized
296        }
297    }
298
299    /// Get the total width of all columns.
300    pub fn get_columns_total_width(&self) -> f32 {
301        let count = self.column_count();
302        if count <= 0 {
303            return 0.0;
304        }
305
306        let mut total_width = 0.0;
307        for i in 0..count {
308            total_width += self.column_width(i);
309        }
310        total_width
311    }
312
313    /// Set all columns to equal width.
314    pub fn set_columns_equal_width(&self) {
315        let count = self.column_count();
316        if count <= 1 {
317            return;
318        }
319
320        let total_width = self.get_columns_total_width();
321        let equal_width = total_width / count as f32;
322
323        for i in 0..count {
324            self.set_column_width(i, equal_width);
325        }
326    }
327
328    /// Get column width as a percentage of total width.
329    pub fn get_column_width_percentage(&self, column_index: i32) -> f32 {
330        let total_width = self.get_columns_total_width();
331        if total_width <= 0.0 {
332            return 0.0;
333        }
334
335        let column_width = self.column_width(column_index);
336        (column_width / total_width) * 100.0
337    }
338
339    /// Set column width as a percentage of total width.
340    pub fn set_column_width_percentage(&self, column_index: i32, percentage: f32) {
341        assert_non_negative_f32(
342            "Ui::set_column_width_percentage()",
343            "percentage",
344            percentage,
345        );
346        let total_width = self.get_columns_total_width();
347        if total_width <= 0.0 {
348            return;
349        }
350
351        let new_width = (total_width * percentage) / 100.0;
352        self.set_column_width(column_index, new_width);
353    }
354}
355
356/// Token representing an active columns layout.
357#[must_use]
358pub struct ColumnsToken<'ui> {
359    ui: &'ui Ui,
360}
361
362impl Drop for ColumnsToken<'_> {
363    fn drop(&mut self) {
364        self.ui.end_columns();
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::OldColumnFlags;
371
372    fn setup_context() -> crate::Context {
373        let mut ctx = crate::Context::create();
374        let _ = ctx.font_atlas_mut().build();
375        ctx.io_mut().set_display_size([128.0, 128.0]);
376        ctx.io_mut().set_delta_time(1.0 / 60.0);
377        ctx
378    }
379
380    #[test]
381    fn is_any_column_resizing_reads_current_columns_state() {
382        let mut ctx = setup_context();
383        let ui = ctx.frame();
384
385        ui.window("columns_resize_test").build(|| {
386            assert!(!ui.is_any_column_resizing());
387
388            let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
389            let window = unsafe { crate::sys::igGetCurrentWindowRead() };
390            assert!(!window.is_null());
391
392            let columns = unsafe { (*window).DC.CurrentColumns };
393            assert!(!columns.is_null());
394            assert!(!ui.is_any_column_resizing());
395
396            unsafe {
397                (*columns).IsBeingResized = true;
398            }
399
400            assert!(ui.is_any_column_resizing());
401        });
402    }
403
404    #[test]
405    fn columns_reject_invalid_counts_and_nested_layouts() {
406        let mut ctx = setup_context();
407        let ui = ctx.frame();
408
409        ui.window("columns_invalid_counts").build(|| {
410            assert!(
411                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
412                    ui.columns(0, "bad_columns", true);
413                }))
414                .is_err()
415            );
416            assert!(
417                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
418                    let _columns = ui.begin_columns_token("bad_columns", 0, OldColumnFlags::NONE);
419                }))
420                .is_err()
421            );
422
423            let _columns = ui.begin_columns_token("outer_columns", 2, OldColumnFlags::NONE);
424            assert!(
425                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
426                    let _nested = ui.begin_columns_token("nested_columns", 2, OldColumnFlags::NONE);
427                }))
428                .is_err()
429            );
430        });
431    }
432
433    #[test]
434    fn columns_reject_out_of_range_indices_before_ffi() {
435        let mut ctx = setup_context();
436        let ui = ctx.frame();
437
438        ui.window("columns_index_bounds").build(|| {
439            let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
440
441            assert!(
442                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
443                    let _ = ui.column_width(2);
444                }))
445                .is_err()
446            );
447            assert!(
448                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
449                    ui.set_column_width(2, 10.0);
450                }))
451                .is_err()
452            );
453            assert!(
454                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
455                    let _ = ui.column_offset(3);
456                }))
457                .is_err()
458            );
459            assert!(
460                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
461                    ui.push_column_clip_rect(2);
462                }))
463                .is_err()
464            );
465        });
466    }
467
468    #[test]
469    fn columns_reject_invalid_flags_and_numeric_inputs_before_ffi() {
470        let mut ctx = setup_context();
471        let ui = ctx.frame();
472
473        ui.window("columns_numeric_bounds").build(|| {
474            assert!(
475                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
476                    let _columns = ui.begin_columns_token(
477                        "bad_flags",
478                        2,
479                        OldColumnFlags::from_bits_retain(1 << 16),
480                    );
481                }))
482                .is_err()
483            );
484
485            let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
486            ui.set_current_column_width(32.0);
487            ui.set_current_column_offset(0.0);
488            ui.set_column_width(1, 16.0);
489            ui.set_column_offset(1, 8.0);
490            ui.set_column_width_percentage(1, 25.0);
491
492            ui.set_current_column_width(0.0);
493            assert!(
494                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
495                    ui.set_column_width(1, f32::NAN);
496                }))
497                .is_err()
498            );
499            assert!(
500                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
501                    ui.set_current_column_offset(-1.0);
502                }))
503                .is_err()
504            );
505            assert!(
506                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
507                    ui.set_column_offset(1, f32::INFINITY);
508                }))
509                .is_err()
510            );
511            assert!(
512                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
513                    ui.set_column_width_percentage(1, -1.0);
514                }))
515                .is_err()
516            );
517        });
518    }
519}