1#![allow(
8 clippy::cast_possible_truncation,
9 clippy::cast_sign_loss,
10 clippy::as_conversions,
11 clippy::unnecessary_cast
12)]
13use crate::Ui;
14use crate::sys;
15use bitflags::bitflags;
16
17fn current_columns() -> *mut sys::ImGuiOldColumns {
18 unsafe {
19 let window = sys::igGetCurrentWindowRead();
20 if window.is_null() {
21 std::ptr::null_mut()
22 } else {
23 (*window).DC.CurrentColumns
24 }
25 }
26}
27
28fn assert_no_current_columns(caller: &str) {
29 assert!(
30 current_columns().is_null(),
31 "{caller} cannot be called while another legacy columns layout is active"
32 );
33}
34
35fn assert_current_columns(caller: &str) -> *mut sys::ImGuiOldColumns {
36 let columns = current_columns();
37 assert!(
38 !columns.is_null(),
39 "{caller} must be called inside a legacy columns layout"
40 );
41 columns
42}
43
44fn assert_columns_count(count: i32, caller: &str) {
45 assert!(count >= 1, "{caller} count must be at least 1");
46}
47
48fn assert_finite_f32(caller: &str, name: &str, value: f32) {
49 assert!(value.is_finite(), "{caller} {name} must be finite");
50}
51
52fn assert_non_negative_f32(caller: &str, name: &str, value: f32) {
53 assert_finite_f32(caller, name, value);
54 assert!(value >= 0.0, "{caller} {name} must be non-negative");
55}
56
57fn validate_old_column_flags(caller: &str, flags: OldColumnFlags) {
58 let unsupported = flags.bits() & !OldColumnFlags::all().bits();
59 assert!(
60 unsupported == 0,
61 "{caller} received unsupported ImGuiOldColumnFlags bits: 0x{unsupported:X}"
62 );
63}
64
65fn resolve_column_index(column_index: i32, allow_trailing_offset: bool, caller: &str) -> i32 {
66 let columns = assert_current_columns(caller);
67 let column_index = if column_index < 0 {
68 unsafe { (*columns).Current }
69 } else {
70 column_index
71 };
72 let upper_bound = unsafe {
73 if allow_trailing_offset {
74 (*columns).Count
75 } else {
76 (*columns).Count - 1
77 }
78 };
79 assert!(
80 (0..=upper_bound).contains(&column_index),
81 "{caller} column index {column_index} is outside the allowed range 0..={upper_bound}"
82 );
83 column_index
84}
85
86bitflags! {
87 #[repr(transparent)]
89 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
90 pub struct OldColumnFlags: i32 {
91 const NONE = sys::ImGuiOldColumnFlags_None as i32;
93 const NO_BORDER = sys::ImGuiOldColumnFlags_NoBorder as i32;
95 const NO_RESIZE = sys::ImGuiOldColumnFlags_NoResize as i32;
97 const NO_PRESERVE_WIDTHS = sys::ImGuiOldColumnFlags_NoPreserveWidths as i32;
99 const NO_FORCE_WITHIN_WINDOW = sys::ImGuiOldColumnFlags_NoForceWithinWindow as i32;
101 const GROW_PARENT_CONTENTS_SIZE = sys::ImGuiOldColumnFlags_GrowParentContentsSize as i32;
103 }
104}
105
106impl Default for OldColumnFlags {
107 fn default() -> Self {
108 OldColumnFlags::NONE
109 }
110}
111
112impl Ui {
114 #[doc(alias = "Columns")]
121 pub fn columns(&self, count: i32, id: impl AsRef<str>, border: bool) {
122 assert_columns_count(count, "Ui::columns()");
123 unsafe { sys::igColumns(count, self.scratch_txt(id), border) }
124 }
125
126 #[doc(alias = "BeginColumns")]
133 pub fn begin_columns(&self, id: impl AsRef<str>, count: i32, flags: OldColumnFlags) {
134 assert_columns_count(count, "Ui::begin_columns()");
135 validate_old_column_flags("Ui::begin_columns()", flags);
136 assert_no_current_columns("Ui::begin_columns()");
137 unsafe { sys::igBeginColumns(self.scratch_txt(id), count, flags.bits()) }
138 }
139
140 #[doc(alias = "BeginColumns")]
142 pub fn begin_columns_token(
143 &self,
144 id: impl AsRef<str>,
145 count: i32,
146 flags: OldColumnFlags,
147 ) -> ColumnsToken<'_> {
148 self.begin_columns(id, count, flags);
149 ColumnsToken { ui: self }
150 }
151
152 #[doc(alias = "EndColumns")]
154 pub fn end_columns(&self) {
155 assert_current_columns("Ui::end_columns()");
156 unsafe { sys::igEndColumns() }
157 }
158
159 #[doc(alias = "NextColumn")]
163 pub fn next_column(&self) {
164 unsafe { sys::igNextColumn() }
165 }
166
167 #[doc(alias = "GetColumnIndex")]
169 pub fn current_column_index(&self) -> i32 {
170 unsafe { sys::igGetColumnIndex() }
171 }
172
173 #[doc(alias = "GetColumnWidth")]
175 pub fn current_column_width(&self) -> f32 {
176 unsafe { sys::igGetColumnWidth(-1) }
177 }
178
179 #[doc(alias = "GetColumnWidth")]
181 pub fn column_width(&self, column_index: i32) -> f32 {
182 let column_index = if current_columns().is_null() {
183 column_index
184 } else {
185 resolve_column_index(column_index, false, "Ui::column_width()")
186 };
187 unsafe { sys::igGetColumnWidth(column_index) }
188 }
189
190 #[doc(alias = "SetColumnWidth")]
192 pub fn set_current_column_width(&self, width: f32) {
193 assert_non_negative_f32("Ui::set_current_column_width()", "width", width);
194 unsafe { sys::igSetColumnWidth(-1, width) };
195 }
196
197 #[doc(alias = "SetColumnWidth")]
199 pub fn set_column_width(&self, column_index: i32, width: f32) {
200 let column_index = resolve_column_index(column_index, false, "Ui::set_column_width()");
201 assert_non_negative_f32("Ui::set_column_width()", "width", width);
202 unsafe { sys::igSetColumnWidth(column_index, width) };
203 }
204
205 #[doc(alias = "GetColumnOffset")]
207 pub fn current_column_offset(&self) -> f32 {
208 unsafe { sys::igGetColumnOffset(-1) }
209 }
210
211 #[doc(alias = "GetColumnOffset")]
213 pub fn column_offset(&self, column_index: i32) -> f32 {
214 let column_index = if current_columns().is_null() {
215 column_index
216 } else {
217 resolve_column_index(column_index, true, "Ui::column_offset()")
218 };
219 unsafe { sys::igGetColumnOffset(column_index) }
220 }
221
222 #[doc(alias = "SetColumnOffset")]
224 pub fn set_current_column_offset(&self, offset_x: f32) {
225 assert_non_negative_f32("Ui::set_current_column_offset()", "offset_x", offset_x);
226 unsafe { sys::igSetColumnOffset(-1, offset_x) };
227 }
228
229 #[doc(alias = "SetColumnOffset")]
231 pub fn set_column_offset(&self, column_index: i32, offset_x: f32) {
232 let column_index = resolve_column_index(column_index, true, "Ui::set_column_offset()");
233 assert_non_negative_f32("Ui::set_column_offset()", "offset_x", offset_x);
234 unsafe { sys::igSetColumnOffset(column_index, offset_x) };
235 }
236
237 #[doc(alias = "GetColumnsCount")]
239 pub fn column_count(&self) -> i32 {
240 unsafe { sys::igGetColumnsCount() }
241 }
242
243 #[doc(alias = "PushColumnClipRect")]
250 pub fn push_column_clip_rect(&self, column_index: i32) {
251 let column_index = resolve_column_index(column_index, false, "Ui::push_column_clip_rect()");
252 unsafe { sys::igPushColumnClipRect(column_index) }
253 }
254
255 #[doc(alias = "PushColumnsBackground")]
257 pub fn push_columns_background(&self) {
258 assert_current_columns("Ui::push_columns_background()");
259 unsafe { sys::igPushColumnsBackground() }
260 }
261
262 #[doc(alias = "PopColumnsBackground")]
264 pub fn pop_columns_background(&self) {
265 assert_current_columns("Ui::pop_columns_background()");
266 unsafe { sys::igPopColumnsBackground() }
267 }
268
269 #[doc(alias = "GetColumnsID")]
271 pub fn get_columns_id(&self, str_id: impl AsRef<str>, count: i32) -> u32 {
272 assert_columns_count(count, "Ui::get_columns_id()");
273 unsafe { sys::igGetColumnsID(self.scratch_txt(str_id), count) }
274 }
275
276 pub fn is_any_column_resizing(&self) -> bool {
284 unsafe {
285 let window = sys::igGetCurrentWindowRead();
286 if window.is_null() {
287 return false;
288 }
289
290 let columns = (*window).DC.CurrentColumns;
291 if columns.is_null() {
292 return false;
293 }
294
295 (*columns).IsBeingResized
296 }
297 }
298
299 pub fn get_columns_total_width(&self) -> f32 {
301 let count = self.column_count();
302 if count <= 0 {
303 return 0.0;
304 }
305
306 let mut total_width = 0.0;
307 for i in 0..count {
308 total_width += self.column_width(i);
309 }
310 total_width
311 }
312
313 pub fn set_columns_equal_width(&self) {
315 let count = self.column_count();
316 if count <= 1 {
317 return;
318 }
319
320 let total_width = self.get_columns_total_width();
321 let equal_width = total_width / count as f32;
322
323 for i in 0..count {
324 self.set_column_width(i, equal_width);
325 }
326 }
327
328 pub fn get_column_width_percentage(&self, column_index: i32) -> f32 {
330 let total_width = self.get_columns_total_width();
331 if total_width <= 0.0 {
332 return 0.0;
333 }
334
335 let column_width = self.column_width(column_index);
336 (column_width / total_width) * 100.0
337 }
338
339 pub fn set_column_width_percentage(&self, column_index: i32, percentage: f32) {
341 assert_non_negative_f32(
342 "Ui::set_column_width_percentage()",
343 "percentage",
344 percentage,
345 );
346 let total_width = self.get_columns_total_width();
347 if total_width <= 0.0 {
348 return;
349 }
350
351 let new_width = (total_width * percentage) / 100.0;
352 self.set_column_width(column_index, new_width);
353 }
354}
355
356#[must_use]
358pub struct ColumnsToken<'ui> {
359 ui: &'ui Ui,
360}
361
362impl Drop for ColumnsToken<'_> {
363 fn drop(&mut self) {
364 self.ui.end_columns();
365 }
366}
367
368#[cfg(test)]
369mod tests {
370 use super::OldColumnFlags;
371
372 fn setup_context() -> crate::Context {
373 let mut ctx = crate::Context::create();
374 let _ = ctx.font_atlas_mut().build();
375 ctx.io_mut().set_display_size([128.0, 128.0]);
376 ctx.io_mut().set_delta_time(1.0 / 60.0);
377 ctx
378 }
379
380 #[test]
381 fn is_any_column_resizing_reads_current_columns_state() {
382 let mut ctx = setup_context();
383 let ui = ctx.frame();
384
385 ui.window("columns_resize_test").build(|| {
386 assert!(!ui.is_any_column_resizing());
387
388 let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
389 let window = unsafe { crate::sys::igGetCurrentWindowRead() };
390 assert!(!window.is_null());
391
392 let columns = unsafe { (*window).DC.CurrentColumns };
393 assert!(!columns.is_null());
394 assert!(!ui.is_any_column_resizing());
395
396 unsafe {
397 (*columns).IsBeingResized = true;
398 }
399
400 assert!(ui.is_any_column_resizing());
401 });
402 }
403
404 #[test]
405 fn columns_reject_invalid_counts_and_nested_layouts() {
406 let mut ctx = setup_context();
407 let ui = ctx.frame();
408
409 ui.window("columns_invalid_counts").build(|| {
410 assert!(
411 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
412 ui.columns(0, "bad_columns", true);
413 }))
414 .is_err()
415 );
416 assert!(
417 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
418 let _columns = ui.begin_columns_token("bad_columns", 0, OldColumnFlags::NONE);
419 }))
420 .is_err()
421 );
422
423 let _columns = ui.begin_columns_token("outer_columns", 2, OldColumnFlags::NONE);
424 assert!(
425 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
426 let _nested = ui.begin_columns_token("nested_columns", 2, OldColumnFlags::NONE);
427 }))
428 .is_err()
429 );
430 });
431 }
432
433 #[test]
434 fn columns_reject_out_of_range_indices_before_ffi() {
435 let mut ctx = setup_context();
436 let ui = ctx.frame();
437
438 ui.window("columns_index_bounds").build(|| {
439 let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
440
441 assert!(
442 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
443 let _ = ui.column_width(2);
444 }))
445 .is_err()
446 );
447 assert!(
448 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
449 ui.set_column_width(2, 10.0);
450 }))
451 .is_err()
452 );
453 assert!(
454 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
455 let _ = ui.column_offset(3);
456 }))
457 .is_err()
458 );
459 assert!(
460 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
461 ui.push_column_clip_rect(2);
462 }))
463 .is_err()
464 );
465 });
466 }
467
468 #[test]
469 fn columns_reject_invalid_flags_and_numeric_inputs_before_ffi() {
470 let mut ctx = setup_context();
471 let ui = ctx.frame();
472
473 ui.window("columns_numeric_bounds").build(|| {
474 assert!(
475 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
476 let _columns = ui.begin_columns_token(
477 "bad_flags",
478 2,
479 OldColumnFlags::from_bits_retain(1 << 16),
480 );
481 }))
482 .is_err()
483 );
484
485 let _columns = ui.begin_columns_token("legacy_columns", 2, OldColumnFlags::NONE);
486 ui.set_current_column_width(32.0);
487 ui.set_current_column_offset(0.0);
488 ui.set_column_width(1, 16.0);
489 ui.set_column_offset(1, 8.0);
490 ui.set_column_width_percentage(1, 25.0);
491
492 ui.set_current_column_width(0.0);
493 assert!(
494 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
495 ui.set_column_width(1, f32::NAN);
496 }))
497 .is_err()
498 );
499 assert!(
500 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
501 ui.set_current_column_offset(-1.0);
502 }))
503 .is_err()
504 );
505 assert!(
506 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
507 ui.set_column_offset(1, f32::INFINITY);
508 }))
509 .is_err()
510 );
511 assert!(
512 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
513 ui.set_column_width_percentage(1, -1.0);
514 }))
515 .is_err()
516 );
517 });
518 }
519}