Skip to main content

dear_implot/
style.rs

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