dear_implot/
numeric_format.rs1use std::borrow::Cow;
2use std::fmt;
3use std::str::FromStr;
4
5use dear_imgui_rs::{NumericFormat, NumericFormatError};
6
7#[derive(Clone, Debug, PartialEq)]
14pub struct FloatFormat<'a>(NumericFormat<'a, f64>);
15
16impl<'a> FloatFormat<'a> {
17 pub fn new(storage: impl Into<Cow<'a, str>>) -> Result<Self, FloatFormatError> {
19 let format = NumericFormat::new(storage)?;
20 if let Some(byte_offset) = format.as_str().bytes().position(|byte| !byte.is_ascii()) {
21 return Err(FloatFormatError::NonAscii { byte_offset });
22 }
23 Ok(Self(format))
24 }
25
26 pub fn as_str(&self) -> &str {
28 self.0.as_str()
29 }
30
31 pub fn borrowed(&self) -> FloatFormat<'_> {
33 FloatFormat(self.0.borrowed())
34 }
35
36 pub fn into_owned(self) -> FloatFormat<'static> {
38 FloatFormat(self.0.into_owned())
39 }
40
41 pub fn into_numeric_format(self) -> NumericFormat<'a, f64> {
43 self.0
44 }
45}
46
47impl AsRef<str> for FloatFormat<'_> {
48 fn as_ref(&self) -> &str {
49 self.as_str()
50 }
51}
52
53impl<'a> TryFrom<&'a str> for FloatFormat<'a> {
54 type Error = FloatFormatError;
55
56 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
57 Self::new(value)
58 }
59}
60
61impl TryFrom<String> for FloatFormat<'static> {
62 type Error = FloatFormatError;
63
64 fn try_from(value: String) -> Result<Self, Self::Error> {
65 Self::new(value)
66 }
67}
68
69impl FromStr for FloatFormat<'static> {
70 type Err = FloatFormatError;
71
72 fn from_str(value: &str) -> Result<Self, Self::Err> {
73 Self::new(value.to_owned())
74 }
75}
76
77#[derive(Clone, Debug, Eq, PartialEq)]
79#[non_exhaustive]
80pub enum FloatFormatError {
81 Numeric(NumericFormatError),
83 NonAscii { byte_offset: usize },
85}
86
87impl fmt::Display for FloatFormatError {
88 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
89 match self {
90 Self::Numeric(error) => error.fmt(formatter),
91 Self::NonAscii { byte_offset } => write!(
92 formatter,
93 "ImPlot numeric formats must be ASCII; non-ASCII text starts at byte {byte_offset}"
94 ),
95 }
96 }
97}
98
99impl std::error::Error for FloatFormatError {
100 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101 match self {
102 Self::Numeric(error) => Some(error),
103 Self::NonAscii { .. } => None,
104 }
105 }
106}
107
108impl From<NumericFormatError> for FloatFormatError {
109 fn from(error: NumericFormatError) -> Self {
110 Self::Numeric(error)
111 }
112}
113
114#[derive(Clone, Debug, PartialEq)]
116pub struct AxisFormat<'a>(FloatFormat<'a>);
117
118impl<'a> AxisFormat<'a> {
119 pub const MAX_LEN: usize = 15;
121
122 pub fn new(storage: impl Into<Cow<'a, str>>) -> Result<Self, AxisFormatError> {
124 Self::try_from(FloatFormat::new(storage)?)
125 }
126
127 pub fn as_str(&self) -> &str {
129 self.0.as_str()
130 }
131
132 pub fn borrowed(&self) -> AxisFormat<'_> {
134 AxisFormat(self.0.borrowed())
135 }
136
137 pub fn into_owned(self) -> AxisFormat<'static> {
139 AxisFormat(self.0.into_owned())
140 }
141
142 pub fn into_float_format(self) -> FloatFormat<'a> {
144 self.0
145 }
146}
147
148impl<'a> TryFrom<FloatFormat<'a>> for AxisFormat<'a> {
149 type Error = AxisFormatError;
150
151 fn try_from(format: FloatFormat<'a>) -> Result<Self, Self::Error> {
152 let length = format.as_str().len();
153 if length > Self::MAX_LEN {
154 return Err(AxisFormatError::TooLong {
155 length,
156 maximum: Self::MAX_LEN,
157 });
158 }
159 Ok(Self(format))
160 }
161}
162
163impl AsRef<str> for AxisFormat<'_> {
164 fn as_ref(&self) -> &str {
165 self.as_str()
166 }
167}
168
169impl<'a> TryFrom<&'a str> for AxisFormat<'a> {
170 type Error = AxisFormatError;
171
172 fn try_from(value: &'a str) -> Result<Self, Self::Error> {
173 Self::new(value)
174 }
175}
176
177impl TryFrom<String> for AxisFormat<'static> {
178 type Error = AxisFormatError;
179
180 fn try_from(value: String) -> Result<Self, Self::Error> {
181 Self::new(value)
182 }
183}
184
185impl FromStr for AxisFormat<'static> {
186 type Err = AxisFormatError;
187
188 fn from_str(value: &str) -> Result<Self, Self::Err> {
189 Self::new(value.to_owned())
190 }
191}
192
193#[derive(Clone, Debug, Eq, PartialEq)]
195#[non_exhaustive]
196pub enum AxisFormatError {
197 Format(FloatFormatError),
199 TooLong { length: usize, maximum: usize },
201}
202
203impl fmt::Display for AxisFormatError {
204 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
205 match self {
206 Self::Format(error) => error.fmt(formatter),
207 Self::TooLong { length, maximum } => write!(
208 formatter,
209 "axis format is {length} bytes; ImPlot supports at most {maximum}"
210 ),
211 }
212 }
213}
214
215impl std::error::Error for AxisFormatError {
216 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
217 match self {
218 Self::Format(error) => Some(error),
219 Self::TooLong { .. } => None,
220 }
221 }
222}
223
224impl From<FloatFormatError> for AxisFormatError {
225 fn from(error: FloatFormatError) -> Self {
226 Self::Format(error)
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 #[test]
235 fn axis_formats_validate_numeric_type_and_storage_length() {
236 assert_eq!(AxisFormat::new("%.3f").unwrap().as_str(), "%.3f");
237 assert!(matches!(
238 AxisFormat::new("%s"),
239 Err(AxisFormatError::Format(_))
240 ));
241 assert!(matches!(
242 FloatFormat::new("%.2f °C"),
243 Err(FloatFormatError::NonAscii { .. })
244 ));
245
246 let maximum = format!("{}%.1f", "x".repeat(11));
247 assert_eq!(maximum.len(), AxisFormat::MAX_LEN);
248 assert!(AxisFormat::new(maximum).is_ok());
249
250 let too_long = format!("{}%.1f", "x".repeat(12));
251 assert!(matches!(
252 AxisFormat::new(too_long),
253 Err(AxisFormatError::TooLong { length: 16, .. })
254 ));
255 }
256}