custom-display 0.1.0

A trait for implementing custom formatting logic for types
Documentation
// SPDX-FileCopyrightText: 2026 Marissa (cuddle puddle) <dev@princess.lgbt>
//
// SPDX-License-Identifier: MPL-2.0

use std::borrow::Borrow;
use std::fmt::{self, Alignment, Display, Formatter};

use super::PrecisionBehavior;

/**
 * A custom format, roughly equivalent to [`Display`] except that it is not
 * restricted by [coherence].
 *
 * [`Displayable`]s that use this trait (created by calling [`display()`] or
 * [`into_display()`]) can automatically handle [width], [fill and alignment],
 * as well as [precision] if [`precision_behavior()`] returns [`AutoTruncate`]
 * (the behavior specified for non-numeric types).
 *
 * # Implementation Requirements
 *
 * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
 * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED",  "MAY", and
 * "OPTIONAL" in this document are to be interpreted as described in
 * RFC 2119.
 *
 * Implementations MUST document how they handle [sign], [precision], [width],
 * [fill and alignment].
 *
 * Most implementations will be unit sructs or type-parameterized tuple structs
 * containing only [`PhantomData`].
 *
 * [coherence]: https://doc.rust-lang.org/reference/items/implementations.html#trait-implementation-coherence
 * [`display()`]: Self::display()
 * [`into_display()`]: Self::into_display()
 * [width]: std::fmt#width
 * [fill and alignment]: std::fmt#fillalignment
 * [precision]: std::fmt#precision
 * [`precision_behavior()`]: Self::precision_behavior
 * [`AutoTruncate`]: PrecisionBehavior::AutoTruncate
 * [sign]: std::fmt#sign0
 * [`PhantomData`]: std::marker::PhantomData
 */
pub trait CustomDisplay {
    /** The type of value displayed by this `CustomDisplay`. */
    type Value: ?Sized;

    /**
     * Formats and writes a value to the given [`Formatter`].
     *
     * # Errors
     *
     * Errors if formatting fails.
     *
     * # Implementation Requirements
     *
     * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
     * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED",  "MAY", and
     * "OPTIONAL" in this document are to be interpreted as described in
     * RFC 2119.
     *
     * If [precision] is supported by this `CustomDisplay` and
     * [`precision_behavior()`] returns [`Manual`], this method MUST handle it
     * using [`Formatter::precision()`].
     *
     * If [sign] is supported by this `CustomDisplay`, this method MUST handle
     * it using [`Formatter::sign()`].
     *
     * If [`precision_behavior()`] does not return [`Manual`], this method MUST
     * NOT handle [width], [fill or alignment][fill_alignment]. Otherwise, this
     * method MAY handle [width], [fill and alignment][fill_alignment], though
     * it does not need to; those can be automatically handled by
     * [`Displayable`]'s [`Display`] implementation.
     *
     * [precision]: std::fmt#precision
     * [`precision_behavior()`]: Self::precision_behavior()
     * [`Manual`]: PrecisionBehavior::Manual
     * [sign]: std::fmt#sign0
     * [width]: std::fmt#width
     * [fill_alignment]: std::fmt#fillalignment
     */
    fn fmt(&self, value: &Self::Value, f: &mut Formatter<'_>) -> fmt::Result;

    /**
     * Returns the behavior of a [`Displayable`] that uses this `CustomDisplay`
     * when a [precision] is set.
     *
     * [precision]: std::fmt#precision
     */
    fn precision_behavior(&self) -> PrecisionBehavior;

    /**
     * Returns the width of the value in monospace characters when written by
     * [`fmt()`] to a [`Formatter`], or [`None`].
     *
     * If this method returns [`None`], and [`auto_width_fill_alignment()`]
     * returns `true` or [`precision_behavior()`] returns [`AutoTruncate`],
     * formatting will assume all characters have a monospace width of `1` and
     * the width will be computed as the number of characters in the value
     * written by [`fmt()`].
     *
     * # Implementation Requirements
     *
     * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
     * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED",  "MAY", and
     * "OPTIONAL" in this document are to be interpreted as described in
     * RFC 2119.
     *
     * If [`auto_width_fill_alignment()`] returns `false` and
     * [`precision_behavior()`] returns [`Manual`], this method SHOULD return
     * [`None`]. Otherwise, this method MAY return [`None`] if both of the
     * following are true:
     * - It is safe to assume that all characters written by [`fmt()`] have a
     *   monospace width of `1`.
     * - Either:
     *   - The width cannot be computed more cheaply than by writing it to a
     *     [`String`] first.
     *   - One simply doesn't want to implement it and doesn't care about the
     *     performance cost.
     *
     * If this method does not return [`None`], it MUST return exactly the
     * number of characters (not bytes!) that would be written by [`fmt()`]. As
     * a corrolary, this method MUST take the [`Formatter`] into account to the
     * exact same degree that [`fmt()`] does.
     *
     * [`fmt()`]: Self::fmt()
     * [`auto_width_fill_alignment()`]: Self::auto_width_fill_alignment()
     * [`precision_behavior()`]: Self::precision_behavior()
     * [`AutoTruncate`]: PrecisionBehavior::AutoTruncate
     * [`Manual`]: PrecisionBehavior::Manual
     * [width]: std::fmt#width
     * [fill and alignment]: std::fmt#fillalignment
     */
    fn width_in_chars(&self, value: &Self::Value, f: &Formatter<'_>) -> Option<usize>;

    /**
     * Returns whether a [`Displayable`] using this `CustomDisplay` will
     * automatically handle [width], [fill and alignment].
     *
     * # Implementation Requirements
     *
     * The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
     * NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED",  "MAY", and
     * "OPTIONAL" in this document are to be interpreted as described in
     * RFC 2119.
     *
     * If [`fmt()`] handles [width], [fill and alignment], this method MUST
     * return `false`.
     *
     * Otherwise, this method SHOULD return `true` unless [`fmt()`] writes
     * multiple lines of text, as the padding from [width],
     * [fill and alignment] then becomes less helpful.
     *
     * [width]: std::fmt#width
     * [fill and alignment]: std::fmt#fillalignment
     * [`fmt()`]: Self::fmt()
     */
    #[inline]
    fn auto_width_fill_alignment(&self) -> bool {
        true
    }

    /**
     * Returns the [`Alignment`] to use if [width] is set but
     * [fill and alignment] are not.
     *
     * # Implementation Notes
     *
     * By default, this method returns [`Alignment::Left`], as that is the
     * alignment specified by [`std::fmt`][fill and alignment] for non-numeric
     * types.
     *
     * [width]: std::fmt#width
     * [fill and alignment]: std::fmt#fillalignment
     */
    #[inline]
    fn default_alignment(&self) -> Alignment {
        Alignment::Left
    }

    /**
     * Returns a wrapper around this `CustomDisplay` and the given value that
     * implements [`Display`].
     *
     * See [`Displayable`] for more information.
     */
    #[inline]
    fn display<'multi>(
        &'multi self,
        value: &'multi Self::Value,
    ) -> BorrowedDisplayable<'multi, Self> {
        Displayable {
            display: self,
            value,
        }
    }

