1#![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(¤t_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#[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 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 pub fn flags(mut self, flags: TableColumnFlags) -> Self {
184 self.flags = flags;
185 self
186 }
187
188 pub fn fixed_width(mut self, width: f32) -> Self {
190 self.width = Some(TableColumnWidth::Fixed(width));
191 self
192 }
193
194 pub fn stretch_weight(mut self, weight: f32) -> Self {
196 self.width = Some(TableColumnWidth::Stretch(weight));
197 self
198 }
199
200 pub fn indent(mut self, indent: TableColumnIndent) -> Self {
202 self.indent = Some(indent);
203 self
204 }
205
206 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 pub fn user_id(mut self, id: u32) -> Self {
218 self.user_id = id;
219 self
220 }
221}
222
223impl Ui {
225 pub fn table<'ui>(&'ui self, str_id: impl Into<Cow<'ui, str>>) -> TableBuilder<'ui> {
246 TableBuilder::new(self, str_id)
247 }
248 #[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 #[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 #[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 #[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 #[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 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 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 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 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 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 pub fn table_headers_row(&self) {
432 assert_current_table("Ui::table_headers_row()");
433 unsafe {
434 sys::igTableHeadersRow();
435 }
436 }
437
438 pub fn table_next_column(&self) -> bool {
440 unsafe { sys::igTableNextColumn() }
441 }
442
443 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 pub fn table_next_row(&self) {
453 self.table_next_row_with_flags(TableRowFlags::NONE, 0.0);
454 }
455
456 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 #[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 #[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 #[doc(alias = "TableGetColumnCount")]
488 pub fn table_get_column_count(&self) -> i32 {
489 unsafe { sys::igTableGetColumnCount() }
490 }
491
492 #[doc(alias = "TableGetColumnIndex")]
494 pub fn table_get_column_index(&self) -> i32 {
495 unsafe { sys::igTableGetColumnIndex() }
496 }
497
498 #[doc(alias = "TableGetRowIndex")]
500 pub fn table_get_row_index(&self) -> i32 {
501 unsafe { sys::igTableGetRowIndex() }
502 }
503
504 #[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 #[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 #[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 #[doc(alias = "TableGetHoveredColumn")]
548 pub fn table_get_hovered_column(&self) -> i32 {
549 unsafe { sys::igTableGetHoveredColumn() }
550 }
551
552 #[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 #[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 pub fn table_set_bg_color(&self, target: TableBgTarget, rgba: [f32; 4], column_n: i32) {
585 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 #[doc(alias = "TableGetHoveredRow")]
592 pub fn table_get_hovered_row(&self) -> i32 {
593 unsafe { sys::igTableGetHoveredRow() }
594 }
595
596 #[doc(alias = "TableGetHeaderRowHeight")]
598 pub fn table_get_header_row_height(&self) -> f32 {
599 unsafe { sys::igTableGetHeaderRowHeight() }
600 }
601
602 #[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 #[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 #[repr(transparent)]
633 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
634 pub struct TableRowFlags: i32 {
635 const NONE = 0;
637 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#[repr(i32)]
665#[derive(Copy, Clone, Debug, PartialEq, Eq)]
666#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
667pub enum TableBgTarget {
668 None = sys::ImGuiTableBgTarget_None as i32,
670 RowBg0 = sys::ImGuiTableBgTarget_RowBg0 as i32,
672 RowBg1 = sys::ImGuiTableBgTarget_RowBg1 as i32,
674 CellBg = sys::ImGuiTableBgTarget_CellBg as i32,
676}
677
678#[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#[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
708pub struct TableSortSpecs<'a> {
710 raw: *mut sys::ImGuiTableSortSpecs,
711 _marker: std::marker::PhantomData<&'a Ui>,
712}
713
714impl<'a> TableSortSpecs<'a> {
715 pub(crate) unsafe fn from_raw(raw: *mut sys::ImGuiTableSortSpecs) -> Self {
718 Self {
719 raw,
720 _marker: std::marker::PhantomData,
721 }
722 }
723
724 pub fn is_dirty(&self) -> bool {
726 unsafe { (*self.raw).SpecsDirty }
727 }
728
729 pub fn clear_dirty(&mut self) {
731 unsafe { (*self.raw).SpecsDirty = false }
732 }
733
734 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 pub fn iter(&self) -> TableSortSpecsIter<'_> {
745 TableSortSpecsIter {
746 specs: self,
747 index: 0,
748 }
749 }
750}
751
752pub 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#[must_use]
793pub struct TableToken<'ui> {
794 _ui: &'ui Ui,
795}
796
797impl<'ui> TableToken<'ui> {
798 fn new(ui: &'ui Ui) -> Self {
800 TableToken { _ui: ui }
801 }
802
803 pub fn end(self) {
805 }
807}
808
809impl<'ui> Drop for TableToken<'ui> {
810 fn drop(&mut self) {
811 unsafe {
812 sys::igEndTable();
813 }
814 }
815}
816
817#[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 pub fn pop(self) {}
830
831 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#[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 pub fn pop(self) {}
856
857 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#[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 #[doc(alias = "TableGetHeaderAngledMaxLabelWidth")]
900 pub fn table_get_header_angled_max_label_width(&self) -> f32 {
901 unsafe { sys::igTableGetHeaderAngledMaxLabelWidth() }
902 }
903
904 #[doc(alias = "TableAngledHeadersRow")]
906 pub fn table_angled_headers_row(&self) {
907 unsafe { sys::igTableAngledHeadersRow() }
908 }
909
910 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 #[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 #[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 #[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 #[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 #[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 #[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 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 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 #[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#[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#[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 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 pub fn flags(mut self, flags: TableFlags) -> Self {
1350 self.flags = flags;
1351 self
1352 }
1353
1354 pub fn sizing_policy(mut self, policy: TableSizingPolicy) -> Self {
1356 self.sizing_policy = Some(policy);
1357 self
1358 }
1359
1360 pub fn outer_size(mut self, size: [f32; 2]) -> Self {
1362 self.outer_size = size;
1363 self
1364 }
1365
1366 pub fn inner_width(mut self, width: f32) -> Self {
1368 self.inner_width = width;
1369 self
1370 }
1371
1372 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 pub fn column(self, name: impl Into<Cow<'ui, str>>) -> ColumnBuilder<'ui> {
1381 ColumnBuilder::new(self, name)
1382 }
1383
1384 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 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 pub fn headers(mut self, enabled: bool) -> Self {
1416 self.use_headers = enabled;
1417 self
1418 }
1419
1420 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 token.end();
1459 }
1460}
1461
1462#[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 pub fn flags(mut self, flags: TableColumnFlags) -> Self {
1487 self.flags = flags;
1488 self
1489 }
1490
1491 pub fn width(mut self, width: f32) -> Self {
1493 self.width = Some(TableColumnWidth::Fixed(width));
1494 self
1495 }
1496
1497 pub fn weight(mut self, weight: f32) -> Self {
1499 self.width = Some(TableColumnWidth::Stretch(weight));
1500 self
1501 }
1502
1503 pub fn indent(mut self, indent: TableColumnIndent) -> Self {
1505 self.indent = Some(indent);
1506 self
1507 }
1508
1509 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 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 pub fn user_id(mut self, id: u32) -> Self {
1531 self.user_id = id;
1532 self
1533 }
1534
1535 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}