#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::as_conversions
)]
use crate::draw::ImColor32;
use crate::internal::len_i32;
use crate::sys;
use crate::ui::Ui;
use crate::widget::{
TableColumnFlags, TableColumnIndent, TableColumnStateFlags, TableColumnWidth, TableFlags,
TableOptions, TableSizingPolicy,
};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::ffi::CStr;
const TABLE_MAX_COLUMNS: usize = 512;
fn table_column_count_to_i32(column_count: usize) -> i32 {
assert!(
column_count > 0,
"table column_count must be greater than zero"
);
assert!(
column_count < TABLE_MAX_COLUMNS,
"table column_count must be less than {TABLE_MAX_COLUMNS}"
);
i32::try_from(column_count).expect("table column_count exceeded ImGui's i32 range")
}
fn current_table() -> *mut sys::ImGuiTable {
unsafe { sys::igGetCurrentTable() }
}
fn current_table_if_any() -> Option<*mut sys::ImGuiTable> {
let table = current_table();
(!table.is_null()).then_some(table)
}
fn assert_current_table(caller: &str) -> *mut sys::ImGuiTable {
let table = current_table();
assert!(
!table.is_null(),
"{caller} must be called inside a BeginTable/EndTable scope"
);
table
}
fn assert_valid_table_column_in(table: *mut sys::ImGuiTable, column_n: i32, caller: &str) {
let column_count = unsafe { (*table).ColumnsCount };
assert!(
(0..column_count).contains(&column_n),
"{caller} column index {column_n} is outside the current table column range 0..{column_count}"
);
}
fn assert_valid_table_column(column_n: i32, caller: &str) {
let table = assert_current_table(caller);
assert_valid_table_column_in(table, column_n, caller);
}
fn assert_non_negative_finite_f32(caller: &str, name: &str, value: f32) {
assert!(value.is_finite(), "{caller} {name} must be finite");
assert!(value >= 0.0, "{caller} {name} must be non-negative");
}
fn resolve_table_column(column_n: i32, caller: &str) -> i32 {
let table = assert_current_table(caller);
let column_n = if column_n < 0 {
unsafe { (*table).CurrentColumn }
} else {
column_n
};
assert_valid_table_column_in(table, column_n, caller);
column_n
}
fn assert_current_table_has_flags(flags: TableFlags, caller: &str) {
let table = assert_current_table(caller);
let table_flags = TableFlags::from_bits_truncate(unsafe { (*table).Flags });
assert!(
table_flags.contains(flags),
"{caller} requires the current table to have {flags:?}"
);
}
fn assert_table_setup_phase(caller: &str) {
let table = assert_current_table(caller);
assert!(
!unsafe { (*table).IsLayoutLocked },
"{caller} must be called before the first table row or column"
);
}
fn assert_table_column_width_phase(caller: &str) {
let table = assert_current_table(caller);
assert!(
!unsafe { (*table).IsLayoutLocked },
"{caller} must be called before the table layout is locked"
);
assert!(
unsafe { (*table).MinColumnWidth > 0.0 },
"{caller} requires Dear ImGui table layout metrics to be initialized"
);
}
fn assert_current_table_cell(caller: &str) {
let table = assert_current_table(caller);
let (current_column, column_count) = unsafe { ((*table).CurrentColumn, (*table).ColumnsCount) };
assert!(
(0..column_count).contains(¤t_column),
"{caller} must be called while a table cell is current"
);
}
fn assert_current_table_row(caller: &str) {
let table = assert_current_table(caller);
assert!(
unsafe { (*table).CurrentRow } >= 0,
"{caller} must be called while a table row is current"
);
}
#[derive(Clone, Debug)]
pub struct TableColumnSetup<Name> {
pub name: Name,
pub flags: TableColumnFlags,
pub width: Option<TableColumnWidth>,
pub indent: Option<TableColumnIndent>,
pub user_id: u32,
}
impl<Name> TableColumnSetup<Name> {
pub fn new(name: Name) -> Self {
Self {
name,
flags: TableColumnFlags::NONE,
width: None,
indent: None,
user_id: 0,
}
}
pub fn flags(mut self, flags: TableColumnFlags) -> Self {
self.flags = flags;
self
}
pub fn fixed_width(mut self, width: f32) -> Self {
self.width = Some(TableColumnWidth::Fixed(width));
self
}
pub fn stretch_weight(mut self, weight: f32) -> Self {
self.width = Some(TableColumnWidth::Stretch(weight));
self
}
pub fn indent(mut self, indent: TableColumnIndent) -> Self {
self.indent = Some(indent);
self
}
pub fn indent_enabled(mut self, enabled: bool) -> Self {
self.indent = Some(if enabled {
TableColumnIndent::Enable
} else {
TableColumnIndent::Disable
});
self
}
pub fn user_id(mut self, id: u32) -> Self {
self.user_id = id;
self
}
}
impl Ui {
pub fn table<'ui>(&'ui self, str_id: impl Into<Cow<'ui, str>>) -> TableBuilder<'ui> {
TableBuilder::new(self, str_id)
}
#[must_use = "if return is dropped immediately, table is ended immediately."]
pub fn begin_table(
&self,
str_id: impl AsRef<str>,
column_count: usize,
) -> Option<TableToken<'_>> {
self.begin_table_with_flags(str_id, column_count, TableFlags::NONE)
}
#[must_use = "if return is dropped immediately, table is ended immediately."]
pub fn begin_table_with_flags(
&self,
str_id: impl AsRef<str>,
column_count: usize,
flags: impl Into<TableOptions>,
) -> Option<TableToken<'_>> {
self.begin_table_with_sizing(str_id, column_count, flags, [0.0, 0.0], 0.0)
}
#[must_use = "if return is dropped immediately, table is ended immediately."]
pub fn begin_table_with_sizing(
&self,
str_id: impl AsRef<str>,
column_count: usize,
flags: impl Into<TableOptions>,
outer_size: impl Into<[f32; 2]>,
inner_width: f32,
) -> Option<TableToken<'_>> {
let options = flags.into();
options.validate("Ui::begin_table_with_sizing()");
assert!(
inner_width.is_finite(),
"Ui::begin_table_with_sizing() inner_width must be finite"
);
assert!(
!options.flags.contains(TableFlags::SCROLL_X) || inner_width >= 0.0,
"Ui::begin_table_with_sizing() inner_width must be non-negative when SCROLL_X is enabled"
);
let outer_size = outer_size.into();
assert!(
outer_size[0].is_finite() && outer_size[1].is_finite(),
"Ui::begin_table_with_sizing() outer_size must contain finite values"
);
let str_id_ptr = self.scratch_txt(str_id);
let outer_size_vec: sys::ImVec2 = outer_size.into();
let column_count = table_column_count_to_i32(column_count);
let should_render = unsafe {
sys::igBeginTable(
str_id_ptr,
column_count,
options.raw(),
outer_size_vec,
inner_width,
)
};
if should_render {
Some(TableToken::new(self))
} else {
None
}
}
#[must_use = "if return is dropped immediately, table is ended immediately."]
pub fn begin_table_header<Name: AsRef<str>, const N: usize>(
&self,
str_id: impl AsRef<str>,
column_data: [TableColumnSetup<Name>; N],
) -> Option<TableToken<'_>> {
self.begin_table_header_with_flags(str_id, column_data, TableFlags::NONE)
}
#[must_use = "if return is dropped immediately, table is ended immediately."]
pub fn begin_table_header_with_flags<Name: AsRef<str>, const N: usize>(
&self,
str_id: impl AsRef<str>,
column_data: [TableColumnSetup<Name>; N],
flags: impl Into<TableOptions>,
) -> Option<TableToken<'_>> {
if let Some(token) = self.begin_table_with_flags(str_id, N, flags) {
for column in &column_data {
self.table_setup_column_with_indent(
&column.name,
column.flags,
column.width,
column.indent,
column.user_id,
);
}
self.table_headers_row();
Some(token)
} else {
None
}
}
pub fn table_setup_column(
&self,
label: impl AsRef<str>,
flags: TableColumnFlags,
width: Option<TableColumnWidth>,
user_id: u32,
) {
self.table_setup_column_with_indent(label, flags, width, None, user_id);
}
pub fn table_setup_column_with_indent(
&self,
label: impl AsRef<str>,
flags: TableColumnFlags,
width: Option<TableColumnWidth>,
indent: Option<TableColumnIndent>,
user_id: u32,
) {
let table = assert_current_table("Ui::table_setup_column_with_indent()");
assert!(
unsafe { i32::from((*table).DeclColumnsCount) < (*table).ColumnsCount },
"Ui::table_setup_column_with_indent() called more times than the table column count"
);
assert_table_setup_phase("Ui::table_setup_column_with_indent()");
flags.validate_for_setup("Ui::table_setup_column_with_indent()", width, indent);
let init_width_or_weight = width.map_or(0.0, TableColumnWidth::value);
assert!(
init_width_or_weight.is_finite(),
"Ui::table_setup_column_with_indent() width or weight must be finite"
);
let label_ptr = self.scratch_txt(label);
let raw_flags = flags.bits()
| width.map_or(0, TableColumnWidth::raw_flags)
| indent.map_or(0, TableColumnIndent::raw_flags);
unsafe {
sys::igTableSetupColumn(label_ptr, raw_flags, init_width_or_weight, user_id);
}
}
pub fn table_setup_column_fixed_width(
&self,
label: impl AsRef<str>,
flags: TableColumnFlags,
width: f32,
user_id: u32,
) {
self.table_setup_column(label, flags, Some(TableColumnWidth::Fixed(width)), user_id);
}
pub fn table_setup_column_stretch_weight(
&self,
label: impl AsRef<str>,
flags: TableColumnFlags,
weight: f32,
user_id: u32,
) {
self.table_setup_column(
label,
flags,
Some(TableColumnWidth::Stretch(weight)),
user_id,
);
}
pub fn table_headers_row(&self) {
assert_current_table("Ui::table_headers_row()");
unsafe {
sys::igTableHeadersRow();
}
}
pub fn table_next_column(&self) -> bool {
unsafe { sys::igTableNextColumn() }
}
pub fn table_set_column_index(&self, column_n: i32) -> bool {
if let Some(table) = current_table_if_any() {
assert_valid_table_column_in(table, column_n, "Ui::table_set_column_index()");
}
unsafe { sys::igTableSetColumnIndex(column_n) }
}
pub fn table_next_row(&self) {
self.table_next_row_with_flags(TableRowFlags::NONE, 0.0);
}
pub fn table_next_row_with_flags(&self, flags: TableRowFlags, min_row_height: f32) {
unsafe {
sys::igTableNextRow(flags.bits(), min_row_height);
}
}
#[doc(alias = "TableSetupScrollFreeze")]
pub fn table_setup_scroll_freeze(&self, frozen_cols: i32, frozen_rows: i32) {
assert_table_setup_phase("Ui::table_setup_scroll_freeze()");
assert!(
(0..TABLE_MAX_COLUMNS as i32).contains(&frozen_cols),
"Ui::table_setup_scroll_freeze() frozen_cols must be in 0..{TABLE_MAX_COLUMNS}"
);
assert!(
(0..128).contains(&frozen_rows),
"Ui::table_setup_scroll_freeze() frozen_rows must be in 0..128"
);
unsafe { sys::igTableSetupScrollFreeze(frozen_cols, frozen_rows) }
}
#[doc(alias = "TableHeader")]
pub fn table_header(&self, label: impl AsRef<str>) {
assert_current_table_cell("Ui::table_header()");
let label_ptr = self.scratch_txt(label);
unsafe { sys::igTableHeader(label_ptr) }
}
#[doc(alias = "TableGetColumnCount")]
pub fn table_get_column_count(&self) -> i32 {
unsafe { sys::igTableGetColumnCount() }
}
#[doc(alias = "TableGetColumnIndex")]
pub fn table_get_column_index(&self) -> i32 {
unsafe { sys::igTableGetColumnIndex() }
}
#[doc(alias = "TableGetRowIndex")]
pub fn table_get_row_index(&self) -> i32 {
unsafe { sys::igTableGetRowIndex() }
}
#[doc(alias = "TableGetColumnName")]
pub fn table_get_column_name(&self, column_n: i32) -> &str {
if current_table_if_any().is_some() {
resolve_table_column(column_n, "Ui::table_get_column_name()");
}
unsafe {
let ptr = sys::igTableGetColumnName_Int(column_n);
if ptr.is_null() {
""
} else {
CStr::from_ptr(ptr).to_str().unwrap_or("")
}
}
}
#[doc(alias = "TableGetColumnFlags")]
pub fn table_get_column_flags(&self, column_n: i32) -> TableColumnStateFlags {
if let Some(table) = current_table_if_any() {
let column_count = unsafe { (*table).ColumnsCount };
let resolved_column = if column_n < 0 {
unsafe { (*table).CurrentColumn }
} else {
column_n
};
assert!(
(0..=column_count).contains(&resolved_column),
"Ui::table_get_column_flags() column index {column_n} is outside the allowed range -1/current or 0..={column_count}"
);
}
unsafe { TableColumnStateFlags::from_bits_truncate(sys::igTableGetColumnFlags(column_n)) }
}
#[doc(alias = "TableSetColumnEnabled")]
pub fn table_set_column_enabled(&self, column_n: i32, enabled: bool) {
assert_current_table_has_flags(TableFlags::HIDEABLE, "Ui::table_set_column_enabled()");
resolve_table_column(column_n, "Ui::table_set_column_enabled()");
unsafe { sys::igTableSetColumnEnabled(column_n, enabled) }
}
#[doc(alias = "TableGetHoveredColumn")]
pub fn table_get_hovered_column(&self) -> i32 {
unsafe { sys::igTableGetHoveredColumn() }
}
#[doc(alias = "TableSetColumnWidth")]
pub fn table_set_column_width(&self, column_n: i32, width: f32) {
assert_table_column_width_phase("Ui::table_set_column_width()");
assert_valid_table_column(column_n, "Ui::table_set_column_width()");
assert_non_negative_finite_f32("Ui::table_set_column_width()", "width", width);
unsafe { sys::igTableSetColumnWidth(column_n, width) }
}
#[doc(alias = "TableSetBgColor")]
pub fn table_set_bg_color_u32(&self, target: TableBgTarget, color: u32, column_n: i32) {
assert_current_table_row("Ui::table_set_bg_color_u32()");
match target {
TableBgTarget::None => panic!("Ui::table_set_bg_color_u32() target cannot be None"),
TableBgTarget::CellBg => {
resolve_table_column(column_n, "Ui::table_set_bg_color_u32()");
}
TableBgTarget::RowBg0 | TableBgTarget::RowBg1 => {
assert!(
column_n == -1,
"Ui::table_set_bg_color_u32() row background targets require column_n == -1"
);
}
}
unsafe { sys::igTableSetBgColor(target as i32, color, column_n) }
}
pub fn table_set_bg_color(&self, target: TableBgTarget, rgba: [f32; 4], column_n: i32) {
let col = crate::colors::Color::from_array(rgba).to_imgui_u32();
self.table_set_bg_color_u32(target, col, column_n);
}
#[doc(alias = "TableGetHoveredRow")]
pub fn table_get_hovered_row(&self) -> i32 {
unsafe { sys::igTableGetHoveredRow() }
}
#[doc(alias = "TableGetHeaderRowHeight")]
pub fn table_get_header_row_height(&self) -> f32 {
unsafe { sys::igTableGetHeaderRowHeight() }
}
#[doc(alias = "TableSetColumnSortDirection")]
pub fn table_set_column_sort_direction(
&self,
column_n: i32,
dir: SortDirection,
append_to_sort_specs: bool,
) {
assert_valid_table_column(column_n, "Ui::table_set_column_sort_direction()");
unsafe { sys::igTableSetColumnSortDirection(column_n, dir.into(), append_to_sort_specs) }
}
#[doc(alias = "TableGetSortSpecs")]
pub fn table_get_sort_specs(&self) -> Option<TableSortSpecs<'_>> {
unsafe {
let ptr = sys::igTableGetSortSpecs();
if ptr.is_null() {
None
} else {
Some(TableSortSpecs::from_raw(ptr))
}
}
}
}
bitflags::bitflags! {
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct TableRowFlags: i32 {
const NONE = 0;
const HEADERS = sys::ImGuiTableRowFlags_Headers as i32;
}
}
#[cfg(feature = "serde")]
impl Serialize for TableRowFlags {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_i32(self.bits())
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for TableRowFlags {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bits = i32::deserialize(deserializer)?;
Ok(TableRowFlags::from_bits_truncate(bits))
}
}
#[repr(i32)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum TableBgTarget {
None = sys::ImGuiTableBgTarget_None as i32,
RowBg0 = sys::ImGuiTableBgTarget_RowBg0 as i32,
RowBg1 = sys::ImGuiTableBgTarget_RowBg1 as i32,
CellBg = sys::ImGuiTableBgTarget_CellBg as i32,
}
#[repr(u8)]
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum SortDirection {
None = sys::ImGuiSortDirection_None as u8,
Ascending = sys::ImGuiSortDirection_Ascending as u8,
Descending = sys::ImGuiSortDirection_Descending as u8,
}
impl From<SortDirection> for sys::ImGuiSortDirection {
#[inline]
fn from(value: SortDirection) -> sys::ImGuiSortDirection {
match value {
SortDirection::None => sys::ImGuiSortDirection_None,
SortDirection::Ascending => sys::ImGuiSortDirection_Ascending,
SortDirection::Descending => sys::ImGuiSortDirection_Descending,
}
}
}
#[derive(Copy, Clone, Debug)]
pub struct TableColumnSortSpec {
pub column_user_id: u32,
pub column_index: i16,
pub sort_order: i16,
pub sort_direction: SortDirection,
}
pub struct TableSortSpecs<'a> {
raw: *mut sys::ImGuiTableSortSpecs,
_marker: std::marker::PhantomData<&'a Ui>,
}
impl<'a> TableSortSpecs<'a> {
pub(crate) unsafe fn from_raw(raw: *mut sys::ImGuiTableSortSpecs) -> Self {
Self {
raw,
_marker: std::marker::PhantomData,
}
}
pub fn is_dirty(&self) -> bool {
unsafe { (*self.raw).SpecsDirty }
}
pub fn clear_dirty(&mut self) {
unsafe { (*self.raw).SpecsDirty = false }
}
pub fn len(&self) -> usize {
unsafe { (*self.raw).SpecsCount as usize }
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn iter(&self) -> TableSortSpecsIter<'_> {
TableSortSpecsIter {
specs: self,
index: 0,
}
}
}
pub struct TableSortSpecsIter<'a> {
specs: &'a TableSortSpecs<'a>,
index: usize,
}
impl<'a> Iterator for TableSortSpecsIter<'a> {
type Item = TableColumnSortSpec;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.specs.len() {
return None;
}
unsafe {
let ptr = (*self.specs.raw).Specs;
if ptr.is_null() {
return None;
}
let spec = &*ptr.add(self.index);
self.index += 1;
let d = spec.SortDirection as u8;
let dir = if d == sys::ImGuiSortDirection_None as u8 {
SortDirection::None
} else if d == sys::ImGuiSortDirection_Ascending as u8 {
SortDirection::Ascending
} else if d == sys::ImGuiSortDirection_Descending as u8 {
SortDirection::Descending
} else {
SortDirection::None
};
Some(TableColumnSortSpec {
column_user_id: spec.ColumnUserID,
column_index: spec.ColumnIndex,
sort_order: spec.SortOrder,
sort_direction: dir,
})
}
}
}
#[must_use]
pub struct TableToken<'ui> {
_ui: &'ui Ui,
}
impl<'ui> TableToken<'ui> {
fn new(ui: &'ui Ui) -> Self {
TableToken { _ui: ui }
}
pub fn end(self) {
}
}
impl<'ui> Drop for TableToken<'ui> {
fn drop(&mut self) {
unsafe {
sys::igEndTable();
}
}
}
#[must_use = "dropping the token pops the table background draw channel immediately"]
pub struct TableBackgroundChannelToken<'ui> {
_ui: &'ui Ui,
}
impl<'ui> TableBackgroundChannelToken<'ui> {
fn new(ui: &'ui Ui) -> Self {
Self { _ui: ui }
}
pub fn pop(self) {}
pub fn end(self) {}
}
impl Drop for TableBackgroundChannelToken<'_> {
fn drop(&mut self) {
unsafe {
sys::igTablePopBackgroundChannel();
}
}
}
#[must_use = "dropping the token pops the table column draw channel immediately"]
pub struct TableColumnChannelToken<'ui> {
_ui: &'ui Ui,
}
impl<'ui> TableColumnChannelToken<'ui> {
fn new(ui: &'ui Ui) -> Self {
Self { _ui: ui }
}
pub fn pop(self) {}
pub fn end(self) {}
}
impl Drop for TableColumnChannelToken<'_> {
fn drop(&mut self) {
unsafe {
sys::igTablePopColumnChannel();
}
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct TableHeaderData {
pub index: i16,
pub text_color: ImColor32,
pub bg_color0: ImColor32,
pub bg_color1: ImColor32,
}
impl TableHeaderData {
pub fn new(
index: i16,
text_color: ImColor32,
bg_color0: ImColor32,
bg_color1: ImColor32,
) -> Self {
Self {
index,
text_color,
bg_color0,
bg_color1,
}
}
}
impl Ui {
#[doc(alias = "TableGetHeaderAngledMaxLabelWidth")]
pub fn table_get_header_angled_max_label_width(&self) -> f32 {
unsafe { sys::igTableGetHeaderAngledMaxLabelWidth() }
}
#[doc(alias = "TableAngledHeadersRow")]
pub fn table_angled_headers_row(&self) {
unsafe { sys::igTableAngledHeadersRow() }
}
pub fn table_angled_headers_row_ex_with_data(
&self,
row_id: u32,
angle: f32,
max_label_width: f32,
headers: &[TableHeaderData],
) {
if headers.is_empty() {
unsafe { sys::igTableAngledHeadersRow() }
return;
}
let count = len_i32(
"Ui::table_angled_headers_row_ex_with_data()",
"headers",
headers.len(),
);
let table = assert_current_table("Ui::table_angled_headers_row_ex_with_data()");
let mut data: Vec<sys::ImGuiTableHeaderData> = Vec::with_capacity(headers.len());
for h in headers {
assert_valid_table_column_in(
table,
i32::from(h.index),
"Ui::table_angled_headers_row_ex_with_data()",
);
data.push(sys::ImGuiTableHeaderData {
Index: h.index as sys::ImGuiTableColumnIdx,
TextColor: u32::from(h.text_color),
BgColor0: u32::from(h.bg_color0),
BgColor1: u32::from(h.bg_color1),
});
}
unsafe {
sys::igTableAngledHeadersRowEx(row_id, angle, max_label_width, data.as_ptr(), count);
}
}
#[doc(alias = "TablePushBackgroundChannel")]
pub fn table_push_background_channel(&self) {
assert_current_table_cell("Ui::table_push_background_channel()");
unsafe { sys::igTablePushBackgroundChannel() }
}
#[doc(alias = "TablePopBackgroundChannel")]
pub fn table_pop_background_channel(&self) {
assert_current_table_cell("Ui::table_pop_background_channel()");
unsafe { sys::igTablePopBackgroundChannel() }
}
#[doc(alias = "TablePushColumnChannel")]
pub fn table_push_column_channel(&self, column_n: i32) {
assert_valid_table_column(column_n, "Ui::table_push_column_channel()");
unsafe { sys::igTablePushColumnChannel(column_n) }
}
#[doc(alias = "TablePopColumnChannel")]
pub fn table_pop_column_channel(&self) {
assert_current_table_cell("Ui::table_pop_column_channel()");
unsafe { sys::igTablePopColumnChannel() }
}
#[must_use = "dropping the token pops the table background draw channel immediately"]
#[doc(alias = "TablePushBackgroundChannel")]
pub fn table_background_channel(&self) -> TableBackgroundChannelToken<'_> {
self.table_push_background_channel();
TableBackgroundChannelToken::new(self)
}
#[must_use = "dropping the token pops the table column draw channel immediately"]
#[doc(alias = "TablePushColumnChannel")]
pub fn table_column_channel(&self, column_n: i32) -> TableColumnChannelToken<'_> {
self.table_push_column_channel(column_n);
TableColumnChannelToken::new(self)
}
pub fn with_table_background_channel<R>(&self, f: impl FnOnce() -> R) -> R {
let _token = self.table_background_channel();
f()
}
pub fn with_table_column_channel<R>(&self, column_n: i32, f: impl FnOnce() -> R) -> R {
let _token = self.table_column_channel(column_n);
f()
}
#[doc(alias = "TableOpenContextMenu")]
pub fn table_open_context_menu(&self, column_n: Option<i32>) {
unsafe { sys::igTableOpenContextMenu(column_n.unwrap_or(-1)) }
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup_context() -> crate::Context {
let mut ctx = crate::Context::create();
{
let io = ctx.io_mut();
io.set_display_size([128.0, 128.0]);
io.set_delta_time(1.0 / 60.0);
}
let _ = ctx.font_atlas_mut().build();
let _ = ctx.set_ini_filename::<std::path::PathBuf>(None);
ctx
}
unsafe fn current_table_draw_channel() -> i32 {
let table = assert_current_table("current_table_draw_channel()");
let draw_list = unsafe { (*(*table).InnerWindow).DrawList };
unsafe { (*draw_list)._Splitter._Current }
}
#[test]
fn table_column_channel_is_popped_after_panic() {
let mut ctx = setup_context();
let ui = ctx.frame();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = ui.window("table_channel_panic").build(|| {
let _table = ui.begin_table("table", 2).unwrap();
ui.table_next_row();
assert!(ui.table_set_column_index(0));
let initial_channel = unsafe { current_table_draw_channel() };
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.with_table_column_channel(1, || {
let pushed_channel = unsafe { current_table_draw_channel() };
assert_ne!(pushed_channel, initial_channel);
panic!("forced panic while table column channel is pushed");
});
}));
assert!(result.is_err());
assert_eq!(unsafe { current_table_draw_channel() }, initial_channel);
});
}));
assert!(result.is_ok());
}
#[test]
fn begin_table_rejects_invalid_column_counts_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = ui.begin_table("zero_columns", 0);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = ui.begin_table("too_many_columns", TABLE_MAX_COLUMNS);
}))
.is_err()
);
}
#[test]
fn table_column_channel_rejects_out_of_range_column_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
let _ = ui.window("table_channel_oob").build(|| {
let _table = ui.begin_table("table", 2).unwrap();
ui.table_next_row();
assert!(ui.table_set_column_index(0));
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _token = ui.table_column_channel(-1);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _token = ui.table_column_channel(2);
}))
.is_err()
);
});
}
#[test]
fn table_channels_require_current_cell_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
let _ = ui.window("table_channel_cell_required").build(|| {
let _table = ui.begin_table("table", 2).unwrap();
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _token = ui.table_background_channel();
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_pop_column_channel();
}))
.is_err()
);
});
}
#[test]
fn table_accessors_reject_invalid_columns_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
let _ = ui.window("table_accessors_oob").build(|| {
let _table = ui.begin_table("table", 2).unwrap();
ui.table_next_row();
assert!(ui.table_set_column_index(0));
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_index(2);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = ui.table_get_column_name(2);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_enabled(2, true);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_sort_direction(2, SortDirection::Ascending, false);
}))
.is_err()
);
});
}
#[test]
fn table_setup_methods_reject_late_or_excess_calls_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
let _ = ui.window("table_setup_preconditions").build(|| {
let _table = ui.begin_table("table", 1).unwrap();
ui.table_setup_column("one", TableColumnFlags::NONE, None, 0);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_setup_column("two", TableColumnFlags::NONE, None, 0);
}))
.is_err()
);
ui.table_next_row();
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_setup_scroll_freeze(1, 0);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_width(0, 32.0);
}))
.is_err()
);
});
}
#[test]
fn table_set_column_width_rejects_invalid_widths_before_ffi() {
let mut ctx = setup_context();
{
let ui = ctx.frame();
let _ = ui.window("table_width_bounds").build(|| {
let _table = ui.begin_table("table", 1).unwrap();
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_width(0, 32.0);
}))
.is_err()
);
ui.table_next_row();
});
}
ctx.render();
let ui = ctx.frame();
let _ = ui.window("table_width_bounds").build(|| {
let _table = ui.begin_table("table", 1).unwrap();
ui.table_set_column_width(0, 0.0);
ui.table_set_column_width(0, 32.0);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_width(0, -1.0);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_column_width(0, f32::NAN);
}))
.is_err()
);
});
}
#[test]
fn table_bg_color_validates_target_and_column_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
let _ = ui.window("table_bg_preconditions").build(|| {
let _table = ui.begin_table("table", 2).unwrap();
ui.table_next_row();
assert!(ui.table_set_column_index(0));
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_bg_color_u32(TableBgTarget::None, 0, -1);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_bg_color_u32(TableBgTarget::CellBg, 0, 2);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.table_set_bg_color_u32(TableBgTarget::RowBg0, 0, 0);
}))
.is_err()
);
});
}
#[test]
fn table_angled_headers_validate_indices_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
let _ = ui.window("table_angled_header_invalid").build(|| {
let _table = ui.begin_table("table", 2).unwrap();
ui.table_setup_column("one", TableColumnFlags::ANGLED_HEADER, None, 0);
ui.table_setup_column("two", TableColumnFlags::ANGLED_HEADER, None, 0);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let invalid = [TableHeaderData::new(
-1,
ImColor32::WHITE,
ImColor32::BLACK,
ImColor32::BLACK,
)];
ui.table_angled_headers_row_ex_with_data(0, 0.0, 0.0, &invalid);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let invalid = [TableHeaderData::new(
2,
ImColor32::WHITE,
ImColor32::BLACK,
ImColor32::BLACK,
)];
ui.table_angled_headers_row_ex_with_data(0, 0.0, 0.0, &invalid);
}))
.is_err()
);
});
}
}
#[derive(Debug)]
pub struct TableBuilder<'ui> {
ui: &'ui Ui,
id: Cow<'ui, str>,
flags: TableFlags,
sizing_policy: Option<TableSizingPolicy>,
outer_size: [f32; 2],
inner_width: f32,
columns: Vec<TableColumnSetup<Cow<'ui, str>>>,
use_headers: bool,
freeze: Option<(i32, i32)>,
}
impl<'ui> TableBuilder<'ui> {
pub fn new(ui: &'ui Ui, str_id: impl Into<Cow<'ui, str>>) -> Self {
Self {
ui,
id: str_id.into(),
flags: TableFlags::NONE,
sizing_policy: None,
outer_size: [0.0, 0.0],
inner_width: 0.0,
columns: Vec::new(),
use_headers: false,
freeze: None,
}
}
pub fn flags(mut self, flags: TableFlags) -> Self {
self.flags = flags;
self
}
pub fn sizing_policy(mut self, policy: TableSizingPolicy) -> Self {
self.sizing_policy = Some(policy);
self
}
pub fn outer_size(mut self, size: [f32; 2]) -> Self {
self.outer_size = size;
self
}
pub fn inner_width(mut self, width: f32) -> Self {
self.inner_width = width;
self
}
pub fn freeze(mut self, frozen_cols: i32, frozen_rows: i32) -> Self {
self.freeze = Some((frozen_cols, frozen_rows));
self
}
pub fn column(self, name: impl Into<Cow<'ui, str>>) -> ColumnBuilder<'ui> {
ColumnBuilder::new(self, name)
}
pub fn columns<Name: Into<Cow<'ui, str>>>(
mut self,
cols: impl IntoIterator<Item = TableColumnSetup<Name>>,
) -> Self {
self.columns.clear();
for c in cols {
self.columns.push(TableColumnSetup {
name: c.name.into(),
flags: c.flags,
width: c.width,
indent: c.indent,
user_id: c.user_id,
});
}
self
}
pub fn add_column<Name: Into<Cow<'ui, str>>>(mut self, col: TableColumnSetup<Name>) -> Self {
self.columns.push(TableColumnSetup {
name: col.name.into(),
flags: col.flags,
width: col.width,
indent: col.indent,
user_id: col.user_id,
});
self
}
pub fn headers(mut self, enabled: bool) -> Self {
self.use_headers = enabled;
self
}
pub fn build(self, f: impl FnOnce(&Ui)) {
let mut options = TableOptions::from(self.flags);
if let Some(policy) = self.sizing_policy {
options = options.sizing_policy(policy);
}
let Some(token) = self.ui.begin_table_with_sizing(
self.id.as_ref(),
self.columns.len(),
options,
self.outer_size,
self.inner_width,
) else {
return;
};
if let Some((fc, fr)) = self.freeze {
self.ui.table_setup_scroll_freeze(fc, fr);
}
if !self.columns.is_empty() {
for col in &self.columns {
self.ui.table_setup_column_with_indent(
col.name.as_ref(),
col.flags,
col.width,
col.indent,
col.user_id,
);
}
if self.use_headers {
self.ui.table_headers_row();
}
}
f(self.ui);
token.end();
}
}
#[derive(Debug)]
pub struct ColumnBuilder<'ui> {
parent: TableBuilder<'ui>,
name: Cow<'ui, str>,
flags: TableColumnFlags,
width: Option<TableColumnWidth>,
indent: Option<TableColumnIndent>,
user_id: u32,
}
impl<'ui> ColumnBuilder<'ui> {
fn new(parent: TableBuilder<'ui>, name: impl Into<Cow<'ui, str>>) -> Self {
Self {
parent,
name: name.into(),
flags: TableColumnFlags::NONE,
width: None,
indent: None,
user_id: 0,
}
}
pub fn flags(mut self, flags: TableColumnFlags) -> Self {
self.flags = flags;
self
}
pub fn width(mut self, width: f32) -> Self {
self.width = Some(TableColumnWidth::Fixed(width));
self
}
pub fn weight(mut self, weight: f32) -> Self {
self.width = Some(TableColumnWidth::Stretch(weight));
self
}
pub fn indent(mut self, indent: TableColumnIndent) -> Self {
self.indent = Some(indent);
self
}
pub fn indent_enabled(mut self, enabled: bool) -> Self {
self.indent = Some(if enabled {
TableColumnIndent::Enable
} else {
TableColumnIndent::Disable
});
self
}
pub fn angled_header(mut self, enabled: bool) -> Self {
if enabled {
self.flags.insert(TableColumnFlags::ANGLED_HEADER);
} else {
self.flags.remove(TableColumnFlags::ANGLED_HEADER);
}
self
}
pub fn user_id(mut self, id: u32) -> Self {
self.user_id = id;
self
}
pub fn done(mut self) -> TableBuilder<'ui> {
self.parent.columns.push(TableColumnSetup {
name: self.name,
flags: self.flags,
width: self.width,
indent: self.indent,
user_id: self.user_id,
});
self.parent
}
}