custom_display/display.rs
1// SPDX-FileCopyrightText: 2026 Marissa (cuddle puddle) <dev@princess.lgbt>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5use std::borrow::Borrow;
6use std::fmt::{self, Alignment, Display, Formatter};
7
8use super::PrecisionBehavior;
9
10/**
11 * A custom format, roughly equivalent to [`Display`] except that it is not
12 * restricted by [coherence].
13 *
14 * [`Displayable`]s that use this trait (created by calling [`display()`] or
15 * [`into_display()`]) can automatically handle [width], [fill and alignment],
16 * as well as [precision] if [`precision_behavior()`] returns [`AutoTruncate`]
17 * (the behavior specified for non-numeric types).
18 *
19 * # Implementation Requirements
20 *
21 * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
22 * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
23 * "OPTIONAL" in this document are to be interpreted as described in
24 * RFC 2119.
25 *
26 * Implementations MUST document how they handle [sign], [precision], [width],
27 * [fill and alignment].
28 *
29 * Most implementations will be unit sructs or type-parameterized tuple structs
30 * containing only [`PhantomData`].
31 *
32 * [coherence]: https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence
33 * [`display()`]: Self::display()
34 * [`into_display()`]: Self::into_display()
35 * [width]: std::fmt#width
36 * [fill and alignment]: std::fmt#fillalignment
37 * [precision]: std::fmt#precision
38 * [`precision_behavior()`]: Self::precision_behavior
39 * [`AutoTruncate`]: PrecisionBehavior::AutoTruncate
40 * [sign]: std::fmt#sign0
41 * [`PhantomData`]: std::marker::PhantomData
42 */
43pub trait CustomDisplay {
44 /** The type of value displayed by this `CustomDisplay`. */
45 type Value: ?Sized;
46
47 /**
48 * Formats and writes a value to the given [`Formatter`].
49 *
50 * # Errors
51 *
52 * Errors if formatting fails.
53 *
54 * # Implementation Requirements
55 *
56 * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
57 * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
58 * "OPTIONAL" in this document are to be interpreted as described in
59 * RFC 2119.
60 *
61 * If [precision] is supported by this `CustomDisplay` and
62 * [`precision_behavior()`] returns [`Manual`], this method MUST handle it
63 * using [`Formatter::precision()`].
64 *
65 * If [sign] is supported by this `CustomDisplay`, this method MUST handle
66 * it using [`Formatter::sign()`].
67 *
68 * If [`precision_behavior()`] does not return [`Manual`], this method MUST
69 * NOT handle [width], [fill or alignment][fill_alignment]. Otherwise, this
70 * method MAY handle [width], [fill and alignment][fill_alignment], though
71 * it does not need to; those can be automatically handled by
72 * [`Displayable`]'s [`Display`] implementation.
73 *
74 * [precision]: std::fmt#precision
75 * [`precision_behavior()`]: Self::precision_behavior()
76 * [`Manual`]: PrecisionBehavior::Manual
77 * [sign]: std::fmt#sign0
78 * [width]: std::fmt#width
79 * [fill_alignment]: std::fmt#fillalignment
80 */
81 fn fmt(&self, value: &Self::Value, f: &mut Formatter<'_>) -> fmt::Result;
82
83 /**
84 * Returns the behavior of a [`Displayable`] that uses this `CustomDisplay`
85 * when a [precision] is set.
86 *
87 * [precision]: std::fmt#precision
88 */
89 fn precision_behavior(&self) -> PrecisionBehavior;
90
91 /**
92 * Returns the width of the value in monospace characters when written by
93 * [`fmt()`] to a [`Formatter`], or [`None`].
94 *
95 * If this method returns [`None`], and [`auto_width_fill_alignment()`]
96 * returns `true` or [`precision_behavior()`] returns [`AutoTruncate`],
97 * formatting will assume all characters have a monospace width of `1` and
98 * the width will be computed as the number of characters in the value
99 * written by [`fmt()`].
100 *
101 * # Implementation Requirements
102 *
103 * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
104 * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
105 * "OPTIONAL" in this document are to be interpreted as described in
106 * RFC 2119.
107 *
108 * If [`auto_width_fill_alignment()`] returns `false` and
109 * [`precision_behavior()`] returns [`Manual`], this method SHOULD return
110 * [`None`]. Otherwise, this method MAY return [`None`] if both of the
111 * following are true:
112 * - It is safe to assume that all characters written by [`fmt()`] have a
113 * monospace width of `1`.
114 * - Either:
115 * - The width cannot be computed more cheaply than by writing it to a
116 * [`String`] first.
117 * - One simply doesn't want to implement it and doesn't care about the
118 * performance cost.
119 *
120 * If this method does not return [`None`], it MUST return exactly the
121 * number of characters (not bytes!) that would be written by [`fmt()`]. As
122 * a corrolary, this method MUST take the [`Formatter`] into account to the
123 * exact same degree that [`fmt()`] does.
124 *
125 * [`fmt()`]: Self::fmt()
126 * [`auto_width_fill_alignment()`]: Self::auto_width_fill_alignment()
127 * [`precision_behavior()`]: Self::precision_behavior()
128 * [`AutoTruncate`]: PrecisionBehavior::AutoTruncate
129 * [`Manual`]: PrecisionBehavior::Manual
130 * [width]: std::fmt#width
131 * [fill and alignment]: std::fmt#fillalignment
132 */
133 fn width_in_chars(&self, value: &Self::Value, f: &Formatter<'_>) -> Option<usize>;
134
135 /**
136 * Returns whether a [`Displayable`] using this `CustomDisplay` will
137 * automatically handle [width], [fill and alignment].
138 *
139 * # Implementation Requirements
140 *
141 * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
142 * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
143 * "OPTIONAL" in this document are to be interpreted as described in
144 * RFC 2119.
145 *
146 * If [`fmt()`] handles [width], [fill and alignment], this method MUST
147 * return `false`.
148 *
149 * Otherwise, this method SHOULD return `true` unless [`fmt()`] writes
150 * multiple lines of text, as the padding from [width],
151 * [fill and alignment] then becomes less helpful.
152 *
153 * [width]: std::fmt#width
154 * [fill and alignment]: std::fmt#fillalignment
155 * [`fmt()`]: Self::fmt()
156 */
157 #[inline]
158 fn auto_width_fill_alignment(&self) -> bool {
159 true
160 }
161
162 /**
163 * Returns the [`Alignment`] to use if [width] is set but
164 * [fill and alignment] are not.
165 *
166 * # Implementation Notes
167 *
168 * By default, this method returns [`Alignment::Left`], as that is the
169 * alignment specified by [`std::fmt`][fill and alignment] for non-numeric
170 * types.
171 *
172 * [width]: std::fmt#width
173 * [fill and alignment]: std::fmt#fillalignment
174 */
175 #[inline]
176 fn default_alignment(&self) -> Alignment {
177 Alignment::Left
178 }
179
180 /**
181 * Returns a wrapper around this `CustomDisplay` and the given value that
182 * implements [`Display`].
183 *
184 * See [`Displayable`] for more information.
185 */
186 #[inline]
187 fn display<'multi>(
188 &'multi self,
189 value: &'multi Self::Value,
190 ) -> BorrowedDisplayable<'multi, Self> {
191 Displayable {
192 display: self,
193 value,
194 }
195 }
196
197 /**
198 * Returns a wrapper around this `CustomDisplay` and the given value that
199 * implements [`Display`] and takes ownership of this `CustomDisplay`.
200 *
201 * See [`Displayable`] for more information.
202 */
203 #[inline]
204 fn into_display(self, value: &Self::Value) -> OwnedDisplayable<'_, Self>
205 where
206 Self: Sized,
207 {
208 Displayable {
209 display: self,
210 value,
211 }
212 }
213}
214
215/**
216 * A [`Displayable`] that borrows its [`CustomDisplay`].
217 *
218 * See [`Displayable`] for more information.
219 */
220pub type BorrowedDisplayable<'multi, CD> = Displayable<'multi, CD, &'multi CD>;
221
222/**
223 * A [`Displayable`] that owns its [`CustomDisplay`].
224 *
225 * The value to be formatted is still borrowed.
226 *
227 * See [`Displayable`] for more information.
228 */
229pub type OwnedDisplayable<'value, CD> = Displayable<'value, CD, CD>;
230
231/**
232 * A wrapper around a [`CustomDisplay`] and a value that implements [`Display`]
233 * by calling [`CustomDisplay::fmt()`] with the value, automatically truncates
234 * the result of [`CustomDisplay::fmt()`] based on [precision] if
235 * [`CustomDisplay::precision_behavior()`] returns [`AutoTruncate`], and
236 * handles [width], [fill and alignment] if
237 * [`CustomDisplay::auto_width_fill_alignment()`] returns `true`.
238 *
239 * [precision]: std::fmt#precision
240 * [`AutoTruncate`]: PrecisionBehavior::AutoTruncate
241 * [width]: std::fmt#width
242 * [fill and alignment]: std::fmt#fillalignment
243 */
244#[derive(Clone, Copy, Debug)]
245pub struct Displayable<'value, CD, B>
246where
247 CD: CustomDisplay + ?Sized,
248 B: Borrow<CD>,
249{
250 display: B,
251 value: &'value CD::Value,
252}
253
254impl<CD, B> Displayable<'_, CD, B>
255where
256 CD: CustomDisplay + ?Sized,
257 B: Borrow<CD>,
258{
259 #[expect(
260 clippy::inline_always,
261 reason = "exceedingly simple comparison; method exists to make code \
262 more readable, as comparison is verbose"
263 )]
264 #[inline(always)]
265 fn auto_truncate(&self) -> bool {
266 self.display.borrow().precision_behavior() == PrecisionBehavior::AutoTruncate
267 }
268
269 fn write_maybe_pre_formatted(
270 &self,
271 f: &mut Formatter<'_>,
272 pre_formatted: Option<String>,
273 ) -> fmt::Result {
274 if let Some(formatted) = pre_formatted {
275 f.write_str(&formatted)
276 } else {
277 self.display.borrow().fmt(self.value, f)
278 }
279 }
280}
281
282#[expect(clippy::question_mark_used, reason = "format error propagation")]
283fn write_fill(f: &mut Formatter<'_>, width: usize, fill: char) -> fmt::Result {
284 for _ in 0..width {
285 write!(f, "{fill}")?;
286 }
287 Ok(())
288}
289
290impl<CD, B> Display for Displayable<'_, CD, B>
291where
292 CD: CustomDisplay + ?Sized,
293 B: Borrow<CD>,
294{
295 #[expect(clippy::question_mark_used, reason = "format error propagation")]
296 #[expect(
297 clippy::arithmetic_side_effects,
298 reason = "subtracting guaranteed smaller values"
299 )]
300 #[inline]
301 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
302 let display = self.display.borrow();
303
304 // case 1
305 if display.auto_width_fill_alignment()
306 && let Some(width) = f.width()
307 {
308 let (value_width, pre_formatted) = if self.auto_truncate()
309 && let Some(precision) = f.precision()
310 {
311 // case 1a: precision is used to truncate
312 if let Some(cheap_width) = display.width_in_chars(self.value, f)
313 && cheap_width <= precision
314 {
315 // case 1a.1: doesn't need truncation
316 (cheap_width, None)
317 } else {
318 // case 1a.2: no cheap width or needs truncation
319 #[expect(
320 clippy::recursive_format_impl,
321 reason = "recurses into case 3 from case 1a.2"
322 )]
323 let formatted = format!("{self}");
324
325 // if using the same default alignment as `pad` (left), or
326 // alignment was specified, return early with a simpler and
327 // slightly more efficient implementation that handles
328 // everything for us
329 if (display.default_alignment() == Alignment::Left) || f.align().is_some() {
330 return f.pad(&formatted);
331 }
332
333 // formatting this thing 3 times, fun
334 let truncated = format!("{formatted:.precision$}");
335 (truncated.chars().count(), Some(truncated))
336 }
337 } else if let Some(cheap_width) = display.width_in_chars(self.value, f) {
338 // case 1b: no truncation and cheap width
339 (cheap_width, None)
340 } else {
341 // case 1c: have to calculate width by formatting first
342 #[expect(
343 clippy::recursive_format_impl,
344 reason = "recurses into case 3 from case 1c"
345 )]
346 let formatted = format!("{self}");
347 (formatted.chars().count(), Some(formatted))
348 };
349
350 if value_width >= width {
351 // no fill needed
352 self.write_maybe_pre_formatted(f, pre_formatted)
353 } else {
354 // safe because `value_width` checked to be < `width` above
355 let total_fill = width - value_width;
356 let fill = f.fill();
357
358 match f.align().unwrap_or_else(|| display.default_alignment()) {
359 Alignment::Left => {
360 self.write_maybe_pre_formatted(f, pre_formatted)?;
361 write_fill(f, total_fill, fill)
362 }
363 Alignment::Right => {
364 write_fill(f, total_fill, fill)?;
365 self.write_maybe_pre_formatted(f, pre_formatted)
366 }
367 Alignment::Center => {
368 let left_fill = total_fill / 2;
369 // safe because `total_fill` >= `left_fill` by construction;
370 // equivalent to `total_fill.div_ceil(2)` but faster
371 let right_fill = total_fill - left_fill;
372
373 write_fill(f, left_fill, fill)?;
374 self.write_maybe_pre_formatted(f, pre_formatted)?;
375 write_fill(f, right_fill, fill)
376 }
377 }
378 }
379 } else if self.auto_truncate() && f.precision().is_some() {
380 // case 2: no width/fill/alignment, precision is used to truncate
381 #[expect(
382 clippy::recursive_format_impl,
383 reason = "recurses into case 3 from case 2"
384 )]
385 let formatted = format!("{self}");
386 // because this branch doesn't have width set, we don't need to
387 // worry about mismatched default alignment and can let `pad`
388 // do the truncating.
389 f.pad(&formatted)
390 } else {
391 // case 3: no width/fill/alignment or precision handling
392 display.fmt(self.value, f)
393 }
394 }
395}