    /**
     * Returns a wrapper around this `CustomDisplay` and the given value that
     * implements [`Display`] and takes ownership of this `CustomDisplay`.
     *
     * See [`Displayable`] for more information.
     */
    #[inline]
    fn into_display(self, value: &Self::Value) -> OwnedDisplayable<'_, Self>
    where
        Self: Sized,
    {
        Displayable {
            display: self,
            value,
        }
    }
}

/**
 * A [`Displayable`] that borrows its [`CustomDisplay`].
 *
 * See [`Displayable`] for more information.
 */
pub type BorrowedDisplayable<'multi, CD> = Displayable<'multi, CD, &'multi CD>;

/**
 * A [`Displayable`] that owns its [`CustomDisplay`].
 *
 * The value to be formatted is still borrowed.
 *
 * See [`Displayable`] for more information.
 */
pub type OwnedDisplayable<'value, CD> = Displayable<'value, CD, CD>;

/**
 * A wrapper around a [`CustomDisplay`] and a value that implements [`Display`]
 * by calling [`CustomDisplay::fmt()`] with the value, automatically truncates
 * the result of [`CustomDisplay::fmt()`] based on [precision] if
 * [`CustomDisplay::precision_behavior()`] returns [`AutoTruncate`], and
 * handles [width], [fill and alignment] if
 * [`CustomDisplay::auto_width_fill_alignment()`] returns `true`.
 *
 * [precision]: std::fmt#precision
 * [`AutoTruncate`]: PrecisionBehavior::AutoTruncate
 * [width]: std::fmt#width
 * [fill and alignment]: std::fmt#fillalignment
 */
#[derive(Clone, Copy, Debug)]
pub struct Displayable<'value, CD, B>
where
    CD: CustomDisplay + ?Sized,
    B: Borrow<CD>,
{
    display: B,
    value: &'value CD::Value,
}

