Skip to main content

dear_implot/
style.rs

1// Style and theming for plots
2
3use crate::sys;
4use crate::{FloatFormat, PlotContext, PlotContextBinding, PlotUi};
5use dear_imgui_rs::{with_scratch_txt, with_scratch_txt_two};
6use std::borrow::Cow;
7use std::marker::PhantomData;
8use std::rc::Rc;
9
10use crate::Colormap;
11
12const DEFAULT_COLORMAP_SCALE_FORMAT: &str = "%g";
13const DEFAULT_COLORMAP_SLIDER_FORMAT: &str = "";
14
15/// Style variables that can be modified
16#[repr(i32)]
17#[derive(Copy, Clone, Debug, PartialEq, Eq)]
18pub enum StyleVar {
19    PlotDefaultSize = sys::ImPlotStyleVar_PlotDefaultSize as i32,
20    PlotMinSize = sys::ImPlotStyleVar_PlotMinSize as i32,
21    PlotBorderSize = sys::ImPlotStyleVar_PlotBorderSize as i32,
22    MinorAlpha = sys::ImPlotStyleVar_MinorAlpha as i32,
23    MajorTickLen = sys::ImPlotStyleVar_MajorTickLen as i32,
24    MinorTickLen = sys::ImPlotStyleVar_MinorTickLen as i32,
25    MajorTickSize = sys::ImPlotStyleVar_MajorTickSize as i32,
26    MinorTickSize = sys::ImPlotStyleVar_MinorTickSize as i32,
27    MajorGridSize = sys::ImPlotStyleVar_MajorGridSize as i32,
28    MinorGridSize = sys::ImPlotStyleVar_MinorGridSize as i32,
29    PlotPadding = sys::ImPlotStyleVar_PlotPadding as i32,
30    LabelPadding = sys::ImPlotStyleVar_LabelPadding as i32,
31    LegendPadding = sys::ImPlotStyleVar_LegendPadding as i32,
32    LegendInnerPadding = sys::ImPlotStyleVar_LegendInnerPadding as i32,
33    LegendSpacing = sys::ImPlotStyleVar_LegendSpacing as i32,
34    MousePosPadding = sys::ImPlotStyleVar_MousePosPadding as i32,
35    AnnotationPadding = sys::ImPlotStyleVar_AnnotationPadding as i32,
36    FitPadding = sys::ImPlotStyleVar_FitPadding as i32,
37    DigitalPadding = sys::ImPlotStyleVar_DigitalPadding as i32,
38    DigitalSpacing = sys::ImPlotStyleVar_DigitalSpacing as i32,
39}
40
41/// Token for managing style variable changes
42pub struct StyleVarToken<'ui> {
43    binding: PlotContextBinding,
44    was_popped: bool,
45    _lifetime: PhantomData<&'ui PlotUi<'ui>>,
46    _not_send_or_sync: PhantomData<Rc<()>>,
47}
48
49impl StyleVarToken<'_> {
50    /// Pop this style variable from the stack
51    pub fn pop(mut self) {
52        self.pop_inner();
53    }
54
55    fn pop_inner(&mut self) {
56        if self.was_popped {
57            panic!("Attempted to pop an ImPlot style var token twice.");
58        }
59        self.binding
60            .with_bound_context("dear-implot: StyleVarToken", || {
61                unsafe { sys::ImPlot_PopStyleVar(1) };
62            });
63        self.was_popped = true;
64    }
65}
66
67impl Drop for StyleVarToken<'_> {
68    fn drop(&mut self) {
69        if !self.was_popped {
70            let _ = self
71                .binding
72                .try_with_bound_context(|| unsafe { sys::ImPlot_PopStyleVar(1) });
73            self.was_popped = true;
74        }
75    }
76}
77
78/// Token for managing style color changes
79pub struct StyleColorToken<'ui> {
80    binding: PlotContextBinding,
81    was_popped: bool,
82    _lifetime: PhantomData<&'ui PlotUi<'ui>>,
83    _not_send_or_sync: PhantomData<Rc<()>>,
84}
85
86impl StyleColorToken<'_> {
87    /// Pop this style color from the stack
88    pub fn pop(mut self) {
89        self.pop_inner();
90    }
91
92    fn pop_inner(&mut self) {
93        if self.was_popped {
94            panic!("Attempted to pop an ImPlot style color token twice.");
95        }
96        self.binding
97            .with_bound_context("dear-implot: StyleColorToken", || {
98                unsafe { sys::ImPlot_PopStyleColor(1) };
99            });
100        self.was_popped = true;
101    }
102}
103
104impl Drop for StyleColorToken<'_> {
105    fn drop(&mut self) {
106        if !self.was_popped {
107            let _ = self
108                .binding
109                .try_with_bound_context(|| unsafe { sys::ImPlot_PopStyleColor(1) });
110            self.was_popped = true;
111        }
112    }
113}
114
115/// Runtime ImPlot colormap index.
116#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
117#[repr(transparent)]
118pub struct ColormapIndex(pub(crate) i32);
119
120impl ColormapIndex {
121    #[inline]
122    pub const fn new(index: usize) -> Option<Self> {
123        if index <= i32::MAX as usize {
124            Some(Self(index as i32))
125        } else {
126            None
127        }
128    }
129
130    #[inline]
131    pub const fn get(self) -> usize {
132        self.0 as usize
133    }
134
135    #[inline]
136    pub const fn raw(self) -> i32 {
137        self.0
138    }
139
140    #[inline]
141    pub(crate) const fn from_raw(raw: i32) -> Option<Self> {
142        if raw >= 0 { Some(Self(raw)) } else { None }
143    }
144}
145
146impl From<Colormap> for ColormapIndex {
147    #[inline]
148    fn from(value: Colormap) -> Self {
149        value.index()
150    }
151}
152
153impl From<usize> for ColormapIndex {
154    #[inline]
155    fn from(value: usize) -> Self {
156        Self::new(value).expect("colormap index exceeded ImPlot's i32 range")
157    }
158}
159
160/// Selected colormap for helpers that may use either the current style colormap or an explicit one.
161#[derive(Copy, Clone, Debug, PartialEq, Eq)]
162pub enum ColormapSelection {
163    Current,
164    Index(ColormapIndex),
165}
166
167impl ColormapSelection {
168    #[inline]
169    pub(crate) const fn raw(self) -> i32 {
170        match self {
171            Self::Current => crate::IMPLOT_AUTO,
172            Self::Index(index) => index.raw(),
173        }
174    }
175}
176
177impl From<Colormap> for ColormapSelection {
178    #[inline]
179    fn from(value: Colormap) -> Self {
180        Self::Index(value.index())
181    }
182}
183
184impl From<ColormapIndex> for ColormapSelection {
185    #[inline]
186    fn from(value: ColormapIndex) -> Self {
187        Self::Index(value)
188    }
189}
190
191impl From<Option<ColormapIndex>> for ColormapSelection {
192    #[inline]
193    fn from(value: Option<ColormapIndex>) -> Self {
194        value.map_or(Self::Current, Self::Index)
195    }
196}
197
198/// Zero-based color entry inside the active or selected colormap.
199#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
200#[repr(transparent)]
201pub struct ColormapColorIndex(i32);
202
203impl ColormapColorIndex {
204    #[inline]
205    pub const fn new(index: usize) -> Option<Self> {
206        Self::from_usize(index)
207    }
208
209    #[inline]
210    pub const fn from_usize(index: usize) -> Option<Self> {
211        if index <= i32::MAX as usize {
212            Some(Self(index as i32))
213        } else {
214            None
215        }
216    }
217
218    #[inline]
219    pub const fn get(self) -> usize {
220        self.0 as usize
221    }
222
223    #[inline]
224    pub const fn raw(self) -> i32 {
225        self.0
226    }
227}
228
229impl From<usize> for ColormapColorIndex {
230    #[inline]
231    fn from(value: usize) -> Self {
232        Self::new(value).expect("colormap color index exceeded ImPlot's i32 range")
233    }
234}
235
236/// Token for managing colormap changes.
237#[must_use]
238pub struct ColormapToken<'ui> {
239    binding: PlotContextBinding,
240    was_popped: bool,
241    _lifetime: PhantomData<&'ui PlotUi<'ui>>,
242    _not_send_or_sync: PhantomData<Rc<()>>,
243}
244
245impl ColormapToken<'_> {
246    /// Pop this colormap from the stack.
247    pub fn pop(mut self) {
248        self.pop_inner();
249    }
250
251    fn pop_inner(&mut self) {
252        if self.was_popped {
253            panic!("Attempted to pop an ImPlot colormap token twice.");
254        }
255        self.binding
256            .with_bound_context("dear-implot: ColormapToken", || {
257                unsafe { sys::ImPlot_PopColormap(1) };
258            });
259        self.was_popped = true;
260    }
261}
262
263impl Drop for ColormapToken<'_> {
264    fn drop(&mut self) {
265        if !self.was_popped {
266            let _ = self
267                .binding
268                .try_with_bound_context(|| unsafe { sys::ImPlot_PopColormap(1) });
269            self.was_popped = true;
270        }
271    }
272}
273
274/// One-shot array-backed item style overrides for the next plot submission.
275///
276/// This mirrors the new per-item array fields added to `ImPlotSpec` without storing
277/// borrowed pointers beyond the closure passed to [`with_next_plot_item_array_style`].
278#[derive(Debug, Clone, Default, PartialEq)]
279pub struct PlotItemArrayStyle<'a> {
280    line_colors: Option<Cow<'a, [u32]>>,
281    fill_colors: Option<Cow<'a, [u32]>>,
282    marker_sizes: Option<Cow<'a, [f32]>>,
283    marker_line_colors: Option<Cow<'a, [u32]>>,
284    marker_fill_colors: Option<Cow<'a, [u32]>>,
285}
286
287impl<'a> PlotItemArrayStyle<'a> {
288    /// Create an empty array-style override.
289    pub fn new() -> Self {
290        Self::default()
291    }
292
293    /// Override per-index line colors using Dear ImGui packed colors (`ImU32` / ABGR).
294    pub fn with_line_colors(mut self, colors: &'a [u32]) -> Self {
295        self.line_colors = Some(Cow::Borrowed(colors));
296        self
297    }
298
299    /// Override per-index fill colors using Dear ImGui packed colors (`ImU32` / ABGR).
300    pub fn with_fill_colors(mut self, colors: &'a [u32]) -> Self {
301        self.fill_colors = Some(Cow::Borrowed(colors));
302        self
303    }
304
305    /// Override per-index marker sizes in pixels.
306    pub fn with_marker_sizes(mut self, sizes: &'a [f32]) -> Self {
307        self.marker_sizes = Some(Cow::Borrowed(sizes));
308        self
309    }
310
311    /// Override per-index marker outline colors using Dear ImGui packed colors (`ImU32` / ABGR).
312    pub fn with_marker_line_colors(mut self, colors: &'a [u32]) -> Self {
313        self.marker_line_colors = Some(Cow::Borrowed(colors));
314        self
315    }
316
317    /// Override per-index marker fill colors using Dear ImGui packed colors (`ImU32` / ABGR).
318    pub fn with_marker_fill_colors(mut self, colors: &'a [u32]) -> Self {
319        self.marker_fill_colors = Some(Cow::Borrowed(colors));
320        self
321    }
322
323    fn apply_to_spec(&self, spec: &mut sys::ImPlotSpec_c) {
324        spec.LineColors = self
325            .line_colors
326            .as_ref()
327            .map_or(std::ptr::null_mut(), |colors| colors.as_ptr() as *mut _);
328        spec.FillColors = self
329            .fill_colors
330            .as_ref()
331            .map_or(std::ptr::null_mut(), |colors| colors.as_ptr() as *mut _);
332        spec.MarkerSizes = self
333            .marker_sizes
334            .as_ref()
335            .map_or(std::ptr::null_mut(), |sizes| sizes.as_ptr() as *mut _);
336        spec.MarkerLineColors = self
337            .marker_line_colors
338            .as_ref()
339            .map_or(std::ptr::null_mut(), |colors| colors.as_ptr() as *mut _);
340        spec.MarkerFillColors = self
341            .marker_fill_colors
342            .as_ref()
343            .map_or(std::ptr::null_mut(), |colors| colors.as_ptr() as *mut _);
344    }
345}
346
347struct ScopedNextPlotItemArrayStyle {
348    previous: Option<sys::ImPlotSpec_c>,
349    active: bool,
350}
351
352impl ScopedNextPlotItemArrayStyle {
353    fn restore_if_unused(&mut self) {
354        if !self.active {
355            return;
356        }
357
358        if crate::plots::take_next_plot_spec().is_some() {
359            crate::plots::set_next_plot_spec(self.previous.take());
360        }
361        self.active = false;
362    }
363}
364
365impl Drop for ScopedNextPlotItemArrayStyle {
366    fn drop(&mut self) {
367        self.restore_if_unused();
368    }
369}
370
371fn with_scoped_next_plot_item_array_style<'a, R>(
372    style: PlotItemArrayStyle<'a>,
373    f: impl FnOnce() -> R,
374) -> R {
375    let previous = crate::plots::take_next_plot_spec();
376    let mut spec = previous.unwrap_or_else(crate::plots::default_plot_spec);
377    style.apply_to_spec(&mut spec);
378    crate::plots::set_next_plot_spec(Some(spec));
379
380    let mut guard = ScopedNextPlotItemArrayStyle {
381        previous,
382        active: true,
383    };
384    let out = f();
385    guard.restore_if_unused();
386    out
387}
388
389impl<'ui> PlotUi<'ui> {
390    /// Apply array-backed item styling to the next plot submission executed inside `f`.
391    ///
392    /// This is closure-scoped so borrowed slices stay valid for the entire next plot
393    /// call and are restored even if `f` panics before submitting an item.
394    ///
395    /// # Safety
396    ///
397    /// Every non-empty style array must contain enough elements for every index the submitted plot
398    /// may read. Upstream accepts only pointers and carries no array lengths, so this cannot be
399    /// validated by the wrapper.
400    pub unsafe fn with_next_plot_item_array_style<'a, R>(
401        &self,
402        style: PlotItemArrayStyle<'a>,
403        f: impl FnOnce(&PlotUi<'ui>) -> R,
404    ) -> R {
405        self.with_bound_context(|| with_scoped_next_plot_item_array_style(style, || f(self)))
406    }
407
408    /// Push a float style variable to this ImPlot context's stack.
409    pub fn push_style_var_f32(&self, var: StyleVar, value: f32) -> StyleVarToken<'_> {
410        self.with_bound_context(|| {
411            unsafe {
412                sys::ImPlot_PushStyleVar_Float(var as sys::ImPlotStyleVar, value);
413            }
414            StyleVarToken {
415                binding: self.context.binding(),
416                was_popped: false,
417                _lifetime: PhantomData,
418                _not_send_or_sync: PhantomData,
419            }
420        })
421    }
422
423    /// Push a Vec2 style variable to this ImPlot context's stack.
424    pub fn push_style_var_vec2(&self, var: StyleVar, value: [f32; 2]) -> StyleVarToken<'_> {
425        self.with_bound_context(|| {
426            unsafe {
427                sys::ImPlot_PushStyleVar_Vec2(
428                    var as sys::ImPlotStyleVar,
429                    sys::ImVec2_c {
430                        x: value[0],
431                        y: value[1],
432                    },
433                );
434            }
435            StyleVarToken {
436                binding: self.context.binding(),
437                was_popped: false,
438                _lifetime: PhantomData,
439                _not_send_or_sync: PhantomData,
440            }
441        })
442    }
443
444    /// Push a style color to this ImPlot context's stack.
445    pub fn push_style_color(
446        &self,
447        element: crate::PlotColorElement,
448        color: [f32; 4],
449    ) -> StyleColorToken<'_> {
450        self.with_bound_context(|| {
451            unsafe {
452                // Convert color to ImU32 format (RGBA).
453                let r = (color[0] * 255.0) as u32;
454                let g = (color[1] * 255.0) as u32;
455                let b = (color[2] * 255.0) as u32;
456                let a = (color[3] * 255.0) as u32;
457                let color_u32 = (a << 24) | (b << 16) | (g << 8) | r;
458
459                sys::ImPlot_PushStyleColor_U32(element as sys::ImPlotCol, color_u32);
460            }
461            StyleColorToken {
462                binding: self.context.binding(),
463                was_popped: false,
464                _lifetime: PhantomData,
465                _not_send_or_sync: PhantomData,
466            }
467        })
468    }
469
470    /// Push a colormap to this ImPlot context's stack.
471    pub fn push_colormap(&self, cmap: impl Into<ColormapIndex>) -> ColormapToken<'_> {
472        self.with_bound_context(|| {
473            unsafe {
474                sys::ImPlot_PushColormap_PlotColormap(cmap.into().raw());
475            }
476            ColormapToken {
477                binding: self.context.binding(),
478                was_popped: false,
479                _lifetime: PhantomData,
480                _not_send_or_sync: PhantomData,
481            }
482        })
483    }
484
485    /// Push a colormap by name to this ImPlot context's stack.
486    pub fn push_colormap_name(&self, name: &str) -> ColormapToken<'_> {
487        assert!(!name.contains('\0'), "colormap name contained NUL");
488        self.with_bound_context(|| {
489            with_scratch_txt(name, |ptr| unsafe { sys::ImPlot_PushColormap_Str(ptr) });
490            ColormapToken {
491                binding: self.context.binding(),
492                was_popped: false,
493                _lifetime: PhantomData,
494                _not_send_or_sync: PhantomData,
495            }
496        })
497    }
498}
499
500fn colormap_count_from_i32(raw: i32, caller: &str) -> usize {
501    assert!(raw >= 0, "{caller} returned a negative colormap count");
502    usize::try_from(raw).expect("non-negative colormap count must fit usize")
503}
504
505fn assert_colormap_sample_t(t: f32) {
506    assert!(
507        (0.0..=1.0).contains(&t),
508        "sample_colormap t must be between 0 and 1"
509    );
510}
511
512impl PlotContext {
513    #[inline]
514    fn with_bound_style<R>(&self, caller: &str, f: impl FnOnce() -> R) -> R {
515        self.binding().with_bound_context(caller, || f())
516    }
517
518    /// Add a custom colormap from colors. The colors are copied by ImPlot.
519    pub fn add_colormap(
520        &self,
521        name: &str,
522        colors: &[[f32; 4]],
523        qualitative: bool,
524    ) -> ColormapIndex {
525        assert!(!name.contains('\0'), "colormap name contained NUL");
526        assert!(
527            colors.len() > 1,
528            "colormap must contain at least two colors"
529        );
530        assert!(
531            colors
532                .iter()
533                .flatten()
534                .all(|component| component.is_finite()),
535            "colormap colors must be finite"
536        );
537        let count = i32::try_from(colors.len()).expect("colormap contained too many colors");
538        let colors: Vec<sys::ImVec4> = colors
539            .iter()
540            .map(|color| sys::ImVec4 {
541                x: color[0],
542                y: color[1],
543                z: color[2],
544                w: color[3],
545            })
546            .collect();
547        let index = self.with_bound_style("dear-implot: PlotContext::add_colormap()", || {
548            with_scratch_txt(name, |ptr| unsafe {
549                sys::ImPlot_AddColormap_Vec4Ptr(ptr, colors.as_ptr(), count, qualitative)
550            })
551        });
552        ColormapIndex::from_raw(index).expect("ImPlot returned a negative colormap index")
553    }
554
555    /// Return the number of available colormaps.
556    pub fn colormap_count(&self) -> usize {
557        self.with_bound_style("dear-implot: PlotContext::colormap_count()", || {
558            colormap_count_from_i32(
559                unsafe { sys::ImPlot_GetColormapCount() },
560                "PlotContext::colormap_count()",
561            )
562        })
563    }
564
565    /// Return a colormap name, or an empty string if the index is invalid for this context.
566    pub fn colormap_name(&self, index: impl Into<ColormapIndex>) -> String {
567        self.with_bound_style("dear-implot: PlotContext::colormap_name()", || unsafe {
568            let p = sys::ImPlot_GetColormapName(index.into().raw());
569            if p.is_null() {
570                return String::new();
571            }
572            std::ffi::CStr::from_ptr(p).to_string_lossy().into_owned()
573        })
574    }
575
576    /// Look up a colormap index by its name.
577    pub fn colormap_index_by_name(&self, name: &str) -> Option<ColormapIndex> {
578        if name.contains('\0') {
579            return None;
580        }
581        let index = self
582            .with_bound_style("dear-implot: PlotContext::colormap_index_by_name()", || {
583                with_scratch_txt(name, |ptr| unsafe { sys::ImPlot_GetColormapIndex(ptr) })
584            });
585        ColormapIndex::from_raw(index)
586    }
587
588    /// Return the number of color entries in a colormap.
589    pub fn colormap_size(&self, index: impl Into<ColormapIndex>) -> usize {
590        self.with_bound_style("dear-implot: PlotContext::colormap_size()", || {
591            colormap_count_from_i32(
592                unsafe { sys::ImPlot_GetColormapSize(index.into().raw()) },
593                "PlotContext::colormap_size()",
594            )
595        })
596    }
597
598    /// Return the default colormap stored in this ImPlot context's style.
599    pub fn style_colormap_index(&self) -> Option<ColormapIndex> {
600        self.with_bound_style(
601            "dear-implot: PlotContext::style_colormap_index()",
602            || unsafe {
603                let style = sys::ImPlot_GetStyle();
604                if style.is_null() {
605                    return None;
606                }
607                ColormapIndex::from_raw((*style).Colormap)
608            },
609        )
610    }
611
612    /// Return this context's default colormap name.
613    pub fn style_colormap_name(&self) -> Option<String> {
614        let idx = self.style_colormap_index()?;
615        let count = self.colormap_count();
616        if idx.get() >= count {
617            return None;
618        }
619        Some(self.colormap_name(idx))
620    }
621
622    /// Permanently set the default colormap used by this ImPlot context.
623    pub fn set_style_colormap(&self, index: impl Into<ColormapIndex>) {
624        self.with_bound_style(
625            "dear-implot: PlotContext::set_style_colormap()",
626            || unsafe {
627                let style = sys::ImPlot_GetStyle();
628                if !style.is_null() {
629                    let count = colormap_count_from_i32(
630                        sys::ImPlot_GetColormapCount(),
631                        "PlotContext::set_style_colormap()",
632                    );
633                    if count > 0 {
634                        let index = index.into().get();
635                        let idx = index.min(count - 1);
636                        (*style).Colormap = ColormapIndex::from(idx).raw();
637                    }
638                }
639            },
640        )
641    }
642
643    /// Permanently set the default colormap by name. Invalid names are ignored.
644    pub fn set_style_colormap_by_name(&self, name: &str) {
645        if let Some(idx) = self.colormap_index_by_name(name) {
646            self.set_style_colormap(idx);
647        }
648    }
649
650    /// Return a color from this context's active colormap.
651    pub fn colormap_color(&self, index: ColormapColorIndex) -> [f32; 4] {
652        self.with_bound_style("dear-implot: PlotContext::colormap_color()", || unsafe {
653            let out = sys::ImPlot_GetColormapColor(index.raw(), crate::IMPLOT_AUTO);
654            [out.x, out.y, out.z, out.w]
655        })
656    }
657
658    /// Return a color from a selected colormap.
659    pub fn colormap_color_from(
660        &self,
661        index: ColormapColorIndex,
662        cmap: impl Into<ColormapIndex>,
663    ) -> [f32; 4] {
664        self.with_bound_style(
665            "dear-implot: PlotContext::colormap_color_from()",
666            || unsafe {
667                let out = sys::ImPlot_GetColormapColor(index.raw(), cmap.into().raw());
668                [out.x, out.y, out.z, out.w]
669            },
670        )
671    }
672
673    /// Sample this context's active colormap at `t` in `[0, 1]`.
674    pub fn sample_colormap(&self, t: f32) -> [f32; 4] {
675        assert_colormap_sample_t(t);
676        self.with_bound_style("dear-implot: PlotContext::sample_colormap()", || unsafe {
677            let out = sys::ImPlot_SampleColormap(t, crate::IMPLOT_AUTO);
678            [out.x, out.y, out.z, out.w]
679        })
680    }
681
682    /// Sample a selected colormap at `t` in `[0, 1]`.
683    pub fn sample_colormap_from(&self, t: f32, cmap: impl Into<ColormapSelection>) -> [f32; 4] {
684        assert_colormap_sample_t(t);
685        self.with_bound_style(
686            "dear-implot: PlotContext::sample_colormap_from()",
687            || unsafe {
688                let out = sys::ImPlot_SampleColormap(t, cmap.into().raw());
689                [out.x, out.y, out.z, out.w]
690            },
691        )
692    }
693
694    /// Return the next color from this context's current colormap and advance its color cursor.
695    pub fn next_colormap_color(&self) -> [f32; 4] {
696        self.with_bound_style(
697            "dear-implot: PlotContext::next_colormap_color()",
698            || unsafe {
699                let out = sys::ImPlot_NextColormapColor();
700                [out.x, out.y, out.z, out.w]
701            },
702        )
703    }
704
705    /// Map this context's input scheme to ImPlot defaults.
706    pub fn map_input_default(&self) {
707        self.with_bound_style("dear-implot: PlotContext::map_input_default()", || unsafe {
708            sys::ImPlot_MapInputDefault(sys::ImPlot_GetInputMap())
709        })
710    }
711
712    /// Map this context's input scheme to ImPlot's reversed scheme.
713    pub fn map_input_reverse(&self) {
714        self.with_bound_style("dear-implot: PlotContext::map_input_reverse()", || unsafe {
715            sys::ImPlot_MapInputReverse(sys::ImPlot_GetInputMap())
716        })
717    }
718}
719
720impl PlotUi<'_> {
721    /// Show the ImPlot style editor window for this context.
722    pub fn show_style_editor(&self) {
723        self.with_bound_context(|| unsafe { sys::ImPlot_ShowStyleEditor(std::ptr::null_mut()) })
724    }
725
726    /// Show the ImPlot style selector combo; returns true if selection changed.
727    pub fn show_style_selector(&self, label: &str) -> bool {
728        let label = if label.contains('\0') { "" } else { label };
729        self.with_bound_context(|| {
730            with_scratch_txt(label, |ptr| unsafe { sys::ImPlot_ShowStyleSelector(ptr) })
731        })
732    }
733
734    /// Show the ImPlot colormap selector combo; returns true if selection changed.
735    pub fn show_colormap_selector(&self, label: &str) -> bool {
736        let label = if label.contains('\0') { "" } else { label };
737        self.with_bound_context(|| {
738            with_scratch_txt(label, |ptr| unsafe {
739                sys::ImPlot_ShowColormapSelector(ptr)
740            })
741        })
742    }
743
744    /// Show the ImPlot input-map selector combo; returns true if selection changed.
745    pub fn show_input_map_selector(&self, label: &str) -> bool {
746        let label = if label.contains('\0') { "" } else { label };
747        self.with_bound_context(|| {
748            with_scratch_txt(label, |ptr| unsafe {
749                sys::ImPlot_ShowInputMapSelector(ptr)
750            })
751        })
752    }
753
754    /// Draw a colormap scale widget.
755    pub fn colormap_scale(
756        &self,
757        label: &str,
758        scale_min: f64,
759        scale_max: f64,
760        height: f32,
761        cmap: impl Into<ColormapSelection>,
762    ) {
763        self.colormap_scale_impl(
764            label,
765            scale_min,
766            scale_max,
767            height,
768            DEFAULT_COLORMAP_SCALE_FORMAT,
769            cmap.into(),
770        );
771    }
772
773    /// Draw a colormap scale with a validated tick format.
774    pub fn colormap_scale_with_format(
775        &self,
776        label: &str,
777        scale_min: f64,
778        scale_max: f64,
779        height: f32,
780        format: &FloatFormat<'_>,
781        cmap: impl Into<ColormapSelection>,
782    ) {
783        self.colormap_scale_impl(
784            label,
785            scale_min,
786            scale_max,
787            height,
788            format.as_str(),
789            cmap.into(),
790        );
791    }
792
793    fn colormap_scale_impl(
794        &self,
795        label: &str,
796        scale_min: f64,
797        scale_max: f64,
798        height: f32,
799        format: &str,
800        cmap: ColormapSelection,
801    ) {
802        assert!(
803            scale_min.is_finite(),
804            "colormap_scale scale_min must be finite"
805        );
806        assert!(
807            scale_max.is_finite(),
808            "colormap_scale scale_max must be finite"
809        );
810        assert!(height.is_finite(), "colormap_scale height must be finite");
811        let label = if label.contains('\0') { "" } else { label };
812        let size = sys::ImVec2_c { x: 0.0, y: height };
813        let flags = sys::ImPlotColormapScaleFlags_None as sys::ImPlotColormapScaleFlags;
814        let cmap = cmap.raw();
815        self.with_bound_context(|| {
816            with_scratch_txt_two(label, format, |label_ptr, format_ptr| unsafe {
817                sys::ImPlot_ColormapScale(
818                    label_ptr, scale_min, scale_max, size, format_ptr, flags, cmap,
819                )
820            })
821        })
822    }
823
824    /// Draw a colormap slider with ImPlot's default value format.
825    pub fn colormap_slider(
826        &self,
827        label: &str,
828        t: &mut f32,
829        out_color: Option<&mut [f32; 4]>,
830        cmap: impl Into<ColormapSelection>,
831    ) -> bool {
832        self.colormap_slider_impl(
833            label,
834            t,
835            out_color,
836            DEFAULT_COLORMAP_SLIDER_FORMAT,
837            cmap.into(),
838        )
839    }
840
841    /// Draw a colormap slider with a validated value format.
842    pub fn colormap_slider_with_format(
843        &self,
844        label: &str,
845        t: &mut f32,
846        out_color: Option<&mut [f32; 4]>,
847        format: &FloatFormat<'_>,
848        cmap: impl Into<ColormapSelection>,
849    ) -> bool {
850        self.colormap_slider_impl(label, t, out_color, format.as_str(), cmap.into())
851    }
852
853    fn colormap_slider_impl(
854        &self,
855        label: &str,
856        t: &mut f32,
857        out_color: Option<&mut [f32; 4]>,
858        format: &str,
859        cmap: ColormapSelection,
860    ) -> bool {
861        assert!(t.is_finite(), "colormap_slider t must be finite");
862        let label = if label.contains('\0') { "" } else { label };
863        let cmap = cmap.raw();
864        let mut out = sys::ImVec4 {
865            x: 0.0,
866            y: 0.0,
867            z: 0.0,
868            w: 0.0,
869        };
870        let out_ptr = if out_color.is_some() {
871            &mut out as *mut sys::ImVec4
872        } else {
873            std::ptr::null_mut()
874        };
875
876        self.with_bound_context(|| {
877            let changed = with_scratch_txt_two(label, format, |label_ptr, format_ptr| unsafe {
878                sys::ImPlot_ColormapSlider(label_ptr, t as *mut f32, out_ptr, format_ptr, cmap)
879            });
880
881            if let Some(out_color) = out_color {
882                *out_color = [out.x, out.y, out.z, out.w];
883            }
884            changed
885        })
886    }
887
888    /// Draw a colormap picker button; returns true if clicked.
889    pub fn colormap_button(
890        &self,
891        label: &str,
892        size: [f32; 2],
893        cmap: impl Into<ColormapSelection>,
894    ) -> bool {
895        assert!(
896            size[0].is_finite() && size[1].is_finite(),
897            "colormap_button size must be finite"
898        );
899        let label = if label.contains('\0') { "" } else { label };
900        let sz = sys::ImVec2_c {
901            x: size[0],
902            y: size[1],
903        };
904        let cmap = cmap.into().raw();
905        self.with_bound_context(|| {
906            with_scratch_txt(label, |ptr| unsafe {
907                sys::ImPlot_ColormapButton(ptr, sz, cmap)
908            })
909        })
910    }
911}
912
913#[cfg(test)]
914mod tests {
915    use super::{
916        Colormap, ColormapColorIndex, ColormapIndex, ColormapSelection, PlotItemArrayStyle,
917        with_scoped_next_plot_item_array_style,
918    };
919    use crate::plots::{
920        PlotDataLayout, PlotDataOffset, PlotDataStride, set_next_plot_spec, take_next_plot_spec,
921    };
922
923    #[test]
924    fn colormap_indices_reject_negative_values() {
925        assert_eq!(ColormapIndex::from_raw(-1), None);
926        assert_eq!(ColormapIndex::new(0).map(ColormapIndex::raw), Some(0));
927        assert_eq!(ColormapIndex::new(0).map(ColormapIndex::get), Some(0));
928        assert_eq!(ColormapIndex::new(i32::MAX as usize + 1), None);
929        assert_eq!(
930            ColormapIndex::from(Colormap::Viridis).raw(),
931            crate::sys::ImPlotColormap_Viridis
932        );
933        assert_eq!(ColormapSelection::Current.raw(), crate::IMPLOT_AUTO);
934        assert_eq!(
935            ColormapSelection::from(Colormap::Viridis).raw(),
936            crate::sys::ImPlotColormap_Viridis
937        );
938
939        assert_eq!(
940            ColormapColorIndex::new(0).map(ColormapColorIndex::get),
941            Some(0)
942        );
943        assert_eq!(
944            ColormapColorIndex::from_usize(i32::MAX as usize).map(ColormapColorIndex::raw),
945            Some(i32::MAX)
946        );
947        assert_eq!(ColormapColorIndex::from_usize(i32::MAX as usize + 1), None);
948    }
949
950    #[test]
951    #[should_panic(expected = "test returned a negative colormap count")]
952    fn colormap_count_conversion_rejects_negative_ffi_values() {
953        let _ = super::colormap_count_from_i32(-1, "test");
954    }
955
956    #[test]
957    #[should_panic(expected = "sample_colormap t must be between 0 and 1")]
958    fn sample_colormap_rejects_out_of_range_t_before_ffi() {
959        super::assert_colormap_sample_t(-0.1);
960    }
961
962    #[test]
963    fn next_plot_item_array_style_is_consumed_by_next_spec() {
964        let line_colors = [0x01020304u32, 0x05060708];
965        let fill_colors = [0x11121314u32];
966        let marker_sizes = [2.0f32, 4.0, 8.0];
967
968        with_scoped_next_plot_item_array_style(
969            PlotItemArrayStyle::new()
970                .with_line_colors(&line_colors)
971                .with_fill_colors(&fill_colors)
972                .with_marker_sizes(&marker_sizes),
973            || {
974                let layout =
975                    PlotDataLayout::new(PlotDataOffset::samples(3), PlotDataStride::bytes(16));
976                let spec = crate::plots::plot_spec_from(7, layout);
977                assert_eq!(spec.Flags, 7);
978                assert_eq!(spec.Offset, 3);
979                assert_eq!(spec.Stride, 16);
980                assert_eq!(spec.LineColors, line_colors.as_ptr() as *mut _);
981                assert_eq!(spec.FillColors, fill_colors.as_ptr() as *mut _);
982                assert_eq!(spec.MarkerSizes, marker_sizes.as_ptr() as *mut _);
983            },
984        );
985
986        let spec = crate::plots::plot_spec_from(0, PlotDataLayout::DEFAULT);
987        assert!(spec.LineColors.is_null());
988        assert!(spec.FillColors.is_null());
989        assert!(spec.MarkerSizes.is_null());
990    }
991
992    #[test]
993    fn next_plot_item_array_style_is_restored_if_unused() {
994        let line_colors = [0xAABBCCDDu32];
995
996        with_scoped_next_plot_item_array_style(
997            PlotItemArrayStyle::new().with_line_colors(&line_colors),
998            || {},
999        );
1000
1001        let spec = crate::plots::plot_spec_from(0, PlotDataLayout::DEFAULT);
1002        assert!(spec.LineColors.is_null());
1003    }
1004
1005    #[test]
1006    fn next_plot_item_array_style_is_restored_if_closure_panics() {
1007        set_next_plot_spec(None);
1008        let line_colors = [0xAABBCCDDu32];
1009
1010        let result = std::panic::catch_unwind(|| {
1011            with_scoped_next_plot_item_array_style(
1012                PlotItemArrayStyle::new().with_line_colors(&line_colors),
1013                || panic!("boom"),
1014            );
1015        });
1016
1017        assert!(result.is_err());
1018        assert!(take_next_plot_spec().is_none());
1019    }
1020}