Skip to main content

dear_imgui_rs/widget/
table.rs

1//! Tables
2//!
3//! Modern multi-column layout and data table API with rich configuration.
4//! Use `TableBuilder` to declare columns and build rows/cells ergonomically.
5//!
6//! Quick example (builder):
7//! ```no_run
8//! # use dear_imgui_rs::*;
9//! # let mut ctx = Context::create();
10//! # let ui = ctx.frame();
11//! ui.table("perf")
12//!     .flags(TableFlags::RESIZABLE)
13//!     .column("Name").width(120.0).done()
14//!     .column("Value").weight(1.0).done()
15//!     .headers(true)
16//!     .build(|ui| {
17//!         ui.table_next_row();
18//!         ui.table_next_column(); ui.text("CPU");
19//!         ui.table_next_column(); ui.text("Intel");
20//!     });
21//! ```
22//!
23//! Quick example (manual API):
24//! ```no_run
25//! # use dear_imgui_rs::*;
26//! # let mut ctx = Context::create();
27//! # let ui = ctx.frame();
28//! if let Some(_t) = ui.begin_table("t", 2) {
29//!     ui.table_next_row();
30//!     ui.table_next_column(); ui.text("A");
31//!     ui.table_next_column(); ui.text("B");
32//! }
33//! ```
34//!
35#![allow(
36    clippy::cast_possible_truncation,
37    clippy::cast_sign_loss,
38    clippy::as_conversions
39)]
40use crate::draw::ImColor32;
41use crate::internal::len_i32;
42use crate::sys;
43use crate::ui::Ui;
44use crate::widget::{
45    TableColumnFlags, TableColumnIndent, TableColumnStateFlags, TableColumnWidth, TableFlags,
46    TableOptions, TableSizingPolicy,
47};
48#[cfg(feature = "serde")]
49use serde::{Deserialize, Serialize};
50use std::borrow::Cow;
51use std::ffi::CStr;
52
53const TABLE_MAX_COLUMNS: usize = 512;
54
55fn table_column_count_to_i32(column_count: usize) -> i32 {
56    assert!(
57        column_count > 0,
58        "table column_count must be greater than zero"
59    );
60    assert!(
61        column_count < TABLE_MAX_COLUMNS,
62        "table column_count must be less than {TABLE_MAX_COLUMNS}"
63    );
64    i32::try_from(column_count).expect("table column_count exceeded ImGui's i32 range")
65}
66
67fn current_table() -> *mut sys::ImGuiTable {
68    unsafe { sys::igGetCurrentTable() }
69}
70
71fn current_table_if_any() -> Option<*mut sys::ImGuiTable> {
72    let table = current_table();
73    (!table.is_null()).then_some(table)
74}
75
76fn assert_current_table(caller: &str) -> *mut sys::ImGuiTable {
77    let table = current_table();
78    assert!(
79        !table.is_null(),
80        "{caller} must be called inside a BeginTable/EndTable scope"
81    );
82    table
83}
84
85fn assert_valid_table_column_in(table: *mut sys::ImGuiTable, column_n: i32, caller: &str) {
86    let column_count = unsafe { (*table).ColumnsCount };
87    assert!(
88        (0..column_count).contains(&column_n),
89        "{caller} column index {column_n} is outside the current table column range 0..{column_count}"
90    );
91}
92
93fn assert_valid_table_column(column_n: i32, caller: &str) {
94    let table = assert_current_table(caller);
95    assert_valid_table_column_in(table, column_n, caller);
96}
97
98fn assert_non_negative_finite_f32(caller: &str, name: &str, value: f32) {
99    assert!(value.is_finite(), "{caller} {name} must be finite");
100    assert!(value >= 0.0, "{caller} {name} must be non-negative");
101}
102
103fn resolve_table_column(column_n: i32, caller: &str) -> i32 {
104    let table = assert_current_table(caller);
105    let column_n = if column_n < 0 {
106        unsafe { (*table).CurrentColumn }
107    } else {
108        column_n
109    };
110    assert_valid_table_column_in(table, column_n, caller);
111    column_n
112}
113
114fn assert_current_table_has_flags(flags: TableFlags, caller: &str) {
115    let table = assert_current_table(caller);
116    let table_flags = TableFlags::from_bits_truncate(unsafe { (*table).Flags });
117    assert!(
118        table_flags.contains(flags),
119        "{caller} requires the current table to have {flags:?}"
120    );
121}
122
123fn assert_table_setup_phase(caller: &str) {
124    let table = assert_current_table(caller);
125    assert!(
126        !unsafe { (*table).IsLayoutLocked },
127        "{caller} must be called before the first table row or column"
128    );
129}
130
131fn assert_table_column_width_phase(caller: &str) {
132    let table = assert_current_table(caller);
133    assert!(
134        !unsafe { (*table).IsLayoutLocked },
135        "{caller} must be called before the table layout is locked"
136    );
137    assert!(
138        unsafe { (*table).MinColumnWidth > 0.0 },
139        "{caller} requires Dear ImGui table layout metrics to be initialized"
140    );
141}
142
143fn assert_current_table_cell(caller: &str) {
144    let table = assert_current_table(caller);
145    let (current_column, column_count) = unsafe { ((*table).CurrentColumn, (*table).ColumnsCount) };
146    assert!(
147        (0..column_count).contains(&current_column),
148        "{caller} must be called while a table cell is current"
149    );
150}
151
152fn assert_current_table_row(caller: &str) {
153    let table = assert_current_table(caller);
154    assert!(
155        unsafe { (*table).CurrentRow } >= 0,
156        "{caller} must be called while a table row is current"
157    );
158}
159
160/// Table column setup information
161#[derive(Clone, Debug)]
162pub struct TableColumnSetup<Name> {
163    pub name: Name,
164    pub flags: TableColumnFlags,
165    pub width: Option<TableColumnWidth>,
166    pub indent: Option<TableColumnIndent>,
167    pub user_id: u32,
168}
169
170impl<Name> TableColumnSetup<Name> {
171    /// Creates a new table column setup
172    pub fn new(name: Name) -> Self {
173        Self {
174            name,
175            flags: TableColumnFlags::NONE,
176            width: None,
177            indent: None,
178            user_id: 0,
179        }
180    }
181
182    /// Sets the column flags
183    pub fn flags(mut self, flags: TableColumnFlags) -> Self {
184        self.flags = flags;
185        self
186    }
187
188    /// Sets a fixed initial column width in pixels.
189    pub fn fixed_width(mut self, width: f32) -> Self {
190        self.width = Some(TableColumnWidth::Fixed(width));
191        self
192    }
193
194    /// Sets an initial stretch weight for this column.
195    pub fn stretch_weight(mut self, weight: f32) -> Self {
196        self.width = Some(TableColumnWidth::Stretch(weight));
197        self
198    }
199
200    /// Sets this column's indentation policy.
201    pub fn indent(mut self, indent: TableColumnIndent) -> Self {
202        self.indent = Some(indent);
203        self
204    }
205
206    /// Enables or disables indentation for this column.
207    pub fn indent_enabled(mut self, enabled: bool) -> Self {
208        self.indent = Some(if enabled {
209            TableColumnIndent::Enable
210        } else {
211            TableColumnIndent::Disable
212        });
213        self
214    }
215
216    /// Sets the user ID
217    pub fn user_id(mut self, id: u32) -> Self {
218        self.user_id = id;
219        self
220    }
221}
222
223/// # Table Widgets
224impl Ui {
225    /// Start a Table builder for ergonomic setup + headers + options.
226    ///
227    /// Example
228    /// ```no_run
229    /// # use dear_imgui_rs::*;
230    /// # fn demo(ui: &Ui) {
231    /// ui.table("perf")
232    ///     .flags(TableFlags::RESIZABLE | TableFlags::SORTABLE)
233    ///     .outer_size([600.0, 240.0])
234    ///     .freeze(1, 1)
235    ///     .column("Name").width(140.0).done()
236    ///     .column("Value").weight(1.0).done()
237    ///     .headers(true)
238    ///     .build(|ui| {
239    ///         ui.table_next_row();
240    ///         ui.table_next_column(); ui.text("CPU");
241    ///         ui.table_next_column(); ui.text("Intel");
242    ///     });
243    /// # }
244    /// ```
245    pub fn table<'ui>(&'ui self, str_id: impl Into<Cow<'ui, str>>) -> TableBuilder<'ui> {
246        TableBuilder::new(self, str_id)
247    }
248    /// Begins a table with no flags and with standard sizing constraints.
249    ///
250    /// This does no work on styling the headers (the top row) -- see either
251    /// [begin_table_header](Self::begin_table_header) or the more complex
252    /// [table_setup_column](Self::table_setup_column).
253    #[must_use = "if return is dropped immediately, table is ended immediately."]
254    pub fn begin_table(
255        &self,
256        str_id: impl AsRef<str>,
257        column_count: usize,
258    ) -> Option<TableToken<'_>> {
259        self.begin_table_with_flags(str_id, column_count, TableFlags::NONE)
260    }
261
262    /// Begins a table with flags and with standard sizing constraints.
263    #[must_use = "if return is dropped immediately, table is ended immediately."]
264    pub fn begin_table_with_flags(
265        &self,
266        str_id: impl AsRef<str>,
267        column_count: usize,
268        flags: impl Into<TableOptions>,
269    ) -> Option<TableToken<'_>> {
270        self.begin_table_with_sizing(str_id, column_count, flags, [0.0, 0.0], 0.0)
271    }
272
273    /// Begins a table with all flags and sizing constraints. This is the base method,
274    /// and gives users the most flexibility.
275    #[must_use = "if return is dropped immediately, table is ended immediately."]
276    pub fn begin_table_with_sizing(
277        &self,
278        str_id: impl AsRef<str>,
279        column_count: usize,
280        flags: impl Into<TableOptions>,
281        outer_size: impl Into<[f32; 2]>,
282        inner_width: f32,
283    ) -> Option<TableToken<'_>> {
284        let options = flags.into();
285        options.validate("Ui::begin_table_with_sizing()");
286        assert!(
287            inner_width.is_finite(),
288            "Ui::begin_table_with_sizing() inner_width must be finite"
289        );
290        assert!(
291            !options.flags.contains(TableFlags::SCROLL_X) || inner_width >= 0.0,
292            "Ui::begin_table_with_sizing() inner_width must be non-negative when SCROLL_X is enabled"
293        );
294        let outer_size = outer_size.into();
295        assert!(
296            outer_size[0].is_finite() && outer_size[1].is_finite(),
297            "Ui::begin_table_with_sizing() outer_size must contain finite values"
298        );
299        let str_id_ptr = self.scratch_txt(str_id);
300        let outer_size_vec: sys::ImVec2 = outer_size.into();
301        let column_count = table_column_count_to_i32(column_count);
302
303        let should_render = unsafe {
304            sys::igBeginTable(
305                str_id_ptr,
306                column_count,
307                options.raw(),
308                outer_size_vec,
309                inner_width,
310            )
311        };
312
313        if should_render {
314            Some(TableToken::new(self))
315        } else {
316            None
317        }
318    }
319
320    /// Begins a table with no flags and with standard sizing constraints.
321    ///
322    /// Takes an array of table header information, the length of which determines
323    /// how many columns will be created.
324    #[must_use = "if return is dropped immediately, table is ended immediately."]
325    pub fn begin_table_header<Name: AsRef<str>, const N: usize>(
326        &self,
327        str_id: impl AsRef<str>,
328        column_data: [TableColumnSetup<Name>; N],
329    ) -> Option<TableToken<'_>> {
330        self.begin_table_header_with_flags(str_id, column_data, TableFlags::NONE)
331    }
332
333    /// Begins a table with flags and with standard sizing constraints.
334    ///
335    /// Takes an array of table header information, the length of which determines
336    /// how many columns will be created.
337    #[must_use = "if return is dropped immediately, table is ended immediately."]
338    pub fn begin_table_header_with_flags<Name: AsRef<str>, const N: usize>(
339        &self,
340        str_id: impl AsRef<str>,
341        column_data: [TableColumnSetup<Name>; N],
342        flags: impl Into<TableOptions>,
343    ) -> Option<TableToken<'_>> {
344        if let Some(token) = self.begin_table_with_flags(str_id, N, flags) {
345            // Setup columns
346            for column in &column_data {
347                self.table_setup_column_with_indent(
348                    &column.name,
349                    column.flags,
350                    column.width,
351                    column.indent,
352                    column.user_id,
353                );
354            }
355            self.table_headers_row();
356            Some(token)
357        } else {
358            None
359        }
360    }
361
362    /// Setup a column for the current table
363    pub fn table_setup_column(
364        &self,
365        label: impl AsRef<str>,
366        flags: TableColumnFlags,
367        width: Option<TableColumnWidth>,
368        user_id: u32,
369    ) {
370        self.table_setup_column_with_indent(label, flags, width, None, user_id);
371    }
372
373    /// Setup a column for the current table, including explicit indent policy.
374    pub fn table_setup_column_with_indent(
375        &self,
376        label: impl AsRef<str>,
377        flags: TableColumnFlags,
378        width: Option<TableColumnWidth>,
379        indent: Option<TableColumnIndent>,
380        user_id: u32,
381    ) {
382        let table = assert_current_table("Ui::table_setup_column_with_indent()");
383        assert!(
384            unsafe { i32::from((*table).DeclColumnsCount) < (*table).ColumnsCount },
385            "Ui::table_setup_column_with_indent() called more times than the table column count"
386        );
387        assert_table_setup_phase("Ui::table_setup_column_with_indent()");
388        flags.validate_for_setup("Ui::table_setup_column_with_indent()", width, indent);
389        let init_width_or_weight = width.map_or(0.0, TableColumnWidth::value);
390        assert!(
391            init_width_or_weight.is_finite(),
392            "Ui::table_setup_column_with_indent() width or weight must be finite"
393        );
394        let label_ptr = self.scratch_txt(label);
395        let raw_flags = flags.bits()
396            | width.map_or(0, TableColumnWidth::raw_flags)
397            | indent.map_or(0, TableColumnIndent::raw_flags);
398        unsafe {
399            sys::igTableSetupColumn(label_ptr, raw_flags, init_width_or_weight, user_id);
400        }
401    }
402
403    /// Setup a column with a fixed initial width.
404    pub fn table_setup_column_fixed_width(
405        &self,
406        label: impl AsRef<str>,
407        flags: TableColumnFlags,
408        width: f32,
409        user_id: u32,
410    ) {
411        self.table_setup_column(label, flags, Some(TableColumnWidth::Fixed(width)), user_id);
412    }
413
414    /// Setup a column with a stretch weight.
415    pub fn table_setup_column_stretch_weight(
416        &self,
417        label: impl AsRef<str>,
418        flags: TableColumnFlags,
419        weight: f32,
420        user_id: u32,
421    ) {
422        self.table_setup_column(
423            label,
424            flags,
425            Some(TableColumnWidth::Stretch(weight)),
426            user_id,
427        );
428    }
429
430    /// Submit all headers cells based on data provided to TableSetupColumn() + submit context menu
431    pub fn table_headers_row(&self) {
432        assert_current_table("Ui::table_headers_row()");
433        unsafe {
434            sys::igTableHeadersRow();
435        }
436    }
437
438    /// Append into the next column (or first column of next row if currently in last column)
439    pub fn table_next_column(&self) -> bool {
440        unsafe { sys::igTableNextColumn() }
441    }
442
443    /// Append into the specified column
444    pub fn table_set_column_index(&self, column_n: i32) -> bool {
445        if let Some(table) = current_table_if_any() {
446            assert_valid_table_column_in(table, column_n, "Ui::table_set_column_index()");
447        }
448        unsafe { sys::igTableSetColumnIndex(column_n) }
449    }
450
451    /// Append into the next row
452    pub fn table_next_row(&self) {
453        self.table_next_row_with_flags(TableRowFlags::NONE, 0.0);
454    }
455
456    /// Append into the next row with flags and minimum height
457    pub fn table_next_row_with_flags(&self, flags: TableRowFlags, min_row_height: f32) {
458        unsafe {
459            sys::igTableNextRow(flags.bits(), min_row_height);
460        }
461    }
462
463    /// Freeze columns/rows so they stay visible when scrolling.
464    #[doc(alias = "TableSetupScrollFreeze")]
465    pub fn table_setup_scroll_freeze(&self, frozen_cols: i32, frozen_rows: i32) {
466        assert_table_setup_phase("Ui::table_setup_scroll_freeze()");
467        assert!(
468            (0..TABLE_MAX_COLUMNS as i32).contains(&frozen_cols),
469            "Ui::table_setup_scroll_freeze() frozen_cols must be in 0..{TABLE_MAX_COLUMNS}"
470        );
471        assert!(
472            (0..128).contains(&frozen_rows),
473            "Ui::table_setup_scroll_freeze() frozen_rows must be in 0..128"
474        );
475        unsafe { sys::igTableSetupScrollFreeze(frozen_cols, frozen_rows) }
476    }
477
478    /// Submit one header cell at current column position.
479    #[doc(alias = "TableHeader")]
480    pub fn table_header(&self, label: impl AsRef<str>) {
481        assert_current_table_cell("Ui::table_header()");
482        let label_ptr = self.scratch_txt(label);
483        unsafe { sys::igTableHeader(label_ptr) }
484    }
485
486    /// Return columns count.
487    #[doc(alias = "TableGetColumnCount")]
488    pub fn table_get_column_count(&self) -> i32 {
489        unsafe { sys::igTableGetColumnCount() }
490    }
491
492    /// Return current column index.
493    #[doc(alias = "TableGetColumnIndex")]
494    pub fn table_get_column_index(&self) -> i32 {
495        unsafe { sys::igTableGetColumnIndex() }
496    }
497
498    /// Return current row index.
499    #[doc(alias = "TableGetRowIndex")]
500    pub fn table_get_row_index(&self) -> i32 {
501        unsafe { sys::igTableGetRowIndex() }
502    }
503
504    /// Return the name of a column by index.
505    #[doc(alias = "TableGetColumnName")]
506    pub fn table_get_column_name(&self, column_n: i32) -> &str {
507        if current_table_if_any().is_some() {
508            resolve_table_column(column_n, "Ui::table_get_column_name()");
509        }
510        unsafe {
511            let ptr = sys::igTableGetColumnName_Int(column_n);
512            if ptr.is_null() {
513                ""
514            } else {
515                CStr::from_ptr(ptr).to_str().unwrap_or("")
516            }
517        }
518    }
519
520    /// Return the flags of a column by index.
521    #[doc(alias = "TableGetColumnFlags")]
522    pub fn table_get_column_flags(&self, column_n: i32) -> TableColumnStateFlags {
523        if let Some(table) = current_table_if_any() {
524            let column_count = unsafe { (*table).ColumnsCount };
525            let resolved_column = if column_n < 0 {
526                unsafe { (*table).CurrentColumn }
527            } else {
528                column_n
529            };
530            assert!(
531                (0..=column_count).contains(&resolved_column),
532                "Ui::table_get_column_flags() column index {column_n} is outside the allowed range -1/current or 0..={column_count}"
533            );
534        }
535        unsafe { TableColumnStateFlags::from_bits_truncate(sys::igTableGetColumnFlags(column_n)) }
536    }
537
538    /// Enable/disable a column by index.
539    #[doc(alias = "TableSetColumnEnabled")]
540    pub fn table_set_column_enabled(&self, column_n: i32, enabled: bool) {
541        assert_current_table_has_flags(TableFlags::HIDEABLE, "Ui::table_set_column_enabled()");
542        resolve_table_column(column_n, "Ui::table_set_column_enabled()");
543        unsafe { sys::igTableSetColumnEnabled(column_n, enabled) }
544    }
545
546    /// Return hovered column index, or -1 when none.
547    #[doc(alias = "TableGetHoveredColumn")]
548    pub fn table_get_hovered_column(&self) -> i32 {
549        unsafe { sys::igTableGetHoveredColumn() }
550    }
551
552    /// Set column width (for fixed-width columns).
553    #[doc(alias = "TableSetColumnWidth")]
554    pub fn table_set_column_width(&self, column_n: i32, width: f32) {
555        assert_table_column_width_phase("Ui::table_set_column_width()");
556        assert_valid_table_column(column_n, "Ui::table_set_column_width()");
557        assert_non_negative_finite_f32("Ui::table_set_column_width()", "width", width);
558        unsafe { sys::igTableSetColumnWidth(column_n, width) }
559    }
560
561    /// Set a table background color target.
562    ///
563    /// Color must be an ImGui-packed ImU32 in ABGR order (IM_COL32).
564    /// Use `crate::colors::Color::to_imgui_u32()` to convert RGBA floats.
565    #[doc(alias = "TableSetBgColor")]
566    pub fn table_set_bg_color_u32(&self, target: TableBgTarget, color: u32, column_n: i32) {
567        assert_current_table_row("Ui::table_set_bg_color_u32()");
568        match target {
569            TableBgTarget::None => panic!("Ui::table_set_bg_color_u32() target cannot be None"),
570            TableBgTarget::CellBg => {
571                resolve_table_column(column_n, "Ui::table_set_bg_color_u32()");
572            }
573            TableBgTarget::RowBg0 | TableBgTarget::RowBg1 => {
574                assert!(
575                    column_n == -1,
576                    "Ui::table_set_bg_color_u32() row background targets require column_n == -1"
577                );
578            }
579        }
580        unsafe { sys::igTableSetBgColor(target as i32, color, column_n) }
581    }
582
583    /// Set a table background color target using RGBA color (0..=1 floats).
584    pub fn table_set_bg_color(&self, target: TableBgTarget, rgba: [f32; 4], column_n: i32) {
585        // Pack to ImGui's ABGR layout.
586        let col = crate::colors::Color::from_array(rgba).to_imgui_u32();
587        self.table_set_bg_color_u32(target, col, column_n);
588    }
589
590    /// Return hovered row index, or -1 when none.
591    #[doc(alias = "TableGetHoveredRow")]
592    pub fn table_get_hovered_row(&self) -> i32 {
593        unsafe { sys::igTableGetHoveredRow() }
594    }
595
596    /// Header row height in pixels.
597    #[doc(alias = "TableGetHeaderRowHeight")]
598    pub fn table_get_header_row_height(&self) -> f32 {
599        unsafe { sys::igTableGetHeaderRowHeight() }
600    }
601
602    /// Set sort direction for a column. Optionally append to existing sort specs (multi-sort).
603    #[doc(alias = "TableSetColumnSortDirection")]
604    pub fn table_set_column_sort_direction(
605        &self,
606        column_n: i32,
607        dir: SortDirection,
608        append_to_sort_specs: bool,
609    ) {
610        assert_valid_table_column(column_n, "Ui::table_set_column_sort_direction()");
611        unsafe { sys::igTableSetColumnSortDirection(column_n, dir.into(), append_to_sort_specs) }
612    }
613
614    /// Get current table sort specifications, if any.
615    /// When non-None and `is_dirty()` is true, the application should sort its data and
616    /// then call `clear_dirty()`.
617    #[doc(alias = "TableGetSortSpecs")]
618    pub fn table_get_sort_specs(&self) -> Option<TableSortSpecs<'_>> {
619        unsafe {
620            let ptr = sys::igTableGetSortSpecs();
621            if ptr.is_null() {
622                None
623            } else {
624                Some(TableSortSpecs::from_raw(ptr))
625            }
626        }
627    }
628}
629
630bitflags::bitflags! {
631    /// Flags for table rows
632    #[repr(transparent)]
633    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
634    pub struct TableRowFlags: i32 {
635        /// No flags
636        const NONE = 0;
637        /// Identify header row (set default background color + width of all columns)
638        const HEADERS = sys::ImGuiTableRowFlags_Headers as i32;
639    }
640}
641
642#[cfg(feature = "serde")]
643impl Serialize for TableRowFlags {
644    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
645    where
646        S: serde::Serializer,
647    {
648        serializer.serialize_i32(self.bits())
649    }
650}
651
652#[cfg(feature = "serde")]
653impl<'de> Deserialize<'de> for TableRowFlags {
654    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
655    where
656        D: serde::Deserializer<'de>,
657    {
658        let bits = i32::deserialize(deserializer)?;
659        Ok(TableRowFlags::from_bits_truncate(bits))
660    }
661}
662
663/// Target for table background colors.
664#[repr(i32)]
665#[derive(Copy, Clone, Debug, PartialEq, Eq)]
666#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
667pub enum TableBgTarget {
668    /// No background target
669    None = sys::ImGuiTableBgTarget_None as i32,
670    /// First alternating row background
671    RowBg0 = sys::ImGuiTableBgTarget_RowBg0 as i32,
672    /// Second alternating row background
673    RowBg1 = sys::ImGuiTableBgTarget_RowBg1 as i32,
674    /// Cell background
675    CellBg = sys::ImGuiTableBgTarget_CellBg as i32,
676}
677
678/// Sorting direction for table columns.
679#[repr(u8)]
680#[derive(Copy, Clone, Debug, PartialEq, Eq)]
681#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
682pub enum SortDirection {
683    None = sys::ImGuiSortDirection_None as u8,
684    Ascending = sys::ImGuiSortDirection_Ascending as u8,
685    Descending = sys::ImGuiSortDirection_Descending as u8,
686}
687
688impl From<SortDirection> for sys::ImGuiSortDirection {
689    #[inline]
690    fn from(value: SortDirection) -> sys::ImGuiSortDirection {
691        match value {
692            SortDirection::None => sys::ImGuiSortDirection_None,
693            SortDirection::Ascending => sys::ImGuiSortDirection_Ascending,
694            SortDirection::Descending => sys::ImGuiSortDirection_Descending,
695        }
696    }
697}
698
699/// One column sort spec.
700#[derive(Copy, Clone, Debug)]
701pub struct TableColumnSortSpec {
702    pub column_user_id: u32,
703    pub column_index: i16,
704    pub sort_order: i16,
705    pub sort_direction: SortDirection,
706}
707
708/// Table sort specs view.
709pub struct TableSortSpecs<'a> {
710    raw: *mut sys::ImGuiTableSortSpecs,
711    _marker: std::marker::PhantomData<&'a Ui>,
712}
713
714impl<'a> TableSortSpecs<'a> {
715    /// # Safety
716    /// `raw` must be a valid pointer returned by ImGui_TableGetSortSpecs for the current table.
717    pub(crate) unsafe fn from_raw(raw: *mut sys::ImGuiTableSortSpecs) -> Self {
718        Self {
719            raw,
720            _marker: std::marker::PhantomData,
721        }
722    }
723
724    /// Whether the specs are marked dirty by dear imgui (you should resort your data).
725    pub fn is_dirty(&self) -> bool {
726        unsafe { (*self.raw).SpecsDirty }
727    }
728
729    /// Clear the dirty flag after you've applied sorting to your data.
730    pub fn clear_dirty(&mut self) {
731        unsafe { (*self.raw).SpecsDirty = false }
732    }
733
734    /// Number of column specs.
735    pub fn len(&self) -> usize {
736        unsafe { (*self.raw).SpecsCount as usize }
737    }
738
739    pub fn is_empty(&self) -> bool {
740        self.len() == 0
741    }
742
743    /// Iterate over column sort specs.
744    pub fn iter(&self) -> TableSortSpecsIter<'_> {
745        TableSortSpecsIter {
746            specs: self,
747            index: 0,
748        }
749    }
750}
751
752/// Iterator over [`TableColumnSortSpec`].
753pub struct TableSortSpecsIter<'a> {
754    specs: &'a TableSortSpecs<'a>,
755    index: usize,
756}
757
758impl<'a> Iterator for TableSortSpecsIter<'a> {
759    type Item = TableColumnSortSpec;
760    fn next(&mut self) -> Option<Self::Item> {
761        if self.index >= self.specs.len() {
762            return None;
763        }
764        unsafe {
765            let ptr = (*self.specs.raw).Specs;
766            if ptr.is_null() {
767                return None;
768            }
769            let spec = &*ptr.add(self.index);
770            self.index += 1;
771            let d = spec.SortDirection as u8;
772            let dir = if d == sys::ImGuiSortDirection_None as u8 {
773                SortDirection::None
774            } else if d == sys::ImGuiSortDirection_Ascending as u8 {
775                SortDirection::Ascending
776            } else if d == sys::ImGuiSortDirection_Descending as u8 {
777                SortDirection::Descending
778            } else {
779                SortDirection::None
780            };
781            Some(TableColumnSortSpec {
782                column_user_id: spec.ColumnUserID,
783                column_index: spec.ColumnIndex,
784                sort_order: spec.SortOrder,
785                sort_direction: dir,
786            })
787        }
788    }
789}
790
791/// Tracks a table that can be ended by calling `.end()` or by dropping
792#[must_use]
793pub struct TableToken<'ui> {
794    _ui: &'ui Ui,
795}
796
797impl<'ui> TableToken<'ui> {
798    /// Creates a new table token
799    fn new(ui: &'ui Ui) -> Self {
800        TableToken { _ui: ui }
801    }
802
803    /// Ends the table
804    pub fn end(self) {
805        // The drop implementation will handle the actual ending
806    }
807}
808
809impl<'ui> Drop for TableToken<'ui> {
810    fn drop(&mut self) {
811        unsafe {
812            sys::igEndTable();
813        }
814    }
815}
816
817/// Tracks a pushed table background draw channel.
818#[must_use = "dropping the token pops the table background draw channel immediately"]
819pub struct TableBackgroundChannelToken<'ui> {
820    _ui: &'ui Ui,
821}
822
823impl<'ui> TableBackgroundChannelToken<'ui> {
824    fn new(ui: &'ui Ui) -> Self {
825        Self { _ui: ui }
826    }
827
828    /// Pops the table background draw channel.
829    pub fn pop(self) {}
830
831    /// Pops the table background draw channel.
832    pub fn end(self) {}
833}
834
835impl Drop for TableBackgroundChannelToken<'_> {
836    fn drop(&mut self) {
837        unsafe {
838            sys::igTablePopBackgroundChannel();
839        }
840    }
841}
842
843/// Tracks a pushed table column draw channel.
844#[must_use = "dropping the token pops the table column draw channel immediately"]
845pub struct TableColumnChannelToken<'ui> {
846    _ui: &'ui Ui,
847}
848
849impl<'ui> TableColumnChannelToken<'ui> {
850    fn new(ui: &'ui Ui) -> Self {
851        Self { _ui: ui }
852    }
853
854    /// Pops the table column draw channel.
855    pub fn pop(self) {}
856
857    /// Pops the table column draw channel.
858    pub fn end(self) {}
859}
860
861impl Drop for TableColumnChannelToken<'_> {
862    fn drop(&mut self) {
863        unsafe {
864            sys::igTablePopColumnChannel();
865        }
866    }
867}
868
869// ============================================================================
870// Additional table convenience APIs
871// ============================================================================
872
873/// Safe description of a single angled header cell.
874#[derive(Copy, Clone, Debug, PartialEq)]
875pub struct TableHeaderData {
876    pub index: i16,
877    pub text_color: ImColor32,
878    pub bg_color0: ImColor32,
879    pub bg_color1: ImColor32,
880}
881
882impl TableHeaderData {
883    pub fn new(
884        index: i16,
885        text_color: ImColor32,
886        bg_color0: ImColor32,
887        bg_color1: ImColor32,
888    ) -> Self {
889        Self {
890            index,
891            text_color,
892            bg_color0,
893            bg_color1,
894        }
895    }
896}
897impl Ui {
898    /// Maximum label width used for angled headers (when enabled in style/options).
899    #[doc(alias = "TableGetHeaderAngledMaxLabelWidth")]
900    pub fn table_get_header_angled_max_label_width(&self) -> f32 {
901        unsafe { sys::igTableGetHeaderAngledMaxLabelWidth() }
902    }
903
904    /// Submit angled headers row (requires style/flags enabling angled headers).
905    #[doc(alias = "TableAngledHeadersRow")]
906    pub fn table_angled_headers_row(&self) {
907        unsafe { sys::igTableAngledHeadersRow() }
908    }
909
910    // Removed legacy TableAngledHeadersRowEx(flags) wrapper; use `table_angled_headers_row_ex_with_data`.
911
912    /// Submit angled headers row with explicit data (Ex variant).
913    ///
914    /// - `row_id`: ImGuiID for the row. Use 0 for automatic if not needed.
915    /// - `angle`: Angle in radians for headers.
916    /// - `max_label_width`: Maximum label width for angled headers.
917    /// - `headers`: Per-column header data.
918    pub fn table_angled_headers_row_ex_with_data(
919        &self,
920        row_id: u32,
921        angle: f32,
922        max_label_width: f32,
923        headers: &[TableHeaderData],
924    ) {
925        if headers.is_empty() {
926            unsafe { sys::igTableAngledHeadersRow() }
927            return;
928        }
929        let count = len_i32(
930            "Ui::table_angled_headers_row_ex_with_data()",
931            "headers",
932            headers.len(),
933        );
934        let table = assert_current_table("Ui::table_angled_headers_row_ex_with_data()");
935        let mut data: Vec<sys::ImGuiTableHeaderData> = Vec::with_capacity(headers.len());
936        for h in headers {
937            assert_valid_table_column_in(
938                table,
939                i32::from(h.index),
940                "Ui::table_angled_headers_row_ex_with_data()",
941            );
942            data.push(sys::ImGuiTableHeaderData {
943                Index: h.index as sys::ImGuiTableColumnIdx,
944                TextColor: u32::from(h.text_color),
945                BgColor0: u32::from(h.bg_color0),
946                BgColor1: u32::from(h.bg_color1),
947            });
948        }
949        unsafe {
950            sys::igTableAngledHeadersRowEx(row_id, angle, max_label_width, data.as_ptr(), count);
951        }
952    }
953
954    /// Push background draw channel for the current table and return a token to pop it.
955    #[doc(alias = "TablePushBackgroundChannel")]
956    pub fn table_push_background_channel(&self) {
957        assert_current_table_cell("Ui::table_push_background_channel()");
958        unsafe { sys::igTablePushBackgroundChannel() }
959    }
960
961    /// Pop background draw channel for the current table.
962    #[doc(alias = "TablePopBackgroundChannel")]
963    pub fn table_pop_background_channel(&self) {
964        assert_current_table_cell("Ui::table_pop_background_channel()");
965        unsafe { sys::igTablePopBackgroundChannel() }
966    }
967
968    /// Push column draw channel for the given column index and return a token to pop it.
969    #[doc(alias = "TablePushColumnChannel")]
970    pub fn table_push_column_channel(&self, column_n: i32) {
971        assert_valid_table_column(column_n, "Ui::table_push_column_channel()");
972        unsafe { sys::igTablePushColumnChannel(column_n) }
973    }
974
975    /// Pop column draw channel.
976    #[doc(alias = "TablePopColumnChannel")]
977    pub fn table_pop_column_channel(&self) {
978        assert_current_table_cell("Ui::table_pop_column_channel()");
979        unsafe { sys::igTablePopColumnChannel() }
980    }
981
982    /// Push background draw channel for the current table and return a token to pop it.
983    #[must_use = "dropping the token pops the table background draw channel immediately"]
984    #[doc(alias = "TablePushBackgroundChannel")]
985    pub fn table_background_channel(&self) -> TableBackgroundChannelToken<'_> {
986        self.table_push_background_channel();
987        TableBackgroundChannelToken::new(self)
988    }
989
990    /// Push column draw channel for the given column index and return a token to pop it.
991    #[must_use = "dropping the token pops the table column draw channel immediately"]
992    #[doc(alias = "TablePushColumnChannel")]
993    pub fn table_column_channel(&self, column_n: i32) -> TableColumnChannelToken<'_> {
994        self.table_push_column_channel(column_n);
995        TableColumnChannelToken::new(self)
996    }
997
998    /// Run a closure after pushing table background channel (auto-pop on return).
999    pub fn with_table_background_channel<R>(&self, f: impl FnOnce() -> R) -> R {
1000        let _token = self.table_background_channel();
1001        f()
1002    }
1003
1004    /// Run a closure after pushing a table column channel (auto-pop on return).
1005    pub fn with_table_column_channel<R>(&self, column_n: i32, f: impl FnOnce() -> R) -> R {
1006        let _token = self.table_column_channel(column_n);
1007        f()
1008    }
1009
1010    /// Open the table context menu for a given column (use -1 for current/default).
1011    #[doc(alias = "TableOpenContextMenu")]
1012    pub fn table_open_context_menu(&self, column_n: Option<i32>) {
1013        unsafe { sys::igTableOpenContextMenu(column_n.unwrap_or(-1)) }
1014    }
1015}
1016
1017// (Optional) RAII versions could be added later if desirable
1018
1019// ============================================================================
1020// TableBuilder: ergonomic table construction
1021// ============================================================================
1022
1023#[cfg(test)]
1024mod tests {
1025    use super::*;
1026
1027    fn setup_context() -> crate::Context {
1028        let mut ctx = crate::Context::create();
1029        {
1030            let io = ctx.io_mut();
1031            io.set_display_size([128.0, 128.0]);
1032            io.set_delta_time(1.0 / 60.0);
1033        }
1034        let _ = ctx.font_atlas_mut().build();
1035        let _ = ctx.set_ini_filename::<std::path::PathBuf>(None);
1036        ctx
1037    }
1038
1039    unsafe fn current_table_draw_channel() -> i32 {
1040        let table = assert_current_table("current_table_draw_channel()");
1041        let draw_list = unsafe { (*(*table).InnerWindow).DrawList };
1042        unsafe { (*draw_list)._Splitter._Current }
1043    }
1044
1045    #[test]
1046    fn table_column_channel_is_popped_after_panic() {
1047        let mut ctx = setup_context();
1048
1049        let ui = ctx.frame();
1050        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1051            let _ = ui.window("table_channel_panic").build(|| {
1052                let _table = ui.begin_table("table", 2).unwrap();
1053                ui.table_next_row();
1054                assert!(ui.table_set_column_index(0));
1055                let initial_channel = unsafe { current_table_draw_channel() };
1056
1057                let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1058                    ui.with_table_column_channel(1, || {
1059                        let pushed_channel = unsafe { current_table_draw_channel() };
1060                        assert_ne!(pushed_channel, initial_channel);
1061                        panic!("forced panic while table column channel is pushed");
1062                    });
1063                }));
1064
1065                assert!(result.is_err());
1066                assert_eq!(unsafe { current_table_draw_channel() }, initial_channel);
1067            });
1068        }));
1069
1070        assert!(result.is_ok());
1071    }
1072
1073    #[test]
1074    fn begin_table_rejects_invalid_column_counts_before_ffi() {
1075        let mut ctx = setup_context();
1076
1077        let ui = ctx.frame();
1078        assert!(
1079            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1080                let _ = ui.begin_table("zero_columns", 0);
1081            }))
1082            .is_err()
1083        );
1084        assert!(
1085            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1086                let _ = ui.begin_table("too_many_columns", TABLE_MAX_COLUMNS);
1087            }))
1088            .is_err()
1089        );
1090    }
1091
1092    #[test]
1093    fn table_column_channel_rejects_out_of_range_column_before_ffi() {
1094        let mut ctx = setup_context();
1095
1096        let ui = ctx.frame();
1097        let _ = ui.window("table_channel_oob").build(|| {
1098            let _table = ui.begin_table("table", 2).unwrap();
1099            ui.table_next_row();
1100            assert!(ui.table_set_column_index(0));
1101
1102            assert!(
1103                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1104                    let _token = ui.table_column_channel(-1);
1105                }))
1106                .is_err()
1107            );
1108            assert!(
1109                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1110                    let _token = ui.table_column_channel(2);
1111                }))
1112                .is_err()
1113            );
1114        });
1115    }
1116
1117    #[test]
1118    fn table_channels_require_current_cell_before_ffi() {
1119        let mut ctx = setup_context();
1120
1121        let ui = ctx.frame();
1122        let _ = ui.window("table_channel_cell_required").build(|| {
1123            let _table = ui.begin_table("table", 2).unwrap();
1124
1125            assert!(
1126                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1127                    let _token = ui.table_background_channel();
1128                }))
1129                .is_err()
1130            );
1131            assert!(
1132                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1133                    ui.table_pop_column_channel();
1134                }))
1135                .is_err()
1136            );
1137        });
1138    }
1139
1140    #[test]
1141    fn table_accessors_reject_invalid_columns_before_ffi() {
1142        let mut ctx = setup_context();
1143
1144        let ui = ctx.frame();
1145        let _ = ui.window("table_accessors_oob").build(|| {
1146            let _table = ui.begin_table("table", 2).unwrap();
1147            ui.table_next_row();
1148            assert!(ui.table_set_column_index(0));
1149
1150            assert!(
1151                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1152                    ui.table_set_column_index(2);
1153                }))
1154                .is_err()
1155            );
1156            assert!(
1157                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1158                    let _ = ui.table_get_column_name(2);
1159                }))
1160                .is_err()
1161            );
1162            assert!(
1163                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1164                    ui.table_set_column_enabled(2, true);
1165                }))
1166                .is_err()
1167            );
1168            assert!(
1169                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1170                    ui.table_set_column_sort_direction(2, SortDirection::Ascending, false);
1171                }))
1172                .is_err()
1173            );
1174        });
1175    }
1176
1177    #[test]
1178    fn table_setup_methods_reject_late_or_excess_calls_before_ffi() {
1179        let mut ctx = setup_context();
1180
1181        let ui = ctx.frame();
1182        let _ = ui.window("table_setup_preconditions").build(|| {
1183            let _table = ui.begin_table("table", 1).unwrap();
1184            ui.table_setup_column("one", TableColumnFlags::NONE, None, 0);
1185
1186            assert!(
1187                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1188                    ui.table_setup_column("two", TableColumnFlags::NONE, None, 0);
1189                }))
1190                .is_err()
1191            );
1192
1193            ui.table_next_row();
1194            assert!(
1195                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1196                    ui.table_setup_scroll_freeze(1, 0);
1197                }))
1198                .is_err()
1199            );
1200            assert!(
1201                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1202                    ui.table_set_column_width(0, 32.0);
1203                }))
1204                .is_err()
1205            );
1206        });
1207    }
1208
1209    #[test]
1210    fn table_set_column_width_rejects_invalid_widths_before_ffi() {
1211        let mut ctx = setup_context();
1212
1213        {
1214            let ui = ctx.frame();
1215            let _ = ui.window("table_width_bounds").build(|| {
1216                let _table = ui.begin_table("table", 1).unwrap();
1217                assert!(
1218                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1219                        ui.table_set_column_width(0, 32.0);
1220                    }))
1221                    .is_err()
1222                );
1223                ui.table_next_row();
1224            });
1225        }
1226        ctx.render();
1227
1228        let ui = ctx.frame();
1229        let _ = ui.window("table_width_bounds").build(|| {
1230            let _table = ui.begin_table("table", 1).unwrap();
1231            ui.table_set_column_width(0, 0.0);
1232            ui.table_set_column_width(0, 32.0);
1233
1234            assert!(
1235                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1236                    ui.table_set_column_width(0, -1.0);
1237                }))
1238                .is_err()
1239            );
1240            assert!(
1241                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1242                    ui.table_set_column_width(0, f32::NAN);
1243                }))
1244                .is_err()
1245            );
1246        });
1247    }
1248
1249    #[test]
1250    fn table_bg_color_validates_target_and_column_before_ffi() {
1251        let mut ctx = setup_context();
1252
1253        let ui = ctx.frame();
1254        let _ = ui.window("table_bg_preconditions").build(|| {
1255            let _table = ui.begin_table("table", 2).unwrap();
1256            ui.table_next_row();
1257            assert!(ui.table_set_column_index(0));
1258
1259            assert!(
1260                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1261                    ui.table_set_bg_color_u32(TableBgTarget::None, 0, -1);
1262                }))
1263                .is_err()
1264            );
1265            assert!(
1266                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1267                    ui.table_set_bg_color_u32(TableBgTarget::CellBg, 0, 2);
1268                }))
1269                .is_err()
1270            );
1271            assert!(
1272                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1273                    ui.table_set_bg_color_u32(TableBgTarget::RowBg0, 0, 0);
1274                }))
1275                .is_err()
1276            );
1277        });
1278    }
1279
1280    #[test]
1281    fn table_angled_headers_validate_indices_before_ffi() {
1282        let mut ctx = setup_context();
1283
1284        let ui = ctx.frame();
1285        let _ = ui.window("table_angled_header_invalid").build(|| {
1286            let _table = ui.begin_table("table", 2).unwrap();
1287            ui.table_setup_column("one", TableColumnFlags::ANGLED_HEADER, None, 0);
1288            ui.table_setup_column("two", TableColumnFlags::ANGLED_HEADER, None, 0);
1289
1290            assert!(
1291                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1292                    let invalid = [TableHeaderData::new(
1293                        -1,
1294                        ImColor32::WHITE,
1295                        ImColor32::BLACK,
1296                        ImColor32::BLACK,
1297                    )];
1298                    ui.table_angled_headers_row_ex_with_data(0, 0.0, 0.0, &invalid);
1299                }))
1300                .is_err()
1301            );
1302            assert!(
1303                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1304                    let invalid = [TableHeaderData::new(
1305                        2,
1306                        ImColor32::WHITE,
1307                        ImColor32::BLACK,
1308                        ImColor32::BLACK,
1309                    )];
1310                    ui.table_angled_headers_row_ex_with_data(0, 0.0, 0.0, &invalid);
1311                }))
1312                .is_err()
1313            );
1314        });
1315    }
1316}
1317
1318/// Builder for ImGui tables with columns + headers + sizing/freeze options.
1319#[derive(Debug)]
1320pub struct TableBuilder<'ui> {
1321    ui: &'ui Ui,
1322    id: Cow<'ui, str>,
1323    flags: TableFlags,
1324    sizing_policy: Option<TableSizingPolicy>,
1325    outer_size: [f32; 2],
1326    inner_width: f32,
1327    columns: Vec<TableColumnSetup<Cow<'ui, str>>>,
1328    use_headers: bool,
1329    freeze: Option<(i32, i32)>,
1330}
1331
1332impl<'ui> TableBuilder<'ui> {
1333    /// Create a new TableBuilder. Prefer using `Ui::table("id")`.
1334    pub fn new(ui: &'ui Ui, str_id: impl Into<Cow<'ui, str>>) -> Self {
1335        Self {
1336            ui,
1337            id: str_id.into(),
1338            flags: TableFlags::NONE,
1339            sizing_policy: None,
1340            outer_size: [0.0, 0.0],
1341            inner_width: 0.0,
1342            columns: Vec::new(),
1343            use_headers: false,
1344            freeze: None,
1345        }
1346    }
1347
1348    /// Set table flags
1349    pub fn flags(mut self, flags: TableFlags) -> Self {
1350        self.flags = flags;
1351        self
1352    }
1353
1354    /// Set the table sizing policy.
1355    pub fn sizing_policy(mut self, policy: TableSizingPolicy) -> Self {
1356        self.sizing_policy = Some(policy);
1357        self
1358    }
1359
1360    /// Set outer size (width, height). Default [0,0]
1361    pub fn outer_size(mut self, size: [f32; 2]) -> Self {
1362        self.outer_size = size;
1363        self
1364    }
1365
1366    /// Set inner width. Default 0.0
1367    pub fn inner_width(mut self, width: f32) -> Self {
1368        self.inner_width = width;
1369        self
1370    }
1371
1372    /// Freeze columns/rows so they stay visible when scrolling
1373    pub fn freeze(mut self, frozen_cols: i32, frozen_rows: i32) -> Self {
1374        self.freeze = Some((frozen_cols, frozen_rows));
1375        self
1376    }
1377
1378    /// Begin defining a column using a chainable ColumnBuilder.
1379    /// Call `.done()` to return to the TableBuilder.
1380    pub fn column(self, name: impl Into<Cow<'ui, str>>) -> ColumnBuilder<'ui> {
1381        ColumnBuilder::new(self, name)
1382    }
1383
1384    /// Replace columns with provided list
1385    pub fn columns<Name: Into<Cow<'ui, str>>>(
1386        mut self,
1387        cols: impl IntoIterator<Item = TableColumnSetup<Name>>,
1388    ) -> Self {
1389        self.columns.clear();
1390        for c in cols {
1391            self.columns.push(TableColumnSetup {
1392                name: c.name.into(),
1393                flags: c.flags,
1394                width: c.width,
1395                indent: c.indent,
1396                user_id: c.user_id,
1397            });
1398        }
1399        self
1400    }
1401
1402    /// Add a single column setup
1403    pub fn add_column<Name: Into<Cow<'ui, str>>>(mut self, col: TableColumnSetup<Name>) -> Self {
1404        self.columns.push(TableColumnSetup {
1405            name: col.name.into(),
1406            flags: col.flags,
1407            width: col.width,
1408            indent: col.indent,
1409            user_id: col.user_id,
1410        });
1411        self
1412    }
1413
1414    /// Auto submit headers row from `TableSetupColumn()` entries
1415    pub fn headers(mut self, enabled: bool) -> Self {
1416        self.use_headers = enabled;
1417        self
1418    }
1419
1420    /// Build the table and run a closure to emit rows/cells
1421    pub fn build(self, f: impl FnOnce(&Ui)) {
1422        let mut options = TableOptions::from(self.flags);
1423        if let Some(policy) = self.sizing_policy {
1424            options = options.sizing_policy(policy);
1425        }
1426        let Some(token) = self.ui.begin_table_with_sizing(
1427            self.id.as_ref(),
1428            self.columns.len(),
1429            options,
1430            self.outer_size,
1431            self.inner_width,
1432        ) else {
1433            return;
1434        };
1435
1436        if let Some((fc, fr)) = self.freeze {
1437            self.ui.table_setup_scroll_freeze(fc, fr);
1438        }
1439
1440        if !self.columns.is_empty() {
1441            for col in &self.columns {
1442                self.ui.table_setup_column_with_indent(
1443                    col.name.as_ref(),
1444                    col.flags,
1445                    col.width,
1446                    col.indent,
1447                    col.user_id,
1448                );
1449            }
1450            if self.use_headers {
1451                self.ui.table_headers_row();
1452            }
1453        }
1454
1455        f(self.ui);
1456
1457        // drop token to end table
1458        token.end();
1459    }
1460}
1461
1462/// Chainable builder for a single column. Use `.done()` to return to the table builder.
1463#[derive(Debug)]
1464pub struct ColumnBuilder<'ui> {
1465    parent: TableBuilder<'ui>,
1466    name: Cow<'ui, str>,
1467    flags: TableColumnFlags,
1468    width: Option<TableColumnWidth>,
1469    indent: Option<TableColumnIndent>,
1470    user_id: u32,
1471}
1472
1473impl<'ui> ColumnBuilder<'ui> {
1474    fn new(parent: TableBuilder<'ui>, name: impl Into<Cow<'ui, str>>) -> Self {
1475        Self {
1476            parent,
1477            name: name.into(),
1478            flags: TableColumnFlags::NONE,
1479            width: None,
1480            indent: None,
1481            user_id: 0,
1482        }
1483    }
1484
1485    /// Set column flags.
1486    pub fn flags(mut self, flags: TableColumnFlags) -> Self {
1487        self.flags = flags;
1488        self
1489    }
1490
1491    /// Set fixed width or stretch weight (ImGui uses same field for both).
1492    pub fn width(mut self, width: f32) -> Self {
1493        self.width = Some(TableColumnWidth::Fixed(width));
1494        self
1495    }
1496
1497    /// Alias of `width()` to express stretch weights.
1498    pub fn weight(mut self, weight: f32) -> Self {
1499        self.width = Some(TableColumnWidth::Stretch(weight));
1500        self
1501    }
1502
1503    /// Set this column's indentation policy.
1504    pub fn indent(mut self, indent: TableColumnIndent) -> Self {
1505        self.indent = Some(indent);
1506        self
1507    }
1508
1509    /// Enable or disable indentation for this column.
1510    pub fn indent_enabled(mut self, enabled: bool) -> Self {
1511        self.indent = Some(if enabled {
1512            TableColumnIndent::Enable
1513        } else {
1514            TableColumnIndent::Disable
1515        });
1516        self
1517    }
1518
1519    /// Toggle angled header flag.
1520    pub fn angled_header(mut self, enabled: bool) -> Self {
1521        if enabled {
1522            self.flags.insert(TableColumnFlags::ANGLED_HEADER);
1523        } else {
1524            self.flags.remove(TableColumnFlags::ANGLED_HEADER);
1525        }
1526        self
1527    }
1528
1529    /// Set user id for this column.
1530    pub fn user_id(mut self, id: u32) -> Self {
1531        self.user_id = id;
1532        self
1533    }
1534
1535    /// Finish this column and return to the table builder.
1536    pub fn done(mut self) -> TableBuilder<'ui> {
1537        self.parent.columns.push(TableColumnSetup {
1538            name: self.name,
1539            flags: self.flags,
1540            width: self.width,
1541            indent: self.indent,
1542            user_id: self.user_id,
1543        });
1544        self.parent
1545    }
1546}