impl<CD, B> Displayable<'_, CD, B>
where
    CD: CustomDisplay + ?Sized,
    B: Borrow<CD>,
{
    #[expect(
        clippy::inline_always,
        reason = "exceedingly simple comparison; method exists to make code \
                  more readable, as comparison is verbose"
    )]
    #[inline(always)]
    fn auto_truncate(&self) -> bool {
        self.display.borrow().precision_behavior() == PrecisionBehavior::AutoTruncate
    }

    fn write_maybe_pre_formatted(
        &self,
        f: &mut Formatter<'_>,
        pre_formatted: Option<String>,
    ) -> fmt::Result {
        if let Some(formatted) = pre_formatted {
            f.write_str(&formatted)
        } else {
            self.display.borrow().fmt(self.value, f)
        }
    }
}

#[expect(clippy::question_mark_used, reason = "format error propagation")]
fn write_fill(f: &mut Formatter<'_>, width: usize, fill: char) -> fmt::Result {
    for _ in 0..width {
        write!(f, "{fill}")?;
    }
    Ok(())
}

impl<CD, B> Display for Displayable<'_, CD, B>
where
    CD: CustomDisplay + ?Sized,
    B: Borrow<CD>,
{
    #[expect(clippy::question_mark_used, reason = "format error propagation")]
    #[expect(
        clippy::arithmetic_side_effects,
        reason = "subtracting guaranteed smaller values"
    )]
    #[inline]
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let display = self.display.borrow();

        // case 1
        if display.auto_width_fill_alignment()
            && let Some(width) = f.width()
        {
            let (value_width, pre_formatted) = if self.auto_truncate()
                && let Some(precision) = f.precision()
            {
                // case 1a: precision is used to truncate
                if let Some(cheap_width) = display.width_in_chars(self.value, f)
                    && cheap_width <= precision
                {
                    // case 1a.1: doesn't need truncation
                    (cheap_width, None)
                } else {
                    // case 1a.2: no cheap width or needs truncation
                    #[expect(
                        clippy::recursive_format_impl,
                        reason = "recurses into case 3 from case 1a.2"
                    )]
                    let formatted = format!("{self}");

                    // if using the same default alignment as `pad` (left), or
                    // alignment was specified, return early with a simpler and
                    // slightly more efficient implementation that handles
                    // everything for us
                    if (display.default_alignment() == Alignment::Left) || f.align().is_some() {
                        return f.pad(&formatted);
                    }

                    // formatting this thing 3 times, fun
                    let truncated = format!("{formatted:.precision$}");
                    (truncated.chars().count(), Some(truncated))
                }
            } else if let Some(cheap_width) = display.width_in_chars(self.value, f) {
                // case 1b: no truncation and cheap width
                (cheap_width, None)
            } else {
                // case 1c: have to calculate width by formatting first
                #[expect(
                    clippy::recursive_format_impl,
                    reason = "recurses into case 3 from case 1c"
                )]
                let formatted = format!("{self}");
                (formatted.chars().count(), Some(formatted))
            };

            if value_width >= width {
                // no fill needed
                self.write_maybe_pre_formatted(f, pre_formatted)
            } else {
                // safe because `value_width` checked to be < `width` above
                let total_fill = width - value_width;
                let fill = f.fill();

                match f.align().unwrap_or_else(|| display.default_alignment()) {
                    Alignment::Left => {
                        self.write_maybe_pre_formatted(f, pre_formatted)?;
                        write_fill(f, total_fill, fill)
                    }
                    Alignment::Right => {
                        write_fill(f, total_fill, fill)?;
                        self.write_maybe_pre_formatted(f, pre_formatted)
                    }
                    Alignment::Center => {
                        let left_fill = total_fill / 2;
                        // safe because `total_fill` >= `left_fill` by construction;
                        // equivalent to `total_fill.div_ceil(2)` but faster
                        let right_fill = total_fill - left_fill;

                        write_fill(f, left_fill, fill)?;
                        self.write_maybe_pre_formatted(f, pre_formatted)?;
                        write_fill(f, right_fill, fill)
                    }
                }
            }
        } else if self.auto_truncate() && f.precision().is_some() {
            // case 2: no width/fill/alignment, precision is used to truncate
            #[expect(
                clippy::recursive_format_impl,
                reason = "recurses into case 3 from case 2"
            )]
            let formatted = format!("{self}");
            // because this branch doesn't have width set, we don't need to
            // worry about mismatched default alignment and can let `pad`
            // do the truncating.
            f.pad(&formatted)
        } else {
            // case 3: no width/fill/alignment or precision handling
            display.fmt(self.value, f)
        }
    }
}