Skip to main content

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}