1use crate::{Axis, AxisFlags, PlotCond, XAxis, YAxis, sys};
2use dear_imgui_rs::{
3 Context as ImGuiContext, Ui, with_scratch_txt, with_scratch_txt_slice, with_scratch_txt_two,
4};
5use dear_imgui_sys as imgui_sys;
6use std::os::raw::c_char;
7use std::{cell::RefCell, rc::Rc};
8
9fn assert_finite_f64(caller: &str, name: &str, value: f64) {
10 assert!(value.is_finite(), "{caller} {name} must be finite");
11}
12
13fn assert_finite_vec2(caller: &str, name: &str, value: [f32; 2]) {
14 assert!(
15 value[0].is_finite() && value[1].is_finite(),
16 "{caller} {name} must be finite"
17 );
18}
19
20fn assert_finite_f64_slice(caller: &str, name: &str, values: &[f64]) {
21 assert!(
22 values.iter().all(|value| value.is_finite()),
23 "{caller} {name} must contain only finite values"
24 );
25}
26
27fn assert_axis_limit_range(caller: &str, min: f64, max: f64) {
28 assert_finite_f64(caller, "min", min);
29 assert_finite_f64(caller, "max", max);
30 assert!(min != max, "{caller} min and max must differ");
31}
32
33fn assert_axis_constraint_range(caller: &str, min: f64, max: f64) {
34 assert_finite_f64(caller, "min", min);
35 assert_finite_f64(caller, "max", max);
36 assert!(min <= max, "{caller} min must be <= max");
37}
38
39fn assert_axis_zoom_range(caller: &str, min: f64, max: f64) {
40 assert_finite_f64(caller, "min", min);
41 assert_finite_f64(caller, "max", max);
42 assert!(min > 0.0, "{caller} min must be positive");
43 assert!(min <= max, "{caller} min must be <= max");
44}
45
46fn assert_positive_tick_count(caller: &str, n_ticks: i32) {
47 assert!(n_ticks > 0, "{caller} n_ticks must be positive");
48}
49
50pub struct PlotContext {
55 raw: *mut sys::ImPlotContext,
56 imgui_ctx_raw: *mut imgui_sys::ImGuiContext,
57 imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
58 owns_context: bool,
59}
60
61#[derive(Clone, Copy)]
62pub(crate) struct PlotContextBinding {
63 plot_ctx_raw: *mut sys::ImPlotContext,
64 imgui_ctx_raw: *mut imgui_sys::ImGuiContext,
65}
66
67impl PlotContextBinding {
68 pub(crate) fn bind(self, caller: &str) {
69 assert!(
70 !self.imgui_ctx_raw.is_null(),
71 "{caller} requires an active ImGui context"
72 );
73 assert!(
74 !self.plot_ctx_raw.is_null(),
75 "{caller} requires an active ImPlot context"
76 );
77 assert_eq!(
78 unsafe { imgui_sys::igGetCurrentContext() },
79 self.imgui_ctx_raw,
80 "{caller} must be used with the currently-active ImGui context"
81 );
82 unsafe {
83 sys::ImPlot_SetImGuiContext(self.imgui_ctx_raw);
84 sys::ImPlot_SetCurrentContext(self.plot_ctx_raw);
85 }
86 }
87}
88
89impl PlotContext {
90 pub fn try_create(imgui_ctx: &ImGuiContext) -> dear_imgui_rs::ImGuiResult<Self> {
95 let imgui_ctx_raw = imgui_ctx.as_raw();
96 let imgui_alive = Some(imgui_ctx.alive_token());
97 assert_eq!(
98 unsafe { imgui_sys::igGetCurrentContext() },
99 imgui_ctx_raw,
100 "dear-implot: PlotContext must be created with the currently-active ImGui context"
101 );
102
103 unsafe { sys::ImPlot_SetImGuiContext(imgui_ctx_raw) };
107
108 let raw = unsafe { sys::ImPlot_CreateContext() };
109 if raw.is_null() {
110 return Err(dear_imgui_rs::ImGuiError::context_creation(
111 "ImPlot_CreateContext returned null",
112 ));
113 }
114
115 unsafe { sys::ImPlot_SetCurrentContext(raw) };
117
118 Ok(Self {
119 raw,
120 imgui_ctx_raw,
121 imgui_alive,
122 owns_context: true,
123 })
124 }
125
126 pub fn create(imgui_ctx: &ImGuiContext) -> Self {
128 Self::try_create(imgui_ctx).expect("Failed to create ImPlot context")
129 }
130
131 pub unsafe fn current() -> Option<Self> {
141 let raw = unsafe { sys::ImPlot_GetCurrentContext() };
142 if raw.is_null() {
143 None
144 } else {
145 Some(Self {
146 raw,
147 imgui_ctx_raw: unsafe { imgui_sys::igGetCurrentContext() },
148 imgui_alive: None,
149 owns_context: false,
150 })
151 }
152 }
153
154 pub fn set_as_current(&self) {
156 self.assert_imgui_alive();
157 self.binding().bind("dear-implot: PlotContext");
158 }
159
160 fn assert_imgui_alive(&self) {
161 if let Some(alive) = &self.imgui_alive {
162 assert!(
163 alive.is_alive(),
164 "dear-implot: ImGui context has been dropped"
165 );
166 }
167 }
168
169 fn binding(&self) -> PlotContextBinding {
170 PlotContextBinding {
171 plot_ctx_raw: self.raw,
172 imgui_ctx_raw: self.imgui_ctx_raw,
173 }
174 }
175
176 pub fn get_plot_ui<'ui>(&'ui self, ui: &'ui Ui) -> PlotUi<'ui> {
181 self.set_as_current();
182 PlotUi { context: self, ui }
183 }
184
185 pub unsafe fn raw(&self) -> *mut sys::ImPlotContext {
192 self.raw
193 }
194}
195
196impl Drop for PlotContext {
197 fn drop(&mut self) {
198 if !self.owns_context || self.raw.is_null() {
199 return;
200 }
201
202 if let Some(alive) = &self.imgui_alive {
203 if !alive.is_alive() {
204 return;
207 }
208 }
209
210 unsafe {
211 let prev_imgui = imgui_sys::igGetCurrentContext();
212 imgui_sys::igSetCurrentContext(self.imgui_ctx_raw);
213 sys::ImPlot_SetImGuiContext(self.imgui_ctx_raw);
214
215 if sys::ImPlot_GetCurrentContext() == self.raw {
216 sys::ImPlot_SetCurrentContext(std::ptr::null_mut());
217 }
218 sys::ImPlot_DestroyContext(self.raw);
219
220 imgui_sys::igSetCurrentContext(prev_imgui);
221 }
222 }
223}
224
225pub struct PlotUi<'ui> {
232 #[allow(dead_code)]
233 context: &'ui PlotContext,
234 #[allow(dead_code)]
235 ui: &'ui Ui,
236}
237
238impl<'ui> PlotUi<'ui> {
239 #[inline]
240 pub(crate) fn bind(&self) {
241 self.context.assert_imgui_alive();
242 self.context.binding().bind("dear-implot: PlotUi");
243 }
244
245 pub fn begin_plot(&self, title: &str) -> Option<PlotToken<'_>> {
250 let size = sys::ImVec2_c { x: -1.0, y: 0.0 };
251 if title.contains('\0') {
252 return None;
253 }
254 self.bind();
255 let started = with_scratch_txt(title, |ptr| unsafe { sys::ImPlot_BeginPlot(ptr, size, 0) });
256
257 if started {
258 Some(PlotToken::new(
259 self.context.binding(),
260 self.context.imgui_alive.clone(),
261 ))
262 } else {
263 None
264 }
265 }
266
267 pub fn begin_plot_with_size(&self, title: &str, size: [f32; 2]) -> Option<PlotToken<'_>> {
269 assert_finite_vec2("PlotUi::begin_plot_with_size()", "size", size);
270 let plot_size = sys::ImVec2_c {
271 x: size[0],
272 y: size[1],
273 };
274 if title.contains('\0') {
275 return None;
276 }
277 self.bind();
278 let started = with_scratch_txt(title, |ptr| unsafe {
279 sys::ImPlot_BeginPlot(ptr, plot_size, 0)
280 });
281
282 if started {
283 Some(PlotToken::new(
284 self.context.binding(),
285 self.context.imgui_alive.clone(),
286 ))
287 } else {
288 None
289 }
290 }
291
292 pub fn plot_line(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
296 if x_data.len() != y_data.len() {
297 return; }
299 let count = match i32::try_from(x_data.len()) {
300 Ok(v) => v,
301 Err(_) => return,
302 };
303
304 let label = if label.contains('\0') { "" } else { label };
305 self.bind();
306 with_scratch_txt(label, |ptr| unsafe {
307 let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
308 sys::ImPlot_PlotLine_doublePtrdoublePtr(
309 ptr,
310 x_data.as_ptr(),
311 y_data.as_ptr(),
312 count,
313 spec,
314 );
315 })
316 }
317
318 pub fn plot_scatter(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
320 if x_data.len() != y_data.len() {
321 return; }
323 let count = match i32::try_from(x_data.len()) {
324 Ok(v) => v,
325 Err(_) => return,
326 };
327
328 let label = if label.contains('\0') { "" } else { label };
329 self.bind();
330 with_scratch_txt(label, |ptr| unsafe {
331 let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
332 sys::ImPlot_PlotScatter_doublePtrdoublePtr(
333 ptr,
334 x_data.as_ptr(),
335 y_data.as_ptr(),
336 count,
337 spec,
338 );
339 })
340 }
341
342 pub fn plot_polygon(&self, label: &str, x_data: &[f64], y_data: &[f64]) {
344 if x_data.len() != y_data.len() {
345 return;
346 }
347 let count = match i32::try_from(x_data.len()) {
348 Ok(v) => v,
349 Err(_) => return,
350 };
351
352 let label = if label.contains('\0') { "" } else { label };
353 self.bind();
354 with_scratch_txt(label, |ptr| unsafe {
355 let spec = crate::plots::plot_spec_from(0, 0, std::mem::size_of::<f64>() as i32);
356 sys::ImPlot_PlotPolygon_doublePtr(ptr, x_data.as_ptr(), y_data.as_ptr(), count, spec);
357 })
358 }
359
360 pub fn is_plot_hovered(&self) -> bool {
362 self.bind();
363 unsafe { sys::ImPlot_IsPlotHovered() }
364 }
365
366 pub fn get_plot_mouse_pos(&self, y_axis: Option<crate::YAxisChoice>) -> sys::ImPlotPoint {
368 let y_axis_i32 = crate::y_axis_choice_option_to_i32(y_axis);
369 let y_axis = match y_axis_i32 {
370 0 => 3,
371 1 => 4,
372 2 => 5,
373 _ => 3,
374 };
375 self.bind();
376 unsafe { sys::ImPlot_GetPlotMousePos(0, y_axis) }
377 }
378
379 pub fn get_plot_mouse_pos_axes(&self, x_axis: XAxis, y_axis: YAxis) -> sys::ImPlotPoint {
381 self.bind();
382 unsafe { sys::ImPlot_GetPlotMousePos(x_axis as i32, y_axis as i32) }
383 }
384
385 pub fn set_axes(&self, x_axis: XAxis, y_axis: YAxis) {
387 self.bind();
388 unsafe { sys::ImPlot_SetAxes(x_axis as i32, y_axis as i32) }
389 }
390
391 pub fn setup_x_axis(&self, axis: XAxis, label: Option<&str>, flags: AxisFlags) {
393 self.bind();
394 let label = label.filter(|s| !s.contains('\0'));
395 match label {
396 Some(label) => with_scratch_txt(label, |ptr| unsafe {
397 sys::ImPlot_SetupAxis(
398 axis as sys::ImAxis,
399 ptr,
400 flags.bits() as sys::ImPlotAxisFlags,
401 )
402 }),
403 None => unsafe {
404 sys::ImPlot_SetupAxis(
405 axis as sys::ImAxis,
406 std::ptr::null(),
407 flags.bits() as sys::ImPlotAxisFlags,
408 )
409 },
410 }
411 }
412
413 pub fn setup_y_axis(&self, axis: YAxis, label: Option<&str>, flags: AxisFlags) {
415 self.bind();
416 let label = label.filter(|s| !s.contains('\0'));
417 match label {
418 Some(label) => with_scratch_txt(label, |ptr| unsafe {
419 sys::ImPlot_SetupAxis(
420 axis as sys::ImAxis,
421 ptr,
422 flags.bits() as sys::ImPlotAxisFlags,
423 )
424 }),
425 None => unsafe {
426 sys::ImPlot_SetupAxis(
427 axis as sys::ImAxis,
428 std::ptr::null(),
429 flags.bits() as sys::ImPlotAxisFlags,
430 )
431 },
432 }
433 }
434
435 pub fn setup_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
437 assert_axis_limit_range("PlotUi::setup_x_axis_limits()", min, max);
438 self.bind();
439 unsafe {
440 sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
441 }
442 }
443
444 pub fn setup_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
446 assert_axis_limit_range("PlotUi::setup_y_axis_limits()", min, max);
447 self.bind();
448 unsafe {
449 sys::ImPlot_SetupAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
450 }
451 }
452
453 pub fn setup_axis_links(
455 &self,
456 axis: Axis,
457 link_min: Option<&mut f64>,
458 link_max: Option<&mut f64>,
459 ) {
460 let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
461 let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
462 self.bind();
463 unsafe { sys::ImPlot_SetupAxisLinks(axis.to_sys(), pmin, pmax) }
464 }
465
466 pub unsafe fn setup_axis_links_unchecked(
473 &self,
474 axis: sys::ImAxis,
475 link_min: Option<&mut f64>,
476 link_max: Option<&mut f64>,
477 ) {
478 let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
479 let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
480 self.bind();
481 unsafe { sys::ImPlot_SetupAxisLinks(axis, pmin, pmax) }
482 }
483
484 pub fn setup_axes(
486 &self,
487 x_label: Option<&str>,
488 y_label: Option<&str>,
489 x_flags: AxisFlags,
490 y_flags: AxisFlags,
491 ) {
492 self.bind();
493 let x_label = x_label.filter(|s| !s.contains('\0'));
494 let y_label = y_label.filter(|s| !s.contains('\0'));
495
496 match (x_label, y_label) {
497 (Some(x_label), Some(y_label)) => {
498 with_scratch_txt_two(x_label, y_label, |xp, yp| unsafe {
499 sys::ImPlot_SetupAxes(
500 xp,
501 yp,
502 x_flags.bits() as sys::ImPlotAxisFlags,
503 y_flags.bits() as sys::ImPlotAxisFlags,
504 )
505 })
506 }
507 (Some(x_label), None) => with_scratch_txt(x_label, |xp| unsafe {
508 sys::ImPlot_SetupAxes(
509 xp,
510 std::ptr::null(),
511 x_flags.bits() as sys::ImPlotAxisFlags,
512 y_flags.bits() as sys::ImPlotAxisFlags,
513 )
514 }),
515 (None, Some(y_label)) => with_scratch_txt(y_label, |yp| unsafe {
516 sys::ImPlot_SetupAxes(
517 std::ptr::null(),
518 yp,
519 x_flags.bits() as sys::ImPlotAxisFlags,
520 y_flags.bits() as sys::ImPlotAxisFlags,
521 )
522 }),
523 (None, None) => unsafe {
524 sys::ImPlot_SetupAxes(
525 std::ptr::null(),
526 std::ptr::null(),
527 x_flags.bits() as sys::ImPlotAxisFlags,
528 y_flags.bits() as sys::ImPlotAxisFlags,
529 )
530 },
531 }
532 }
533
534 pub fn setup_axes_limits(
536 &self,
537 x_min: f64,
538 x_max: f64,
539 y_min: f64,
540 y_max: f64,
541 cond: PlotCond,
542 ) {
543 assert_axis_limit_range("PlotUi::setup_axes_limits() x axis", x_min, x_max);
544 assert_axis_limit_range("PlotUi::setup_axes_limits() y axis", y_min, y_max);
545 self.bind();
546 unsafe { sys::ImPlot_SetupAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond) }
547 }
548
549 pub fn setup_finish(&self) {
551 self.bind();
552 unsafe { sys::ImPlot_SetupFinish() }
553 }
554
555 pub fn set_next_x_axis_limits(&self, axis: XAxis, min: f64, max: f64, cond: PlotCond) {
557 assert_axis_limit_range("PlotUi::set_next_x_axis_limits()", min, max);
558 self.bind();
559 unsafe {
560 sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
561 }
562 }
563
564 pub fn set_next_y_axis_limits(&self, axis: YAxis, min: f64, max: f64, cond: PlotCond) {
566 assert_axis_limit_range("PlotUi::set_next_y_axis_limits()", min, max);
567 self.bind();
568 unsafe {
569 sys::ImPlot_SetNextAxisLimits(axis as sys::ImAxis, min, max, cond as sys::ImPlotCond)
570 }
571 }
572
573 pub fn set_next_axis_links(
575 &self,
576 axis: Axis,
577 link_min: Option<&mut f64>,
578 link_max: Option<&mut f64>,
579 ) {
580 let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
581 let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
582 self.bind();
583 unsafe { sys::ImPlot_SetNextAxisLinks(axis.to_sys(), pmin, pmax) }
584 }
585
586 pub unsafe fn set_next_axis_links_unchecked(
593 &self,
594 axis: sys::ImAxis,
595 link_min: Option<&mut f64>,
596 link_max: Option<&mut f64>,
597 ) {
598 let pmin = link_min.map_or(std::ptr::null_mut(), |r| r as *mut f64);
599 let pmax = link_max.map_or(std::ptr::null_mut(), |r| r as *mut f64);
600 self.bind();
601 unsafe { sys::ImPlot_SetNextAxisLinks(axis, pmin, pmax) }
602 }
603
604 pub fn set_next_axes_limits(
606 &self,
607 x_min: f64,
608 x_max: f64,
609 y_min: f64,
610 y_max: f64,
611 cond: PlotCond,
612 ) {
613 assert_axis_limit_range("PlotUi::set_next_axes_limits() x axis", x_min, x_max);
614 assert_axis_limit_range("PlotUi::set_next_axes_limits() y axis", y_min, y_max);
615 self.bind();
616 unsafe {
617 sys::ImPlot_SetNextAxesLimits(x_min, x_max, y_min, y_max, cond as sys::ImPlotCond)
618 }
619 }
620
621 pub fn set_next_axes_to_fit(&self) {
623 self.bind();
624 unsafe { sys::ImPlot_SetNextAxesToFit() }
625 }
626
627 pub fn set_next_axis_to_fit(&self, axis: Axis) {
629 self.bind();
630 unsafe { sys::ImPlot_SetNextAxisToFit(axis.to_sys()) }
631 }
632
633 pub unsafe fn set_next_axis_to_fit_unchecked(&self, axis: sys::ImAxis) {
640 self.bind();
641 unsafe { sys::ImPlot_SetNextAxisToFit(axis) }
642 }
643
644 pub fn set_next_x_axis_to_fit(&self, axis: XAxis) {
646 self.bind();
647 unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
648 }
649
650 pub fn set_next_y_axis_to_fit(&self, axis: YAxis) {
652 self.bind();
653 unsafe { sys::ImPlot_SetNextAxisToFit(axis as sys::ImAxis) }
654 }
655
656 pub fn setup_x_axis_ticks_positions(
660 &self,
661 axis: XAxis,
662 values: &[f64],
663 labels: Option<&[&str]>,
664 keep_default: bool,
665 ) {
666 assert_finite_f64_slice("PlotUi::setup_x_axis_ticks_positions()", "values", values);
667 self.bind();
668 let count = match i32::try_from(values.len()) {
669 Ok(v) => v,
670 Err(_) => return,
671 };
672 if let Some(labels) = labels {
673 if labels.len() != values.len() {
674 return;
675 }
676 let cleaned: Vec<&str> = labels
677 .iter()
678 .map(|&s| if s.contains('\0') { "" } else { s })
679 .collect();
680 with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
681 sys::ImPlot_SetupAxisTicks_doublePtr(
682 axis as sys::ImAxis,
683 values.as_ptr(),
684 count,
685 ptrs.as_ptr() as *const *const c_char,
686 keep_default,
687 )
688 })
689 } else {
690 unsafe {
691 sys::ImPlot_SetupAxisTicks_doublePtr(
692 axis as sys::ImAxis,
693 values.as_ptr(),
694 count,
695 std::ptr::null(),
696 keep_default,
697 )
698 }
699 }
700 }
701
702 pub fn setup_y_axis_ticks_positions(
706 &self,
707 axis: YAxis,
708 values: &[f64],
709 labels: Option<&[&str]>,
710 keep_default: bool,
711 ) {
712 assert_finite_f64_slice("PlotUi::setup_y_axis_ticks_positions()", "values", values);
713 self.bind();
714 let count = match i32::try_from(values.len()) {
715 Ok(v) => v,
716 Err(_) => return,
717 };
718 if let Some(labels) = labels {
719 if labels.len() != values.len() {
720 return;
721 }
722 let cleaned: Vec<&str> = labels
723 .iter()
724 .map(|&s| if s.contains('\0') { "" } else { s })
725 .collect();
726 with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
727 sys::ImPlot_SetupAxisTicks_doublePtr(
728 axis as sys::ImAxis,
729 values.as_ptr(),
730 count,
731 ptrs.as_ptr() as *const *const c_char,
732 keep_default,
733 )
734 })
735 } else {
736 unsafe {
737 sys::ImPlot_SetupAxisTicks_doublePtr(
738 axis as sys::ImAxis,
739 values.as_ptr(),
740 count,
741 std::ptr::null(),
742 keep_default,
743 )
744 }
745 }
746 }
747
748 pub fn setup_x_axis_ticks_range(
752 &self,
753 axis: XAxis,
754 v_min: f64,
755 v_max: f64,
756 n_ticks: i32,
757 labels: Option<&[&str]>,
758 keep_default: bool,
759 ) {
760 assert_axis_limit_range("PlotUi::setup_x_axis_ticks_range()", v_min, v_max);
761 assert_positive_tick_count("PlotUi::setup_x_axis_ticks_range()", n_ticks);
762 self.bind();
763 if let Some(labels) = labels {
764 let Ok(ticks_usize) = usize::try_from(n_ticks) else {
765 return;
766 };
767 if labels.len() != ticks_usize {
768 return;
769 }
770 let cleaned: Vec<&str> = labels
771 .iter()
772 .map(|&s| if s.contains('\0') { "" } else { s })
773 .collect();
774 with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
775 sys::ImPlot_SetupAxisTicks_double(
776 axis as sys::ImAxis,
777 v_min,
778 v_max,
779 n_ticks,
780 ptrs.as_ptr() as *const *const c_char,
781 keep_default,
782 )
783 })
784 } else {
785 unsafe {
786 sys::ImPlot_SetupAxisTicks_double(
787 axis as sys::ImAxis,
788 v_min,
789 v_max,
790 n_ticks,
791 std::ptr::null(),
792 keep_default,
793 )
794 }
795 }
796 }
797
798 pub fn setup_y_axis_ticks_range(
802 &self,
803 axis: YAxis,
804 v_min: f64,
805 v_max: f64,
806 n_ticks: i32,
807 labels: Option<&[&str]>,
808 keep_default: bool,
809 ) {
810 assert_axis_limit_range("PlotUi::setup_y_axis_ticks_range()", v_min, v_max);
811 assert_positive_tick_count("PlotUi::setup_y_axis_ticks_range()", n_ticks);
812 self.bind();
813 if let Some(labels) = labels {
814 let Ok(ticks_usize) = usize::try_from(n_ticks) else {
815 return;
816 };
817 if labels.len() != ticks_usize {
818 return;
819 }
820 let cleaned: Vec<&str> = labels
821 .iter()
822 .map(|&s| if s.contains('\0') { "" } else { s })
823 .collect();
824 with_scratch_txt_slice(&cleaned, |ptrs| unsafe {
825 sys::ImPlot_SetupAxisTicks_double(
826 axis as sys::ImAxis,
827 v_min,
828 v_max,
829 n_ticks,
830 ptrs.as_ptr() as *const *const c_char,
831 keep_default,
832 )
833 })
834 } else {
835 unsafe {
836 sys::ImPlot_SetupAxisTicks_double(
837 axis as sys::ImAxis,
838 v_min,
839 v_max,
840 n_ticks,
841 std::ptr::null(),
842 keep_default,
843 )
844 }
845 }
846 }
847
848 pub fn setup_x_axis_format(&self, axis: XAxis, fmt: &str) {
850 if fmt.contains('\0') {
851 return;
852 }
853 self.bind();
854 with_scratch_txt(fmt, |ptr| unsafe {
855 sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
856 })
857 }
858
859 pub fn setup_y_axis_format(&self, axis: YAxis, fmt: &str) {
861 if fmt.contains('\0') {
862 return;
863 }
864 self.bind();
865 with_scratch_txt(fmt, |ptr| unsafe {
866 sys::ImPlot_SetupAxisFormat_Str(axis as sys::ImAxis, ptr)
867 })
868 }
869
870 pub fn setup_x_axis_scale(&self, axis: XAxis, scale: sys::ImPlotScale) {
872 self.bind();
873 unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
874 }
875
876 pub fn setup_y_axis_scale(&self, axis: YAxis, scale: sys::ImPlotScale) {
878 self.bind();
879 unsafe { sys::ImPlot_SetupAxisScale_PlotScale(axis as sys::ImAxis, scale) }
880 }
881
882 pub fn setup_axis_limits_constraints(&self, axis: Axis, v_min: f64, v_max: f64) {
884 assert_axis_constraint_range("PlotUi::setup_axis_limits_constraints()", v_min, v_max);
885 self.bind();
886 unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis.to_sys(), v_min, v_max) }
887 }
888
889 pub unsafe fn setup_axis_limits_constraints_unchecked(
896 &self,
897 axis: sys::ImAxis,
898 v_min: f64,
899 v_max: f64,
900 ) {
901 assert_axis_constraint_range(
902 "PlotUi::setup_axis_limits_constraints_unchecked()",
903 v_min,
904 v_max,
905 );
906 self.bind();
907 unsafe { sys::ImPlot_SetupAxisLimitsConstraints(axis, v_min, v_max) }
908 }
909
910 pub fn setup_axis_zoom_constraints(&self, axis: Axis, z_min: f64, z_max: f64) {
912 assert_axis_zoom_range("PlotUi::setup_axis_zoom_constraints()", z_min, z_max);
913 self.bind();
914 unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis.to_sys(), z_min, z_max) }
915 }
916
917 pub unsafe fn setup_axis_zoom_constraints_unchecked(
924 &self,
925 axis: sys::ImAxis,
926 z_min: f64,
927 z_max: f64,
928 ) {
929 assert_axis_zoom_range(
930 "PlotUi::setup_axis_zoom_constraints_unchecked()",
931 z_min,
932 z_max,
933 );
934 self.bind();
935 unsafe { sys::ImPlot_SetupAxisZoomConstraints(axis, z_min, z_max) }
936 }
937
938 pub fn setup_x_axis_format_closure<F>(&self, axis: XAxis, f: F) -> AxisFormatterToken
943 where
944 F: Fn(f64) -> String + Send + Sync + 'static,
945 {
946 self.bind();
947 AxisFormatterToken::new(axis as sys::ImAxis, f)
948 }
949
950 pub fn setup_y_axis_format_closure<F>(&self, axis: YAxis, f: F) -> AxisFormatterToken
954 where
955 F: Fn(f64) -> String + Send + Sync + 'static,
956 {
957 self.bind();
958 AxisFormatterToken::new(axis as sys::ImAxis, f)
959 }
960
961 pub fn setup_x_axis_transform_closure<FW, INV>(
966 &self,
967 axis: XAxis,
968 forward: FW,
969 inverse: INV,
970 ) -> AxisTransformToken
971 where
972 FW: Fn(f64) -> f64 + Send + Sync + 'static,
973 INV: Fn(f64) -> f64 + Send + Sync + 'static,
974 {
975 self.bind();
976 AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
977 }
978
979 pub fn setup_y_axis_transform_closure<FW, INV>(
981 &self,
982 axis: YAxis,
983 forward: FW,
984 inverse: INV,
985 ) -> AxisTransformToken
986 where
987 FW: Fn(f64) -> f64 + Send + Sync + 'static,
988 INV: Fn(f64) -> f64 + Send + Sync + 'static,
989 {
990 self.bind();
991 AxisTransformToken::new(axis as sys::ImAxis, forward, inverse)
992 }
993}
994
995#[derive(Default)]
1008struct PlotScopeStorage {
1009 formatters: Vec<Box<FormatterHolder>>,
1010 transforms: Vec<Box<TransformHolder>>,
1011}
1012
1013thread_local! {
1014 static PLOT_SCOPE_STACK: RefCell<Vec<PlotScopeStorage>> = const { RefCell::new(Vec::new()) };
1015}
1016
1017fn with_plot_scope_storage<T>(f: impl FnOnce(&mut PlotScopeStorage) -> T) -> Option<T> {
1018 PLOT_SCOPE_STACK.with(|stack| {
1019 let mut stack = stack.borrow_mut();
1020 stack.last_mut().map(f)
1021 })
1022}
1023
1024pub(crate) struct PlotScopeGuard {
1025 _not_send_or_sync: std::marker::PhantomData<Rc<()>>,
1026}
1027
1028impl PlotScopeGuard {
1029 pub(crate) fn new() -> Self {
1030 PLOT_SCOPE_STACK.with(|stack| stack.borrow_mut().push(PlotScopeStorage::default()));
1031 Self {
1032 _not_send_or_sync: std::marker::PhantomData,
1033 }
1034 }
1035}
1036
1037impl Drop for PlotScopeGuard {
1038 fn drop(&mut self) {
1039 PLOT_SCOPE_STACK.with(|stack| {
1040 let popped = stack.borrow_mut().pop();
1041 debug_assert!(popped.is_some(), "dear-implot: plot scope stack underflow");
1042 });
1043 }
1044}
1045
1046struct FormatterHolder {
1049 func: Box<dyn Fn(f64) -> String + Send + Sync + 'static>,
1050}
1051
1052#[must_use]
1053pub struct AxisFormatterToken {
1054 _private: (),
1055}
1056
1057impl AxisFormatterToken {
1058 fn new<F>(axis: sys::ImAxis, f: F) -> Self
1059 where
1060 F: Fn(f64) -> String + Send + Sync + 'static,
1061 {
1062 let configured = with_plot_scope_storage(|storage| {
1063 let holder = Box::new(FormatterHolder { func: Box::new(f) });
1064 let user = &*holder as *const FormatterHolder as *mut std::os::raw::c_void;
1065 storage.formatters.push(holder);
1066 unsafe {
1067 sys::ImPlot_SetupAxisFormat_PlotFormatter(
1068 axis as sys::ImAxis,
1069 Some(formatter_thunk),
1070 user,
1071 )
1072 }
1073 })
1074 .is_some();
1075
1076 debug_assert!(
1077 configured,
1078 "dear-implot: axis formatter closure must be set within an active plot"
1079 );
1080
1081 Self { _private: () }
1082 }
1083}
1084
1085impl Drop for AxisFormatterToken {
1086 fn drop(&mut self) {
1087 }
1089}
1090
1091unsafe extern "C" fn formatter_thunk(
1092 value: f64,
1093 buff: *mut std::os::raw::c_char,
1094 size: std::os::raw::c_int,
1095 user_data: *mut std::os::raw::c_void,
1096) -> std::os::raw::c_int {
1097 if user_data.is_null() || buff.is_null() || size <= 0 {
1098 return 0;
1099 }
1100 let holder = unsafe { &*(user_data as *const FormatterHolder) };
1102 let s = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.func)(value))) {
1103 Ok(v) => v,
1104 Err(_) => {
1105 eprintln!("dear-implot: panic in axis formatter callback");
1106 std::process::abort();
1107 }
1108 };
1109 let bytes = s.as_bytes();
1110 let max = (size - 1).max(0) as usize;
1111 let n = bytes.len().min(max);
1112
1113 unsafe {
1117 std::ptr::copy_nonoverlapping(bytes.as_ptr(), buff as *mut u8, n);
1118 *buff.add(n) = 0;
1119 }
1120 n as std::os::raw::c_int
1121}
1122
1123struct TransformHolder {
1126 forward: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
1127 inverse: Box<dyn Fn(f64) -> f64 + Send + Sync + 'static>,
1128}
1129
1130#[must_use]
1131pub struct AxisTransformToken {
1132 _private: (),
1133}
1134
1135impl AxisTransformToken {
1136 fn new<FW, INV>(axis: sys::ImAxis, forward: FW, inverse: INV) -> Self
1137 where
1138 FW: Fn(f64) -> f64 + Send + Sync + 'static,
1139 INV: Fn(f64) -> f64 + Send + Sync + 'static,
1140 {
1141 let configured = with_plot_scope_storage(|storage| {
1142 let holder = Box::new(TransformHolder {
1143 forward: Box::new(forward),
1144 inverse: Box::new(inverse),
1145 });
1146 let user = &*holder as *const TransformHolder as *mut std::os::raw::c_void;
1147 storage.transforms.push(holder);
1148 unsafe {
1149 sys::ImPlot_SetupAxisScale_PlotTransform(
1150 axis as sys::ImAxis,
1151 Some(transform_forward_thunk),
1152 Some(transform_inverse_thunk),
1153 user,
1154 )
1155 }
1156 })
1157 .is_some();
1158
1159 debug_assert!(
1160 configured,
1161 "dear-implot: axis transform closure must be set within an active plot"
1162 );
1163
1164 Self { _private: () }
1165 }
1166}
1167
1168impl Drop for AxisTransformToken {
1169 fn drop(&mut self) {
1170 }
1172}
1173
1174unsafe extern "C" fn transform_forward_thunk(
1175 value: f64,
1176 user_data: *mut std::os::raw::c_void,
1177) -> f64 {
1178 if user_data.is_null() {
1179 return value;
1180 }
1181 let holder = unsafe { &*(user_data as *const TransformHolder) };
1182 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.forward)(value))) {
1183 Ok(v) => v,
1184 Err(_) => {
1185 eprintln!("dear-implot: panic in axis transform (forward) callback");
1186 std::process::abort();
1187 }
1188 }
1189}
1190
1191unsafe extern "C" fn transform_inverse_thunk(
1192 value: f64,
1193 user_data: *mut std::os::raw::c_void,
1194) -> f64 {
1195 if user_data.is_null() {
1196 return value;
1197 }
1198 let holder = unsafe { &*(user_data as *const TransformHolder) };
1199 match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| (holder.inverse)(value))) {
1200 Ok(v) => v,
1201 Err(_) => {
1202 eprintln!("dear-implot: panic in axis transform (inverse) callback");
1203 std::process::abort();
1204 }
1205 }
1206}
1207
1208pub struct PlotToken<'ui> {
1212 binding: PlotContextBinding,
1213 imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
1214 _scope: PlotScopeGuard,
1215 _lifetime: std::marker::PhantomData<&'ui ()>,
1216}
1217
1218impl<'ui> PlotToken<'ui> {
1219 pub(crate) fn new(
1221 binding: PlotContextBinding,
1222 imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
1223 ) -> Self {
1224 Self {
1225 binding,
1226 imgui_alive,
1227 _scope: PlotScopeGuard::new(),
1228 _lifetime: std::marker::PhantomData,
1229 }
1230 }
1231
1232 pub fn end(self) {
1237 }
1239}
1240
1241impl<'ui> Drop for PlotToken<'ui> {
1242 fn drop(&mut self) {
1243 if let Some(alive) = &self.imgui_alive {
1244 assert!(
1245 alive.is_alive(),
1246 "dear-implot: ImGui context has been dropped"
1247 );
1248 }
1249 self.binding.bind("dear-implot: PlotToken");
1250 unsafe {
1251 sys::ImPlot_EndPlot();
1252 }
1253 }
1254}
1255
1256#[cfg(test)]
1257mod tests {
1258 use super::{PlotContext, sys};
1259 use crate::{Axis, PlotCond, XAxis, YAxis};
1260 use dear_imgui_rs::{BackendFlags, Context};
1261 use std::sync::{Mutex, OnceLock};
1262
1263 fn test_guard() -> std::sync::MutexGuard<'static, ()> {
1264 static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
1265 GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
1266 }
1267
1268 fn prepare_imgui(imgui: &mut Context) {
1269 let io = imgui.io_mut();
1270 io.set_display_size([800.0, 600.0]);
1271 io.set_delta_time(1.0 / 60.0);
1272 io.set_backend_flags(io.backend_flags() | BackendFlags::RENDERER_HAS_TEXTURES);
1273 }
1274
1275 #[test]
1276 fn plot_ui_binds_own_context_before_calls() {
1277 let _guard = test_guard();
1278 let mut imgui = Context::create();
1279 prepare_imgui(&mut imgui);
1280 let plot_a = PlotContext::create(&imgui);
1281 let raw_a = plot_a.raw;
1282 let plot_b = PlotContext::create(&imgui);
1283 let raw_b = plot_b.raw;
1284
1285 {
1286 let ui = imgui.frame();
1287 let plot_ui = plot_a.get_plot_ui(&ui);
1288 unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
1289
1290 plot_ui.set_next_axes_to_fit();
1291
1292 assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_a);
1293 }
1294 let _ = imgui.render();
1295
1296 drop(plot_b);
1297 drop(plot_a);
1298 }
1299
1300 #[test]
1301 fn plot_token_binds_own_context_before_drop() {
1302 let _guard = test_guard();
1303 let mut imgui = Context::create();
1304 prepare_imgui(&mut imgui);
1305 let plot_a = PlotContext::create(&imgui);
1306 let raw_a = plot_a.raw;
1307 let plot_b = PlotContext::create(&imgui);
1308 let raw_b = plot_b.raw;
1309
1310 {
1311 let ui = imgui.frame();
1312 let plot_ui = plot_a.get_plot_ui(&ui);
1313 let token = plot_ui.begin_plot("token").expect("failed to begin plot");
1314
1315 unsafe { sys::ImPlot_SetCurrentContext(raw_b) };
1316 drop(token);
1317
1318 assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw_a);
1319 }
1320 let _ = imgui.render();
1321
1322 drop(plot_b);
1323 drop(plot_a);
1324 }
1325
1326 #[test]
1327 fn current_context_wrapper_is_non_owning() {
1328 let _guard = test_guard();
1329 let imgui = Context::create();
1330 let plot = PlotContext::create(&imgui);
1331 let raw = plot.raw;
1332
1333 let borrowed = unsafe { PlotContext::current() }.expect("expected current ImPlot context");
1334 drop(borrowed);
1335
1336 assert_eq!(unsafe { sys::ImPlot_GetCurrentContext() }, raw);
1337 plot.set_as_current();
1338
1339 drop(plot);
1340 }
1341
1342 #[test]
1343 #[should_panic(expected = "PlotUi::set_next_x_axis_limits() min must be finite")]
1344 fn set_next_axis_limits_rejects_non_finite_values_before_ffi() {
1345 let _guard = test_guard();
1346 let mut imgui = Context::create();
1347 prepare_imgui(&mut imgui);
1348 let plot = PlotContext::create(&imgui);
1349
1350 {
1351 let ui = imgui.frame();
1352 let plot_ui = plot.get_plot_ui(&ui);
1353 plot_ui.set_next_x_axis_limits(XAxis::X1, f64::NAN, 1.0, PlotCond::Once);
1354 }
1355 }
1356
1357 #[test]
1358 #[should_panic(expected = "PlotUi::setup_axis_zoom_constraints() min must be positive")]
1359 fn axis_zoom_constraints_reject_non_positive_min_before_ffi() {
1360 let _guard = test_guard();
1361 let mut imgui = Context::create();
1362 prepare_imgui(&mut imgui);
1363 let plot = PlotContext::create(&imgui);
1364
1365 {
1366 let ui = imgui.frame();
1367 let plot_ui = plot.get_plot_ui(&ui);
1368 let token = plot_ui
1369 .begin_plot("constraints")
1370 .expect("failed to begin plot");
1371 plot_ui.setup_axis_zoom_constraints(Axis::Y1, 0.0, 10.0);
1372 token.end();
1373 }
1374 }
1375
1376 #[test]
1377 fn typed_axis_apis_accept_valid_axes() {
1378 let _guard = test_guard();
1379 let mut imgui = Context::create();
1380 prepare_imgui(&mut imgui);
1381 let plot = PlotContext::create(&imgui);
1382
1383 {
1384 let ui = imgui.frame();
1385 let plot_ui = plot.get_plot_ui(&ui);
1386 plot_ui.set_next_axis_to_fit(Axis::X1);
1387
1388 let token = plot_ui
1389 .begin_plot("typed-axis")
1390 .expect("failed to begin plot");
1391 plot_ui.setup_x_axis(XAxis::X1, None, crate::AxisFlags::NONE);
1392 plot_ui.setup_y_axis(YAxis::Y1, None, crate::AxisFlags::NONE);
1393 let mut min = 0.0;
1394 let mut max = 1.0;
1395 plot_ui.setup_axis_links(Axis::Y1, Some(&mut min), Some(&mut max));
1396 plot_ui.setup_axis_limits_constraints(Axis::Y1, -10.0, 10.0);
1397 plot_ui.setup_axis_zoom_constraints(Axis::Y1, 0.1, 20.0);
1398 token.end();
1399 }
1400
1401 let _ = imgui.render();
1402 drop(plot);
1403 }
1404}