#![allow(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::as_conversions,
clippy::unnecessary_cast
)]
use crate::Ui;
use crate::sys;
use bitflags::bitflags;
fn current_columns() -> *mut sys::ImGuiOldColumns {
unsafe {
let window = sys::igGetCurrentWindowRead();
if window.is_null() {
std::ptr::null_mut()
} else {
(*window).DC.CurrentColumns
}
}
}
fn assert_no_current_columns(caller: &str) {
assert!(
current_columns().is_null(),
"{caller} cannot be called while another legacy columns layout is active"
);
}
fn assert_current_columns(caller: &str) -> *mut sys::ImGuiOldColumns {
let columns = current_columns();
assert!(
!columns.is_null(),
"{caller} must be called inside a legacy columns layout"
);
columns
}
fn assert_columns_count(count: i32, caller: &str) {
assert!(count >= 1, "{caller} count must be at least 1");
}
fn assert_finite_f32(caller: &str, name: &str, value: f32) {
assert!(value.is_finite(), "{caller} {name} must be finite");
}
fn assert_non_negative_f32(caller: &str, name: &str, value: f32) {
assert_finite_f32(caller, name, value);
assert!(value >= 0.0, "{caller} {name} must be non-negative");
}
fn validate_old_column_flags(caller: &str, flags: OldColumnFlags) {
let unsupported = flags.bits() & !OldColumnFlags::all().bits();
assert!(
unsupported == 0,
"{caller} received unsupported ImGuiOldColumnFlags bits: 0x{unsupported:X}"
);
}
fn resolve_column_index(column_index: i32, allow_trailing_offset: bool, caller: &str) -> i32 {
let columns = assert_current_columns(caller);
let column_index = if column_index < 0 {
unsafe { (*columns).Current }
} else {
column_index
};
let upper_bound = unsafe {
if allow_trailing_offset {
(*columns).Count
} else {
(*columns).Count - 1
}
};
assert!(
(0..=upper_bound).contains(&column_index),
"{caller} column index {column_index} is outside the allowed range 0..={upper_bound}"
);
column_index
}
bitflags! {
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct OldColumnFlags: i32 {
const NONE = sys::ImGuiOldColumnFlags_None as i32;
const NO_BORDER = sys::ImGuiOldColumnFlags_NoBorder as i32;
const NO_RESIZE = sys::ImGuiOldColumnFlags_NoResize as i32;
const NO_PRESERVE_WIDTHS = sys::ImGuiOldColumnFlags_NoPreserveWidths as i32;
const NO_FORCE_WITHIN_WINDOW = sys::ImGuiOldColumnFlags_NoForceWithinWindow as i32;
const GROW_PARENT_CONTENTS_SIZE = sys::ImGuiOldColumnFlags_GrowParentContentsSize as i32;
}
}
impl Default for OldColumnFlags {
fn default() -> Self {
OldColumnFlags::NONE
}
}
impl Ui {
#[doc(alias = "Columns")]
pub fn columns(&self, count: i32, id: impl AsRef<str>, border: bool) {
assert_columns_count(count, "Ui::columns()");
unsafe { sys::igColumns(count, self.scratch_txt(id), border) }
}
#[doc(alias = "BeginColumns")]
pub fn begin_columns(&self, id: impl AsRef<str>, count: i32, flags: OldColumnFlags) {
assert_columns_count(count, "Ui::begin_columns()");
validate_old_column_flags("Ui::begin_columns()", flags);
assert_no_current_columns("Ui::begin_columns()");
unsafe { sys::igBeginColumns(self.scratch_txt(id), count, flags.bits()) }
}
#[doc(alias = "BeginColumns")]
pub fn begin_columns_token(
&self,
id: impl AsRef<str>,
count: i32,
flags: OldColumnFlags,
) -> ColumnsToken<'_> {
self.begin_columns(id, count, flags);
ColumnsToken { ui: self }
}
#[doc(alias = "EndColumns")]
pub fn end_columns(&self) {
assert_current_columns("Ui::end_columns()");
unsafe { sys::igEndColumns() }
}
#[doc(alias = "NextColumn")]
pub fn next_column(&self) {
unsafe { sys::igNextColumn() }
}
#[doc(alias = "GetColumnIndex")]
pub fn current_column_index(&self) -> i32 {
unsafe { sys::igGetColumnIndex() }
}
#[doc(alias = "GetColumnWidth")]
pub fn current_column_width(&self) -> f32 {
unsafe { sys::igGetColumnWidth(-1) }
}
#[doc(alias = "GetColumnWidth")]
pub fn column_width(&self, column_index: i32) -> f32 {
let column_index = if current_columns().is_null() {
column_index
} else {
resolve_column_index(column_index, false, "Ui::column_width()")
};
unsafe { sys::igGetColumnWidth(column_index) }
}
#[doc(alias = "SetColumnWidth")]
pub fn set_current_column_width(&self, width: f32) {
assert_non_negative_f32("Ui::set_current_column_width()", "width", width);
unsafe { sys::igSetColumnWidth(-1, width) };
}
#[doc(alias = "SetColumnWidth")]
pub fn set_column_width(&self, column_index: i32, width: f32) {
let column_index = resolve_column_index(column_index, false, "Ui::set_column_width()");
assert_non_negative_f32("Ui::set_column_width()", "width", width);
unsafe { sys::igSetColumnWidth(column_index, width) };
}
#[doc(alias = "GetColumnOffset")]
pub fn current_column_offset(&self) -> f32 {
unsafe { sys::igGetColumnOffset(-1) }
}
#[doc(alias = "GetColumnOffset")]
pub fn column_offset(&self, column_index: i32) -> f32 {
let column_index = if current_columns().is_null() {
column_index
} else {
resolve_column_index(column_index, true, "Ui::column_offset()")
};
unsafe { sys::igGetColumnOffset(column_index) }
}
#[doc(alias = "SetColumnOffset")]
pub fn set_current_column_offset(&self, offset_x: f32) {
assert_non_negative_f32("Ui::set_current_column_offset()", "offset_x", offset_x);
unsafe { sys::igSetColumnOffset(-1, offset_x) };
}
#[doc(alias = "SetColumnOffset")]
pub fn set_column_offset(&self, column_index: i32, offset_x: f32) {
let column_index = resolve_column_index(column_index, true, "Ui::set_column_offset()");
assert_non_negative_f32("Ui::set_column_offset()", "offset_x", offset_x);
unsafe { sys::igSetColumnOffset(column_index, offset_x) };
}
#[doc(alias = "GetColumnsCount")]
pub fn column_count(&self) -> i32 {
unsafe { sys::igGetColumnsCount() }
}
#[doc(alias = "PushColumnClipRect")]
pub fn push_column_clip_rect(&self, column_index: i32) {
let column_index = resolve_column_index(column_index, false, "Ui::push_column_clip_rect()");
unsafe { sys::igPushColumnClipRect(column_index) }
}
#[doc(alias = "PushColumnsBackground")]
pub fn push_columns_background(&self) {
assert_current_columns("Ui::push_columns_background()");
unsafe { sys::igPushColumnsBackground() }
}
#[doc(alias = "PopColumnsBackground")]
pub fn pop_columns_background(&self) {
assert_current_columns("Ui::pop_columns_background()");
unsafe { sys::igPopColumnsBackground() }
}
#[doc(alias = "GetColumnsID")]
pub fn get_columns_id(&self, str_id: impl AsRef<str>, count: i32) -> u32 {
assert_columns_count(count, "Ui::get_columns_id()");
unsafe { sys::igGetColumnsID(self.scratch_txt(str_id), count) }
}
pub fn is_any_column_resizing(&self) -> bool {
unsafe {
let window = sys::igGetCurrentWindowRead();
if window.is_null() {
return false;
}
let columns = (*window).DC.CurrentColumns;
if columns.is_null() {
return false;
}
(*columns).IsBeingResized
}
}
pub fn get_columns_total_width(&self) -> f32 {
let count = self.column_count();
if count <= 0 {
return 0.0;
}
let mut total_width = 0.0;
for i in 0..count {
total_width += self.column_width(i);
}
total_width
}
pub fn set_columns_equal_width(&self) {
let count = self.column_count();
if count <= 1 {
return;
}
let total_width = self.get_columns_total_width();
let equal_width = total_width / count as f32;
for i in 0..count {
self.set_column_width(i, equal_width);
}
}
pub fn get_column_width_percentage(&self, column_index: i32) -> f32 {
let total_width = self.get_columns_total_width();
if total_width <= 0.0 {
return 0.0;
}
let column_width = self.column_width(column_index);
(column_width / total_width) * 100.0
}
pub fn set_column_width_percentage(&self, column_index: i32, percentage: f32) {
assert_non_negative_f32(
"Ui::set_column_width_percentage()",
"percentage",
percentage,
);
let total_width = self.get_columns_total_width();
if total_width <= 0.0 {
return;
}
let new_width = (total_width * percentage) / 100.0;
self.set_column_width(column_index, new_width);
}
}
#[must_use]
pub struct ColumnsToken<'ui> {
ui: &'ui Ui,
}
impl Drop for ColumnsToken<'_> {
fn drop(&mut self) {
self.ui.end_columns();
}
}
#[cfg(test)]
mod tests {
use super::OldColumnFlags;
fn setup_context() -> crate::Context {
let mut ctx = crate::Context::create();
let _ = ctx.font_atlas_mut().build();
ctx.io_mut().set_display_size([128.0, 128.0]);
ctx.io_mut().set_delta_time(1.0 / 60.0);
ctx
}
#[test]
fn is_any_column_resizing_reads_current_columns_state() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("columns_resize_test").build(|| {
assert!(!ui.is_any_column_resizing());
let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
let window = unsafe { crate::sys::igGetCurrentWindowRead() };
assert!(!window.is_null());
let columns = unsafe { (*window).DC.CurrentColumns };
assert!(!columns.is_null());
assert!(!ui.is_any_column_resizing());
unsafe {
(*columns).IsBeingResized = true;
}
assert!(ui.is_any_column_resizing());
});
}
#[test]
fn columns_reject_invalid_counts_and_nested_layouts() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("columns_invalid_counts").build(|| {
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.columns(0, "bad_columns", true);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _columns = ui.begin_columns_token("bad_columns", 0, OldColumnFlags::NONE);
}))
.is_err()
);
let _columns = ui.begin_columns_token("outer_columns", 2, OldColumnFlags::NONE);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _nested = ui.begin_columns_token("nested_columns", 2, OldColumnFlags::NONE);
}))
.is_err()
);
});
}
#[test]
fn columns_reject_out_of_range_indices_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("columns_index_bounds").build(|| {
let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = ui.column_width(2);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.set_column_width(2, 10.0);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _ = ui.column_offset(3);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.push_column_clip_rect(2);
}))
.is_err()
);
});
}
#[test]
fn columns_reject_invalid_flags_and_numeric_inputs_before_ffi() {
let mut ctx = setup_context();
let ui = ctx.frame();
ui.window("columns_numeric_bounds").build(|| {
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _columns = ui.begin_columns_token(
"bad_flags",
2,
OldColumnFlags::from_bits_retain(1 << 16),
);
}))
.is_err()
);
let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
ui.set_current_column_width(32.0);
ui.set_current_column_offset(0.0);
ui.set_column_width(1, 16.0);
ui.set_column_offset(1, 8.0);
ui.set_column_width_percentage(1, 25.0);
ui.set_current_column_width(0.0);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.set_column_width(1, f32::NAN);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.set_current_column_offset(-1.0);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.set_column_offset(1, f32::INFINITY);
}))
.is_err()
);
assert!(
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ui.set_column_width_percentage(1, -1.0);
}))
.is_err()
);
});
}
}