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::sys;
42use crate::ui::Ui;
43use crate::widget::{TableColumnFlags, TableFlags};
44#[cfg(feature = "serde")]
45use serde::{Deserialize, Serialize};
46use std::ffi::CStr;
47
48/// Table column setup information
49#[derive(Clone, Debug)]
50pub struct TableColumnSetup<Name: AsRef<str>> {
51    pub name: Name,
52    pub flags: TableColumnFlags,
53    pub init_width_or_weight: f32,
54    pub user_id: u32,
55}
56
57impl<Name: AsRef<str>> TableColumnSetup<Name> {
58    /// Creates a new table column setup
59    pub fn new(name: Name) -> Self {
60        Self {
61            name,
62            flags: TableColumnFlags::NONE,
63            init_width_or_weight: 0.0,
64            user_id: 0,
65        }
66    }
67
68    /// Sets the column flags
69    pub fn flags(mut self, flags: TableColumnFlags) -> Self {
70        self.flags = flags;
71        self
72    }
73
74    /// Sets the initial width or weight
75    pub fn init_width_or_weight(mut self, width: f32) -> Self {
76        self.init_width_or_weight = width;
77        self
78    }
79
80    /// Sets the user ID
81    pub fn user_id(mut self, id: u32) -> Self {
82        self.user_id = id;
83        self
84    }
85}
86
87/// # Table Widgets
88impl Ui {
89    /// Start a Table builder for ergonomic setup + headers + options.
90    ///
91    /// Example
92    /// ```no_run
93    /// # use dear_imgui_rs::*;
94    /// # fn demo(ui: &Ui) {
95    /// ui.table("perf")
96    ///     .flags(TableFlags::RESIZABLE | TableFlags::SORTABLE)
97    ///     .outer_size([600.0, 240.0])
98    ///     .freeze(1, 1)
99    ///     .column("Name").width(140.0).done()
100    ///     .column("Value").weight(1.0).done()
101    ///     .headers(true)
102    ///     .build(|ui| {
103    ///         ui.table_next_row();
104    ///         ui.table_next_column(); ui.text("CPU");
105    ///         ui.table_next_column(); ui.text("Intel");
106    ///     });
107    /// # }
108    /// ```
109    pub fn table(&self, str_id: impl AsRef<str>) -> TableBuilder<'_> {
110        TableBuilder::new(self, str_id)
111    }
112    /// Begins a table with no flags and with standard sizing constraints.
113    ///
114    /// This does no work on styling the headers (the top row) -- see either
115    /// [begin_table_header](Self::begin_table_header) or the more complex
116    /// [table_setup_column](Self::table_setup_column).
117    #[must_use = "if return is dropped immediately, table is ended immediately."]
118    pub fn begin_table(
119        &self,
120        str_id: impl AsRef<str>,
121        column_count: usize,
122    ) -> Option<TableToken<'_>> {
123        self.begin_table_with_flags(str_id, column_count, TableFlags::NONE)
124    }
125
126    /// Begins a table with flags and with standard sizing constraints.
127    #[must_use = "if return is dropped immediately, table is ended immediately."]
128    pub fn begin_table_with_flags(
129        &self,
130        str_id: impl AsRef<str>,
131        column_count: usize,
132        flags: TableFlags,
133    ) -> Option<TableToken<'_>> {
134        self.begin_table_with_sizing(str_id, column_count, flags, [0.0, 0.0], 0.0)
135    }
136
137    /// Begins a table with all flags and sizing constraints. This is the base method,
138    /// and gives users the most flexibility.
139    #[must_use = "if return is dropped immediately, table is ended immediately."]
140    pub fn begin_table_with_sizing(
141        &self,
142        str_id: impl AsRef<str>,
143        column_count: usize,
144        flags: TableFlags,
145        outer_size: impl Into<[f32; 2]>,
146        inner_width: f32,
147    ) -> Option<TableToken<'_>> {
148        let str_id_ptr = self.scratch_txt(str_id);
149        let outer_size_vec: sys::ImVec2 = outer_size.into().into();
150
151        let should_render = unsafe {
152            sys::igBeginTable(
153                str_id_ptr,
154                column_count as i32,
155                flags.bits(),
156                outer_size_vec,
157                inner_width,
158            )
159        };
160
161        if should_render {
162            Some(TableToken::new(self))
163        } else {
164            None
165        }
166    }
167
168    /// Begins a table with no flags and with standard sizing constraints.
169    ///
170    /// Takes an array of table header information, the length of which determines
171    /// how many columns will be created.
172    #[must_use = "if return is dropped immediately, table is ended immediately."]
173    pub fn begin_table_header<Name: AsRef<str>, const N: usize>(
174        &self,
175        str_id: impl AsRef<str>,
176        column_data: [TableColumnSetup<Name>; N],
177    ) -> Option<TableToken<'_>> {
178        self.begin_table_header_with_flags(str_id, column_data, TableFlags::NONE)
179    }
180
181    /// Begins a table with flags and with standard sizing constraints.
182    ///
183    /// Takes an array of table header information, the length of which determines
184    /// how many columns will be created.
185    #[must_use = "if return is dropped immediately, table is ended immediately."]
186    pub fn begin_table_header_with_flags<Name: AsRef<str>, const N: usize>(
187        &self,
188        str_id: impl AsRef<str>,
189        column_data: [TableColumnSetup<Name>; N],
190        flags: TableFlags,
191    ) -> Option<TableToken<'_>> {
192        if let Some(token) = self.begin_table_with_flags(str_id, N, flags) {
193            // Setup columns
194            for column in &column_data {
195                self.table_setup_column(
196                    &column.name,
197                    column.flags,
198                    column.init_width_or_weight,
199                    column.user_id,
200                );
201            }
202            self.table_headers_row();
203            Some(token)
204        } else {
205            None
206        }
207    }
208
209    /// Setup a column for the current table
210    pub fn table_setup_column(
211        &self,
212        label: impl AsRef<str>,
213        flags: TableColumnFlags,
214        init_width_or_weight: f32,
215        user_id: u32,
216    ) {
217        let label_ptr = self.scratch_txt(label);
218        unsafe {
219            sys::igTableSetupColumn(label_ptr, flags.bits(), init_width_or_weight, user_id);
220        }
221    }
222
223    /// Submit all headers cells based on data provided to TableSetupColumn() + submit context menu
224    pub fn table_headers_row(&self) {
225        unsafe {
226            sys::igTableHeadersRow();
227        }
228    }
229
230    /// Append into the next column (or first column of next row if currently in last column)
231    pub fn table_next_column(&self) -> bool {
232        unsafe { sys::igTableNextColumn() }
233    }
234
235    /// Append into the specified column
236    pub fn table_set_column_index(&self, column_n: i32) -> bool {
237        unsafe { sys::igTableSetColumnIndex(column_n) }
238    }
239
240    /// Append into the next row
241    pub fn table_next_row(&self) {
242        self.table_next_row_with_flags(TableRowFlags::NONE, 0.0);
243    }
244
245    /// Append into the next row with flags and minimum height
246    pub fn table_next_row_with_flags(&self, flags: TableRowFlags, min_row_height: f32) {
247        unsafe {
248            sys::igTableNextRow(flags.bits(), min_row_height);
249        }
250    }
251
252    /// Freeze columns/rows so they stay visible when scrolling.
253    #[doc(alias = "TableSetupScrollFreeze")]
254    pub fn table_setup_scroll_freeze(&self, frozen_cols: i32, frozen_rows: i32) {
255        unsafe { sys::igTableSetupScrollFreeze(frozen_cols, frozen_rows) }
256    }
257
258    /// Submit one header cell at current column position.
259    #[doc(alias = "TableHeader")]
260    pub fn table_header(&self, label: impl AsRef<str>) {
261        let label_ptr = self.scratch_txt(label);
262        unsafe { sys::igTableHeader(label_ptr) }
263    }
264
265    /// Return columns count.
266    #[doc(alias = "TableGetColumnCount")]
267    pub fn table_get_column_count(&self) -> i32 {
268        unsafe { sys::igTableGetColumnCount() }
269    }
270
271    /// Return current column index.
272    #[doc(alias = "TableGetColumnIndex")]
273    pub fn table_get_column_index(&self) -> i32 {
274        unsafe { sys::igTableGetColumnIndex() }
275    }
276
277    /// Return current row index.
278    #[doc(alias = "TableGetRowIndex")]
279    pub fn table_get_row_index(&self) -> i32 {
280        unsafe { sys::igTableGetRowIndex() }
281    }
282
283    /// Return the name of a column by index.
284    #[doc(alias = "TableGetColumnName")]
285    pub fn table_get_column_name(&self, column_n: i32) -> &str {
286        unsafe {
287            let ptr = sys::igTableGetColumnName_Int(column_n);
288            if ptr.is_null() {
289                ""
290            } else {
291                CStr::from_ptr(ptr).to_str().unwrap_or("")
292            }
293        }
294    }
295
296    /// Return the flags of a column by index.
297    #[doc(alias = "TableGetColumnFlags")]
298    pub fn table_get_column_flags(&self, column_n: i32) -> TableColumnFlags {
299        unsafe { TableColumnFlags::from_bits_truncate(sys::igTableGetColumnFlags(column_n)) }
300    }
301
302    /// Enable/disable a column by index.
303    #[doc(alias = "TableSetColumnEnabled")]
304    pub fn table_set_column_enabled(&self, column_n: i32, enabled: bool) {
305        unsafe { sys::igTableSetColumnEnabled(column_n, enabled) }
306    }
307
308    /// Return hovered column index, or -1 when none.
309    #[doc(alias = "TableGetHoveredColumn")]
310    pub fn table_get_hovered_column(&self) -> i32 {
311        unsafe { sys::igTableGetHoveredColumn() }
312    }
313
314    /// Set column width (for fixed-width columns).
315    #[doc(alias = "TableSetColumnWidth")]
316    pub fn table_set_column_width(&self, column_n: i32, width: f32) {
317        unsafe { sys::igTableSetColumnWidth(column_n, width) }
318    }
319
320    /// Set a table background color target.
321    ///
322    /// Color must be an ImGui-packed ImU32 in ABGR order (IM_COL32).
323    /// Use `crate::colors::Color::to_imgui_u32()` to convert RGBA floats.
324    #[doc(alias = "TableSetBgColor")]
325    pub fn table_set_bg_color_u32(&self, target: TableBgTarget, color: u32, column_n: i32) {
326        unsafe { sys::igTableSetBgColor(target as i32, color, column_n) }
327    }
328
329    /// Set a table background color target using RGBA color (0..=1 floats).
330    pub fn table_set_bg_color(&self, target: TableBgTarget, rgba: [f32; 4], column_n: i32) {
331        // Pack to ImGui's ABGR layout.
332        let col = crate::colors::Color::from_array(rgba).to_imgui_u32();
333        unsafe { sys::igTableSetBgColor(target as i32, col, column_n) }
334    }
335
336    /// Return hovered row index, or -1 when none.
337    #[doc(alias = "TableGetHoveredRow")]
338    pub fn table_get_hovered_row(&self) -> i32 {
339        unsafe { sys::igTableGetHoveredRow() }
340    }
341
342    /// Header row height in pixels.
343    #[doc(alias = "TableGetHeaderRowHeight")]
344    pub fn table_get_header_row_height(&self) -> f32 {
345        unsafe { sys::igTableGetHeaderRowHeight() }
346    }
347
348    /// Set sort direction for a column. Optionally append to existing sort specs (multi-sort).
349    #[doc(alias = "TableSetColumnSortDirection")]
350    pub fn table_set_column_sort_direction(
351        &self,
352        column_n: i32,
353        dir: SortDirection,
354        append_to_sort_specs: bool,
355    ) {
356        unsafe { sys::igTableSetColumnSortDirection(column_n, dir.into(), append_to_sort_specs) }
357    }
358
359    /// Get current table sort specifications, if any.
360    /// When non-None and `is_dirty()` is true, the application should sort its data and
361    /// then call `clear_dirty()`.
362    #[doc(alias = "TableGetSortSpecs")]
363    pub fn table_get_sort_specs(&self) -> Option<TableSortSpecs<'_>> {
364        unsafe {
365            let ptr = sys::igTableGetSortSpecs();
366            if ptr.is_null() {
367                None
368            } else {
369                Some(TableSortSpecs::from_raw(ptr))
370            }
371        }
372    }
373}
374
375bitflags::bitflags! {
376    /// Flags for table rows
377    #[repr(transparent)]
378    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
379    pub struct TableRowFlags: i32 {
380        /// No flags
381        const NONE = 0;
382        /// Identify header row (set default background color + width of all columns)
383        const HEADERS = sys::ImGuiTableRowFlags_Headers as i32;
384    }
385}
386
387#[cfg(feature = "serde")]
388impl Serialize for TableRowFlags {
389    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
390    where
391        S: serde::Serializer,
392    {
393        serializer.serialize_i32(self.bits())
394    }
395}
396
397#[cfg(feature = "serde")]
398impl<'de> Deserialize<'de> for TableRowFlags {
399    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
400    where
401        D: serde::Deserializer<'de>,
402    {
403        let bits = i32::deserialize(deserializer)?;
404        Ok(TableRowFlags::from_bits_truncate(bits))
405    }
406}
407
408/// Target for table background colors.
409#[repr(i32)]
410#[derive(Copy, Clone, Debug, PartialEq, Eq)]
411#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
412pub enum TableBgTarget {
413    /// No background target
414    None = sys::ImGuiTableBgTarget_None as i32,
415    /// First alternating row background
416    RowBg0 = sys::ImGuiTableBgTarget_RowBg0 as i32,
417    /// Second alternating row background
418    RowBg1 = sys::ImGuiTableBgTarget_RowBg1 as i32,
419    /// Cell background
420    CellBg = sys::ImGuiTableBgTarget_CellBg as i32,
421}
422
423/// Sorting direction for table columns.
424#[repr(u8)]
425#[derive(Copy, Clone, Debug, PartialEq, Eq)]
426#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
427pub enum SortDirection {
428    None = sys::ImGuiSortDirection_None as u8,
429    Ascending = sys::ImGuiSortDirection_Ascending as u8,
430    Descending = sys::ImGuiSortDirection_Descending as u8,
431}
432
433impl From<SortDirection> for sys::ImGuiSortDirection {
434    #[inline]
435    fn from(value: SortDirection) -> sys::ImGuiSortDirection {
436        match value {
437            SortDirection::None => sys::ImGuiSortDirection_None,
438            SortDirection::Ascending => sys::ImGuiSortDirection_Ascending,
439            SortDirection::Descending => sys::ImGuiSortDirection_Descending,
440        }
441    }
442}
443
444/// One column sort spec.
445#[derive(Copy, Clone, Debug)]
446pub struct TableColumnSortSpec {
447    pub column_user_id: u32,
448    pub column_index: i16,
449    pub sort_order: i16,
450    pub sort_direction: SortDirection,
451}
452
453/// Table sort specs view.
454pub struct TableSortSpecs<'a> {
455    raw: *mut sys::ImGuiTableSortSpecs,
456    _marker: std::marker::PhantomData<&'a Ui>,
457}
458
459impl<'a> TableSortSpecs<'a> {
460    /// # Safety
461    /// `raw` must be a valid pointer returned by ImGui_TableGetSortSpecs for the current table.
462    pub(crate) unsafe fn from_raw(raw: *mut sys::ImGuiTableSortSpecs) -> Self {
463        Self {
464            raw,
465            _marker: std::marker::PhantomData,
466        }
467    }
468
469    /// Whether the specs are marked dirty by dear imgui (you should resort your data).
470    pub fn is_dirty(&self) -> bool {
471        unsafe { (*self.raw).SpecsDirty }
472    }
473
474    /// Clear the dirty flag after you've applied sorting to your data.
475    pub fn clear_dirty(&mut self) {
476        unsafe { (*self.raw).SpecsDirty = false }
477    }
478
479    /// Number of column specs.
480    pub fn len(&self) -> usize {
481        unsafe { (*self.raw).SpecsCount as usize }
482    }
483
484    pub fn is_empty(&self) -> bool {
485        self.len() == 0
486    }
487
488    /// Iterate over column sort specs.
489    pub fn iter(&self) -> TableSortSpecsIter<'_> {
490        TableSortSpecsIter {
491            specs: self,
492            index: 0,
493        }
494    }
495}
496
497/// Iterator over [`TableColumnSortSpec`].
498pub struct TableSortSpecsIter<'a> {
499    specs: &'a TableSortSpecs<'a>,
500    index: usize,
501}
502
503impl<'a> Iterator for TableSortSpecsIter<'a> {
504    type Item = TableColumnSortSpec;
505    fn next(&mut self) -> Option<Self::Item> {
506        if self.index >= self.specs.len() {
507            return None;
508        }
509        unsafe {
510            let ptr = (*self.specs.raw).Specs;
511            if ptr.is_null() {
512                return None;
513            }
514            let spec = &*ptr.add(self.index);
515            self.index += 1;
516            let d = spec.SortDirection as u8;
517            let dir = if d == sys::ImGuiSortDirection_None as u8 {
518                SortDirection::None
519            } else if d == sys::ImGuiSortDirection_Ascending as u8 {
520                SortDirection::Ascending
521            } else if d == sys::ImGuiSortDirection_Descending as u8 {
522                SortDirection::Descending
523            } else {
524                SortDirection::None
525            };
526            Some(TableColumnSortSpec {
527                column_user_id: spec.ColumnUserID,
528                column_index: spec.ColumnIndex,
529                sort_order: spec.SortOrder,
530                sort_direction: dir,
531            })
532        }
533    }
534}
535
536/// Tracks a table that can be ended by calling `.end()` or by dropping
537#[must_use]
538pub struct TableToken<'ui> {
539    ui: &'ui Ui,
540}
541
542impl<'ui> TableToken<'ui> {
543    /// Creates a new table token
544    fn new(ui: &'ui Ui) -> Self {
545        TableToken { ui }
546    }
547
548    /// Ends the table
549    pub fn end(self) {
550        // The drop implementation will handle the actual ending
551    }
552}
553
554impl<'ui> Drop for TableToken<'ui> {
555    fn drop(&mut self) {
556        unsafe {
557            sys::igEndTable();
558        }
559    }
560}
561
562// ============================================================================
563// Additional table convenience APIs
564// ============================================================================
565
566/// Safe description of a single angled header cell.
567#[derive(Copy, Clone, Debug, PartialEq)]
568pub struct TableHeaderData {
569    pub index: i16,
570    pub text_color: ImColor32,
571    pub bg_color0: ImColor32,
572    pub bg_color1: ImColor32,
573}
574
575impl TableHeaderData {
576    pub fn new(
577        index: i16,
578        text_color: ImColor32,
579        bg_color0: ImColor32,
580        bg_color1: ImColor32,
581    ) -> Self {
582        Self {
583            index,
584            text_color,
585            bg_color0,
586            bg_color1,
587        }
588    }
589}
590impl Ui {
591    /// Maximum label width used for angled headers (when enabled in style/options).
592    #[doc(alias = "TableGetHeaderAngledMaxLabelWidth")]
593    pub fn table_get_header_angled_max_label_width(&self) -> f32 {
594        unsafe { sys::igTableGetHeaderAngledMaxLabelWidth() }
595    }
596
597    /// Submit angled headers row (requires style/flags enabling angled headers).
598    #[doc(alias = "TableAngledHeadersRow")]
599    pub fn table_angled_headers_row(&self) {
600        unsafe { sys::igTableAngledHeadersRow() }
601    }
602
603    // Removed legacy TableAngledHeadersRowEx(flags) wrapper; use `table_angled_headers_row_ex_with_data`.
604
605    /// Submit angled headers row with explicit data (Ex variant).
606    ///
607    /// - `row_id`: ImGuiID for the row. Use 0 for automatic if not needed.
608    /// - `angle`: Angle in radians for headers.
609    /// - `max_label_width`: Maximum label width for angled headers.
610    /// - `headers`: Per-column header data.
611    pub fn table_angled_headers_row_ex_with_data(
612        &self,
613        row_id: u32,
614        angle: f32,
615        max_label_width: f32,
616        headers: &[TableHeaderData],
617    ) {
618        if headers.is_empty() {
619            unsafe { sys::igTableAngledHeadersRow() }
620            return;
621        }
622        let mut data: Vec<sys::ImGuiTableHeaderData> = Vec::with_capacity(headers.len());
623        for h in headers {
624            data.push(sys::ImGuiTableHeaderData {
625                Index: h.index as sys::ImGuiTableColumnIdx,
626                TextColor: u32::from(h.text_color),
627                BgColor0: u32::from(h.bg_color0),
628                BgColor1: u32::from(h.bg_color1),
629            });
630        }
631        unsafe {
632            sys::igTableAngledHeadersRowEx(
633                row_id,
634                angle,
635                max_label_width,
636                data.as_ptr(),
637                data.len() as i32,
638            );
639        }
640    }
641
642    /// Push background draw channel for the current table and return a token to pop it.
643    #[doc(alias = "TablePushBackgroundChannel")]
644    pub fn table_push_background_channel(&self) {
645        unsafe { sys::igTablePushBackgroundChannel() }
646    }
647
648    /// Pop background draw channel for the current table.
649    #[doc(alias = "TablePopBackgroundChannel")]
650    pub fn table_pop_background_channel(&self) {
651        unsafe { sys::igTablePopBackgroundChannel() }
652    }
653
654    /// Push column draw channel for the given column index and return a token to pop it.
655    #[doc(alias = "TablePushColumnChannel")]
656    pub fn table_push_column_channel(&self, column_n: i32) {
657        unsafe { sys::igTablePushColumnChannel(column_n) }
658    }
659
660    /// Pop column draw channel.
661    #[doc(alias = "TablePopColumnChannel")]
662    pub fn table_pop_column_channel(&self) {
663        unsafe { sys::igTablePopColumnChannel() }
664    }
665
666    /// Run a closure after pushing table background channel (auto-pop on return).
667    pub fn with_table_background_channel<R>(&self, f: impl FnOnce() -> R) -> R {
668        self.table_push_background_channel();
669        let result = f();
670        self.table_pop_background_channel();
671        result
672    }
673
674    /// Run a closure after pushing a table column channel (auto-pop on return).
675    pub fn with_table_column_channel<R>(&self, column_n: i32, f: impl FnOnce() -> R) -> R {
676        self.table_push_column_channel(column_n);
677        let result = f();
678        self.table_pop_column_channel();
679        result
680    }
681
682    /// Open the table context menu for a given column (use -1 for current/default).
683    #[doc(alias = "TableOpenContextMenu")]
684    pub fn table_open_context_menu(&self, column_n: Option<i32>) {
685        unsafe { sys::igTableOpenContextMenu(column_n.unwrap_or(-1)) }
686    }
687}
688
689// (Optional) RAII versions could be added later if desirable
690
691// ============================================================================
692// TableBuilder: ergonomic table construction
693// ============================================================================
694
695/// Builder for ImGui tables with columns + headers + sizing/freeze options.
696#[derive(Debug)]
697pub struct TableBuilder<'ui> {
698    ui: &'ui Ui,
699    id: String,
700    flags: TableFlags,
701    outer_size: [f32; 2],
702    inner_width: f32,
703    columns: Vec<TableColumnSetup<String>>, // owned names
704    use_headers: bool,
705    freeze: Option<(i32, i32)>,
706}
707
708impl<'ui> TableBuilder<'ui> {
709    /// Create a new TableBuilder. Prefer using `Ui::table("id")`.
710    pub fn new(ui: &'ui Ui, str_id: impl AsRef<str>) -> Self {
711        Self {
712            ui,
713            id: str_id.as_ref().to_string(),
714            flags: TableFlags::NONE,
715            outer_size: [0.0, 0.0],
716            inner_width: 0.0,
717            columns: Vec::new(),
718            use_headers: false,
719            freeze: None,
720        }
721    }
722
723    /// Set table flags
724    pub fn flags(mut self, flags: TableFlags) -> Self {
725        self.flags = flags;
726        self
727    }
728
729    /// Set outer size (width, height). Default [0,0]
730    pub fn outer_size(mut self, size: [f32; 2]) -> Self {
731        self.outer_size = size;
732        self
733    }
734
735    /// Set inner width. Default 0.0
736    pub fn inner_width(mut self, width: f32) -> Self {
737        self.inner_width = width;
738        self
739    }
740
741    /// Freeze columns/rows so they stay visible when scrolling
742    pub fn freeze(mut self, frozen_cols: i32, frozen_rows: i32) -> Self {
743        self.freeze = Some((frozen_cols, frozen_rows));
744        self
745    }
746
747    /// Begin defining a column using a chainable ColumnBuilder.
748    /// Call `.done()` to return to the TableBuilder.
749    pub fn column(self, name: impl AsRef<str>) -> ColumnBuilder<'ui> {
750        ColumnBuilder::new(self, name)
751    }
752
753    /// Replace columns with provided list
754    pub fn columns<Name: AsRef<str>>(
755        mut self,
756        cols: impl IntoIterator<Item = TableColumnSetup<Name>>,
757    ) -> Self {
758        self.columns.clear();
759        for c in cols {
760            self.columns.push(TableColumnSetup {
761                name: c.name.as_ref().to_string(),
762                flags: c.flags,
763                init_width_or_weight: c.init_width_or_weight,
764                user_id: c.user_id,
765            });
766        }
767        self
768    }
769
770    /// Add a single column setup
771    pub fn add_column<Name: AsRef<str>>(mut self, col: TableColumnSetup<Name>) -> Self {
772        self.columns.push(TableColumnSetup {
773            name: col.name.as_ref().to_string(),
774            flags: col.flags,
775            init_width_or_weight: col.init_width_or_weight,
776            user_id: col.user_id,
777        });
778        self
779    }
780
781    /// Auto submit headers row from `TableSetupColumn()` entries
782    pub fn headers(mut self, enabled: bool) -> Self {
783        self.use_headers = enabled;
784        self
785    }
786
787    /// Build the table and run a closure to emit rows/cells
788    pub fn build(self, f: impl FnOnce(&Ui)) {
789        let Some(token) = self.ui.begin_table_with_sizing(
790            &self.id,
791            self.columns.len(),
792            self.flags,
793            self.outer_size,
794            self.inner_width,
795        ) else {
796            return;
797        };
798
799        if let Some((fc, fr)) = self.freeze {
800            self.ui.table_setup_scroll_freeze(fc, fr);
801        }
802
803        if !self.columns.is_empty() {
804            for col in &self.columns {
805                self.ui.table_setup_column(
806                    &col.name,
807                    col.flags,
808                    col.init_width_or_weight,
809                    col.user_id,
810                );
811            }
812            if self.use_headers {
813                self.ui.table_headers_row();
814            }
815        }
816
817        f(self.ui);
818
819        // drop token to end table
820        token.end();
821    }
822}
823
824/// Chainable builder for a single column. Use `.done()` to return to the table builder.
825#[derive(Debug)]
826pub struct ColumnBuilder<'ui> {
827    parent: TableBuilder<'ui>,
828    name: String,
829    flags: TableColumnFlags,
830    init_width_or_weight: f32,
831    user_id: u32,
832}
833
834impl<'ui> ColumnBuilder<'ui> {
835    fn new(parent: TableBuilder<'ui>, name: impl AsRef<str>) -> Self {
836        Self {
837            parent,
838            name: name.as_ref().to_string(),
839            flags: TableColumnFlags::NONE,
840            init_width_or_weight: 0.0,
841            user_id: 0,
842        }
843    }
844
845    /// Set column flags.
846    pub fn flags(mut self, flags: TableColumnFlags) -> Self {
847        self.flags = flags;
848        self
849    }
850
851    /// Set fixed width or stretch weight (ImGui uses same field for both).
852    pub fn width(mut self, width: f32) -> Self {
853        self.init_width_or_weight = width;
854        self
855    }
856
857    /// Alias of `width()` to express stretch weights.
858    pub fn weight(self, weight: f32) -> Self {
859        self.width(weight)
860    }
861
862    /// Toggle angled header flag.
863    pub fn angled_header(mut self, enabled: bool) -> Self {
864        if enabled {
865            self.flags.insert(TableColumnFlags::ANGLED_HEADER);
866        } else {
867            self.flags.remove(TableColumnFlags::ANGLED_HEADER);
868        }
869        self
870    }
871
872    /// Set user id for this column.
873    pub fn user_id(mut self, id: u32) -> Self {
874        self.user_id = id;
875        self
876    }
877
878    /// Finish this column and return to the table builder.
879    pub fn done(mut self) -> TableBuilder<'ui> {
880        self.parent.columns.push(TableColumnSetup {
881            name: self.name,
882            flags: self.flags,
883            init_width_or_weight: self.init_width_or_weight,
884            user_id: self.user_id,
885        });
886        self.parent
887    }
888}