Skip to main content

display_types/
capabilities.rs

1/// A reference-counted, type-erased warning value.
2///
3/// Any type that implements [`core::error::Error`] + [`Send`] + [`Sync`] + `'static` can be
4/// wrapped in a `ParseWarning`. The built-in library variants use `EdidWarning`, but
5/// custom handlers may push their own error types without wrapping them in `EdidWarning`.
6///
7/// Using [`Arc`][crate::prelude::Arc] (rather than `Box`) means `ParseWarning` is
8/// [`Clone`], which lets warnings be copied from a parsed representation into
9/// [`DisplayCapabilities`] without consuming the parsed result.
10///
11/// To inspect a specific variant, use the inherent `downcast_ref` method available on
12/// `dyn core::error::Error + Send + Sync + 'static` in `std` builds:
13///
14/// ```text
15/// for w in caps.iter_warnings() {
16///     if let Some(ew) = (**w).downcast_ref::<EdidWarning>() { ... }
17/// }
18/// ```
19#[cfg(any(feature = "alloc", feature = "std"))]
20pub type ParseWarning = crate::prelude::Arc<dyn core::error::Error + Send + Sync + 'static>;
21
22/// Stereo viewing support decoded from DTD byte 17 bits 6, 5, and 0.
23#[non_exhaustive]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum StereoMode {
27    /// Normal display; no stereo (bits 6–5 = `0b00`; bit 0 is don't-care).
28    #[default]
29    None,
30    /// Field-sequential stereo, right image when stereo sync = 1 (bits 6–5 = `0b01`, bit 0 = 0).
31    FieldSequentialRightFirst,
32    /// Field-sequential stereo, left image when stereo sync = 1 (bits 6–5 = `0b10`, bit 0 = 0).
33    FieldSequentialLeftFirst,
34    /// 2-way interleaved stereo, right image on even lines (bits 6–5 = `0b01`, bit 0 = 1).
35    TwoWayInterleavedRightEven,
36    /// 2-way interleaved stereo, left image on even lines (bits 6–5 = `0b10`, bit 0 = 1).
37    TwoWayInterleavedLeftEven,
38    /// 4-way interleaved stereo (bits 6–5 = `0b11`, bit 0 = 0).
39    FourWayInterleaved,
40    /// Side-by-side interleaved stereo (bits 6–5 = `0b11`, bit 0 = 1).
41    SideBySideInterleaved,
42}
43
44/// CVT formula selector for DisplayID 2.x Type IX (`0x24`) and Type V (`0x11`)
45/// Formula-Based Timings.
46///
47/// Decoded from byte 0 bits 2:0. Identifies which CVT variant the consumer should use
48/// to derive blanking parameters and pixel clock from the `(width, height, refresh_rate)`
49/// triple stored on [`VideoMode`]. Codes `3`–`7` are reserved by the DisplayID 2.x spec;
50/// unknown encodings are surfaced as [`CvtAlgorithm::Reserved`] so a future spec value
51/// does not block decoding of the rest of the descriptor.
52#[non_exhaustive]
53#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub enum CvtAlgorithm {
56    /// Standard CVT (no reduced blanking) (encoding `0`).
57    Cvt,
58    /// CVT-RB v1 (encoding `1`).
59    CvtRb,
60    /// CVT-R2 / CVT-RB v2 (encoding `2`).
61    CvtR2,
62    /// Spec-reserved encoding (`3`–`7`) preserved verbatim so unknown values do not block
63    /// decoding of the rest of the descriptor.
64    Reserved(u8),
65}
66
67impl CvtAlgorithm {
68    /// Decodes the 3-bit CVT algorithm field (Type V/IX descriptor byte 0 bits 2:0).
69    /// Upper bits of the input are ignored.
70    pub const fn from_bits(b: u8) -> Self {
71        match b & 0x07 {
72            0 => Self::Cvt,
73            1 => Self::CvtRb,
74            2 => Self::CvtR2,
75            other => Self::Reserved(other),
76        }
77    }
78}
79
80/// Stereo timing mode decoded from Type V (`0x11`) and Type IX (`0x24`) descriptor byte 0
81/// bits 6:5. Indicates whether the timing is for a mono display, stereo-only, or
82/// user-selectable.
83///
84/// This is distinct from [`StereoMode`], which describes the specific stereo viewing method
85/// decoded from a Detailed Timing Descriptor.
86#[non_exhaustive]
87#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum TypeIxStereoMode {
90    /// Mono timing (bits 6:5 = `0b00`).
91    Mono,
92    /// 3D stereo timing (bits 6:5 = `0b01`).
93    Stereo,
94    /// Mono or 3D stereo depending on user action (bits 6:5 = `0b10`).
95    MonoOrStereoByUser,
96    /// Reserved (bits 6:5 = `0b11`).
97    Reserved,
98}
99
100/// Sync signal definition decoded from DTD byte 17 bits 4–1.
101#[non_exhaustive]
102#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
103#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub enum SyncDefinition {
105    /// Analog composite sync (bit 4 = 0, bit 3 = 0).
106    AnalogComposite {
107        /// H-sync pulse present during V-sync (serrations).
108        serrations: bool,
109        /// Sync on all three RGB signals (`true`) or green only (`false`).
110        sync_on_all_rgb: bool,
111    },
112    /// Bipolar analog composite sync (bit 4 = 0, bit 3 = 1).
113    BipolarAnalogComposite {
114        /// H-sync pulse present during V-sync (serrations).
115        serrations: bool,
116        /// Sync on all three RGB signals (`true`) or green only (`false`).
117        sync_on_all_rgb: bool,
118    },
119    /// Digital composite sync on H-sync pin (bit 4 = 1, bit 3 = 0).
120    DigitalComposite {
121        /// H-sync pulse present during V-sync (serrations).
122        serrations: bool,
123        /// H-sync polarity outside V-sync: `true` = positive.
124        h_sync_positive: bool,
125    },
126    /// Digital separate sync (bit 4 = 1, bit 3 = 1).
127    DigitalSeparate {
128        /// V-sync polarity: `true` = positive.
129        v_sync_positive: bool,
130        /// H-sync polarity: `true` = positive.
131        h_sync_positive: bool,
132    },
133}
134
135/// The source from which a [`VideoMode`] was decoded.
136///
137/// Populated automatically by [`vic_to_mode`][crate::cea861::vic_to_mode] and
138/// [`dmt_to_mode`][crate::cea861::dmt_to_mode]; parsers that decode Detailed Timing
139/// Descriptors should set it via [`VideoMode::with_source`]. `None` for modes
140/// constructed directly via [`VideoMode::new`].
141#[non_exhaustive]
142#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
143#[derive(Debug, Clone, Copy, PartialEq, Eq)]
144pub enum ModeSource {
145    /// A CTA-861 Video Identification Code, as used in Short Video Descriptors,
146    /// the Y420 Video Data Block, and the Y420 Capability Map Data Block.
147    Vic(u8),
148    /// A VESA Display Monitor Timings identifier (0x01–0x58).
149    DmtId(u16),
150    /// Zero-based index of a Detailed Timing Descriptor within its containing EDID block.
151    DtdIndex(u8),
152}
153
154/// A display refresh rate expressed as an exact rational number (numerator/denominator in Hz).
155///
156/// Integer rates (60 Hz, 120 Hz, etc.) use `denom = 1`. NTSC-derived fractional rates use
157/// `denom = 1001` (e.g. 60000/1001 ≈ 59.94 Hz, 24000/1001 ≈ 23.976 Hz).
158///
159/// Always stored in lowest terms: all constructors (including `Deserialize`) apply GCD
160/// reduction, so `==`, `Hash`, and `Ord` are consistent and a given rate has exactly one
161/// canonical representation.
162///
163/// Use [`RefreshRate::integral`] for integer rates and [`RefreshRate::fractional`] for all
164/// others. `From<u32>` and `From<u16>` are implemented as `integral` conversions, so
165/// integer literals work wherever `impl Into<RefreshRate>` is accepted.
166#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
167#[cfg_attr(
168    feature = "serde",
169    serde(try_from = "RefreshRateRepr", into = "RefreshRateRepr")
170)]
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
172pub struct RefreshRate {
173    numer: u32,
174    denom: u32,
175}
176
177#[cfg(feature = "serde")]
178#[derive(serde::Serialize, serde::Deserialize)]
179struct RefreshRateRepr {
180    numer: u32,
181    denom: u32,
182}
183
184#[cfg(feature = "serde")]
185impl core::convert::TryFrom<RefreshRateRepr> for RefreshRate {
186    type Error = &'static str;
187    fn try_from(r: RefreshRateRepr) -> Result<Self, Self::Error> {
188        if r.denom == 0 {
189            Err("RefreshRate denominator must not be zero")
190        } else {
191            Ok(Self::fractional(r.numer, r.denom))
192        }
193    }
194}
195
196#[cfg(feature = "serde")]
197impl From<RefreshRate> for RefreshRateRepr {
198    fn from(r: RefreshRate) -> Self {
199        Self {
200            numer: r.numer,
201            denom: r.denom,
202        }
203    }
204}
205
206fn gcd(mut a: u32, mut b: u32) -> u32 {
207    while b != 0 {
208        let t = b;
209        b = a % b;
210        a = t;
211    }
212    a
213}
214
215fn gcd_u64(mut a: u64, mut b: u64) -> u64 {
216    while b != 0 {
217        let t = b;
218        b = a % b;
219        a = t;
220    }
221    a
222}
223
224impl RefreshRate {
225    /// Constructs an integer refresh rate (e.g. `RefreshRate::integral(60)` → 60/1).
226    pub fn integral(hz: u32) -> Self {
227        Self {
228            numer: hz,
229            denom: 1,
230        }
231    }
232
233    /// Constructs an exact rational refresh rate, reduced to lowest terms.
234    ///
235    /// # Panics
236    ///
237    /// Panics if `denom` is zero.
238    pub fn fractional(numer: u32, denom: u32) -> Self {
239        assert!(denom != 0, "RefreshRate denominator must not be zero");
240        let g = gcd(numer, denom);
241        Self {
242            numer: numer / g,
243            denom: denom / g,
244        }
245    }
246
247    /// Constructs a refresh rate from a `numer/denom` ratio in Hz, reduced to lowest terms.
248    ///
249    /// Useful when the rate is computed from intermediate values that exceed `u32`, such as
250    /// `pixel_clock_hz / (h_total × v_total)` for detailed-timing descriptors. Reduces in
251    /// `u64` then narrows to `u32`.
252    ///
253    /// Returns `None` if `denom` is zero or if the reduced fraction does not fit in `u32`.
254    ///
255    /// ```
256    /// use display_types::RefreshRate;
257    ///
258    /// // NTSC-style fractional rate computed from a large numerator and denominator.
259    /// let r = RefreshRate::from_ratio(60_000_000, 1_001_000).unwrap();
260    /// assert_eq!(r, RefreshRate::fractional(60_000, 1_001));
261    /// ```
262    pub fn from_ratio(numer: u64, denom: u64) -> Option<Self> {
263        if denom == 0 {
264            return None;
265        }
266        let g = gcd_u64(numer, denom);
267        let n = u32::try_from(numer / g).ok()?;
268        let d = u32::try_from(denom / g).ok()?;
269        Some(Self { numer: n, denom: d })
270    }
271
272    /// Numerator of the reduced fraction, in Hz.
273    pub fn numer(self) -> u32 {
274        self.numer
275    }
276
277    /// Denominator of the reduced fraction (1 for integer rates, 1001 for NTSC-derived rates).
278    pub fn denom(self) -> u32 {
279        self.denom
280    }
281
282    /// Returns the refresh rate as `f64`.
283    pub fn as_f64(self) -> f64 {
284        self.numer as f64 / self.denom as f64
285    }
286}
287
288impl PartialOrd for RefreshRate {
289    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
290        Some(self.cmp(other))
291    }
292}
293
294impl core::cmp::Ord for RefreshRate {
295    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
296        (self.numer as u64 * other.denom as u64).cmp(&(other.numer as u64 * self.denom as u64))
297    }
298}
299
300impl core::fmt::Display for RefreshRate {
301    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
302        if self.denom == 1 {
303            write!(f, "{} Hz", self.numer)
304        } else {
305            write!(f, "{}/{} Hz", self.numer, self.denom)
306        }
307    }
308}
309
310impl From<u32> for RefreshRate {
311    fn from(hz: u32) -> Self {
312        Self::integral(hz)
313    }
314}
315
316impl From<u16> for RefreshRate {
317    fn from(hz: u16) -> Self {
318        Self::integral(hz as u32)
319    }
320}
321
322/// A display video mode expressed as resolution, refresh rate, and scan type.
323///
324/// Use [`VideoMode::new`] to construct a mode with only identity fields (the common case
325/// for modes decoded from standard timing or SVD entries). Use
326/// [`VideoMode::with_detailed_timing`] to add the blanking-interval and signal fields
327/// available from a Detailed Timing Descriptor or equivalent.
328#[non_exhaustive]
329#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
330#[derive(Debug, Clone, PartialEq, Default)]
331pub struct VideoMode {
332    /// Horizontal resolution in pixels.
333    pub width: u16,
334    /// Vertical resolution in pixels.
335    pub height: u16,
336    /// Refresh rate as an exact rational number in Hz, or `None` when unspecified
337    /// (e.g. a default-constructed `VideoMode` whose rate has not been set).
338    pub refresh_rate: Option<RefreshRate>,
339    /// `true` for interlaced modes; `false` for progressive (the common case).
340    pub interlaced: bool,
341    /// Horizontal front porch in pixels (0 when not decoded from a DTD).
342    pub h_front_porch: u16,
343    /// Horizontal sync pulse width in pixels (0 when not decoded from a DTD).
344    pub h_sync_width: u16,
345    /// Vertical front porch in lines (0 when not decoded from a DTD).
346    pub v_front_porch: u16,
347    /// Vertical sync pulse width in lines (0 when not decoded from a DTD).
348    pub v_sync_width: u16,
349    /// Horizontal border width in pixels on each side of the active area (0 when not from a DTD).
350    pub h_border: u8,
351    /// Vertical border height in lines on each side of the active area (0 when not from a DTD).
352    pub v_border: u8,
353    /// Stereo viewing support (default [`StereoMode::None`] for non-DTD modes).
354    pub stereo: StereoMode,
355    /// Sync signal definition (`None` for non-DTD modes).
356    pub sync: Option<SyncDefinition>,
357    /// Pixel clock in kHz (`None` for modes not decoded from a Detailed Timing Descriptor).
358    pub pixel_clock_khz: Option<u32>,
359    /// The source from which this mode was decoded, if known.
360    ///
361    /// `None` for modes constructed directly via [`VideoMode::new`] without a table lookup.
362    pub source: Option<ModeSource>,
363    /// CVT formula selector for DisplayID 2.x Type V/IX timings (`None` for all other sources).
364    /// Consumers can use this to derive blanking and pixel clock from `(width, height,
365    /// refresh_rate)` via the named CVT variant.
366    pub cvt_algorithm: Option<CvtAlgorithm>,
367    /// `true` when the timing is YCbCr 4:2:0 only. Set from CTA-861 Y420 capability data
368    /// and from DisplayID 2.x Type VII byte 3 bit 7 (block revision ≥ 2).
369    /// Defaults to `false` for all other sources.
370    pub y420: bool,
371    /// `true` when NTSC-style fractional refresh rate (× 1000/1001) is supported alongside
372    /// this timing. Decoded from Type V and Type IX descriptor byte 0 bit 3.
373    /// Defaults to `false` for all other sources.
374    pub ntsc_fractional_refresh: bool,
375    /// Per-mode stereo indicator from Type V (`0x11`) and Type IX (`0x24`) descriptor byte 0
376    /// bits 6:5. `None` for all other timing sources.
377    pub type_ix_stereo: Option<TypeIxStereoMode>,
378}
379
380impl VideoMode {
381    /// Constructs a `VideoMode` with the given identity fields.
382    ///
383    /// All blanking-interval fields (`h_front_porch`, `h_sync_width`, `v_front_porch`,
384    /// `v_sync_width`, `h_border`, `v_border`) default to `0`, `stereo` defaults to
385    /// [`StereoMode::None`], and `sync` defaults to `None`. Use
386    /// [`with_detailed_timing`][Self::with_detailed_timing] to set those fields when
387    /// decoding from a Detailed Timing Descriptor.
388    pub fn new(
389        width: u16,
390        height: u16,
391        refresh_rate: impl Into<RefreshRate>,
392        interlaced: bool,
393    ) -> Self {
394        Self {
395            width,
396            height,
397            refresh_rate: Some(refresh_rate.into()),
398            interlaced,
399            ..Self::default()
400        }
401    }
402
403    /// Sets the exact pixel clock in kHz, returning the updated mode.
404    ///
405    /// Use this when constructing a [`VideoMode`] from hardware timing registers or a
406    /// known-good mode table entry, where the exact pixel clock is available but full
407    /// Detailed Timing Descriptor fields are not. The supplied clock is returned verbatim
408    /// by [`pixel_clock_khz`][crate::pixel_clock_khz], bypassing the CVT-RB fallback
409    /// estimate.
410    ///
411    /// ```
412    /// use display_types::VideoMode;
413    /// use display_types::pixel_clock_khz;
414    ///
415    /// // Custom panel: 1920×1200 @ 60 Hz, exact pixel clock from PLL register.
416    /// let mode = VideoMode::new(1920, 1200, 60u32, false).with_pixel_clock(154_000);
417    /// assert_eq!(pixel_clock_khz(&mode), 154_000);
418    /// ```
419    pub fn with_pixel_clock(mut self, pixel_clock_khz: u32) -> Self {
420        self.pixel_clock_khz = Some(pixel_clock_khz);
421        self
422    }
423
424    /// Sets the CVT formula selector, returning the updated mode. Used by DisplayID 2.x
425    /// Type IX (`0x24`) descriptors which signal a CVT variant alongside `(width, height,
426    /// refresh_rate)`.
427    pub fn with_cvt_algorithm(mut self, alg: CvtAlgorithm) -> Self {
428        self.cvt_algorithm = Some(alg);
429        self
430    }
431
432    /// Sets the YCbCr 4:2:0 flag, returning the updated mode. Used by DisplayID 2.x
433    /// Type VII decoders (block revision ≥ 2) and by callers that derive Y420-only modes
434    /// from CTA-861 Y420 capability data.
435    pub fn with_y420(mut self, y420: bool) -> Self {
436        self.y420 = y420;
437        self
438    }
439
440    /// Sets the NTSC fractional refresh flag, returning the updated mode. Used by
441    /// Type V and Type IX decoders when byte 0 bit 3 is set.
442    pub fn with_ntsc_fractional_refresh(mut self, supported: bool) -> Self {
443        self.ntsc_fractional_refresh = supported;
444        self
445    }
446
447    /// Sets the per-mode stereo indicator from Type V/IX byte 0 bits 6:5, returning the
448    /// updated mode.
449    pub fn with_type_ix_stereo(mut self, stereo: TypeIxStereoMode) -> Self {
450        self.type_ix_stereo = Some(stereo);
451        self
452    }
453
454    /// Sets the mode source, returning the updated mode.
455    ///
456    /// Called automatically by [`vic_to_mode`][crate::cea861::vic_to_mode] and
457    /// [`dmt_to_mode`][crate::cea861::dmt_to_mode]. Parsers decoding Detailed Timing
458    /// Descriptors should call `.with_source(ModeSource::DtdIndex(n))` so that the
459    /// descriptor's position survives into negotiated output.
460    pub fn with_source(mut self, source: ModeSource) -> Self {
461        self.source = Some(source);
462        self
463    }
464
465    /// Adds blanking-interval and signal fields decoded from a Detailed Timing Descriptor
466    /// or equivalent source, returning the updated mode.
467    ///
468    /// The 9-parameter count mirrors the DTD fields directly (EDID §3.10.3 / DisplayID §4.4).
469    #[allow(clippy::too_many_arguments)]
470    pub fn with_detailed_timing(
471        mut self,
472        pixel_clock_khz: u32,
473        h_front_porch: u16,
474        h_sync_width: u16,
475        v_front_porch: u16,
476        v_sync_width: u16,
477        h_border: u8,
478        v_border: u8,
479        stereo: StereoMode,
480        sync: Option<SyncDefinition>,
481    ) -> Self {
482        self.pixel_clock_khz = Some(pixel_clock_khz);
483        self.h_front_porch = h_front_porch;
484        self.h_sync_width = h_sync_width;
485        self.v_front_porch = v_front_porch;
486        self.v_sync_width = v_sync_width;
487        self.h_border = h_border;
488        self.v_border = v_border;
489        self.stereo = stereo;
490        self.sync = sync;
491        self
492    }
493}
494
495/// EDID specification version and revision, decoded from base block bytes 18–19.
496///
497/// Most displays in use report version 1 with revision 3 or 4.
498#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
499#[derive(Debug, Clone, Copy, PartialEq, Eq)]
500pub struct EdidVersion {
501    /// EDID version number (byte 18). Always `1` for all current displays.
502    pub version: u8,
503    /// EDID revision number (byte 19).
504    pub revision: u8,
505}
506
507impl core::fmt::Display for EdidVersion {
508    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
509        write!(f, "{}.{}", self.version, self.revision)
510    }
511}
512
513/// Trait for typed data stored in [`DisplayCapabilities::extension_data`] by custom handlers.
514///
515/// A blanket implementation covers any type that is `Any + Debug + Send + Sync`, so consumers
516/// do not need to implement this trait manually — `#[derive(Debug)]` on a `Send + Sync` type
517/// is sufficient.
518#[cfg(any(feature = "alloc", feature = "std"))]
519pub trait ExtensionData: core::any::Any + core::fmt::Debug + Send + Sync {
520    /// Returns `self` as `&dyn Any` to enable downcasting.
521    fn as_any(&self) -> &dyn core::any::Any;
522}
523
524#[cfg(any(feature = "alloc", feature = "std"))]
525impl<T: core::any::Any + core::fmt::Debug + Send + Sync> ExtensionData for T {
526    fn as_any(&self) -> &dyn core::any::Any {
527        self
528    }
529}
530
531/// Consumer-facing display capability model produced by a display data parser.
532///
533/// All fields defined by the relevant specification are decoded and exposed here.
534/// No field is omitted because it appears obscure or unlikely to be needed — that
535/// judgement belongs to the consumer, not the library.
536///
537/// Fields are `Option` where the underlying data may be absent or undecodable.
538/// `None` means the value was not present or could not be reliably determined; it does
539/// not imply the field is unimportant. The library never invents or defaults data.
540#[non_exhaustive]
541#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
542#[derive(Debug, Clone, Default)]
543pub struct DisplayCapabilities {
544    /// Three-character PNP manufacturer ID (e.g. `GSM` for LG, `SAM` for Samsung).
545    pub manufacturer: Option<crate::manufacture::ManufacturerId>,
546    /// Manufacture date or model year.
547    pub manufacture_date: Option<crate::manufacture::ManufactureDate>,
548    /// EDID specification version and revision.
549    pub edid_version: Option<EdidVersion>,
550    /// Manufacturer-assigned product code.
551    pub product_code: Option<u16>,
552    /// Manufacturer-assigned serial number, if encoded numerically in the base block.
553    pub serial_number: Option<u32>,
554    /// Serial number string from the monitor serial number descriptor (`0xFF`), if present.
555    pub serial_number_string: Option<crate::manufacture::MonitorString>,
556    /// Human-readable display name from the monitor name descriptor, if present.
557    pub display_name: Option<crate::manufacture::MonitorString>,
558    /// Unspecified ASCII text strings from `0xFE` descriptors, in descriptor slot order.
559    ///
560    /// Up to four entries (one per descriptor slot). Each slot is `None` if the corresponding
561    /// descriptor was not a `0xFE` entry.
562    pub unspecified_text: [Option<crate::manufacture::MonitorString>; 4],
563    /// Additional white points from the `0xFB` descriptor.
564    ///
565    /// Up to two entries (the EDID `0xFB` descriptor has two fixed slots). Each slot is
566    /// `None` if the corresponding entry was unused (index byte `0x00`).
567    pub white_points: [Option<crate::color::WhitePoint>; 2],
568    /// `true` if the display uses a digital input interface.
569    pub digital: bool,
570    /// Color bit depth per primary channel.
571    /// `None` for analog displays or when the field is undefined or reserved.
572    pub color_bit_depth: Option<crate::color::ColorBitDepth>,
573    /// Physical display technology (e.g. TFT, OLED, PDP).
574    /// `None` when the Display Device Data Block is absent.
575    pub display_technology: Option<crate::panel::DisplayTechnology>,
576    /// Technology-specific sub-type code (raw, 0–15).
577    /// `None` when the Display Device Data Block is absent.
578    pub display_subtype: Option<u8>,
579    /// Panel operating mode (continuous or non-continuous refresh).
580    /// `None` when the Display Device Data Block is absent.
581    pub operating_mode: Option<crate::panel::OperatingMode>,
582    /// Backlight type.
583    /// `None` when the Display Device Data Block is absent.
584    pub backlight_type: Option<crate::panel::BacklightType>,
585    /// Whether the panel uses a Data Enable (DE) signal.
586    /// `None` when the Display Device Data Block is absent.
587    pub data_enable_used: Option<bool>,
588    /// Data Enable signal polarity: `true` = positive, `false` = negative.
589    /// Valid only when `data_enable_used` is `Some(true)`.
590    /// `None` when the Display Device Data Block is absent.
591    pub data_enable_positive: Option<bool>,
592    /// Native pixel format `(width_px, height_px)`.
593    /// `None` when the Display Device Data Block is absent or either dimension is zero.
594    pub native_pixels: Option<(u16, u16)>,
595    /// Panel aspect ratio encoded as `(AR − 1) × 100` (raw byte).
596    /// For example `77` represents approximately 16:9 (AR ≈ 1.77). `None` when the block is absent.
597    pub panel_aspect_ratio_100: Option<u8>,
598    /// Physical mounting orientation of the panel.
599    /// `None` when the Display Device Data Block is absent.
600    pub physical_orientation: Option<crate::panel::PhysicalOrientation>,
601    /// Panel rotation capability.
602    /// `None` when the Display Device Data Block is absent.
603    pub rotation_capability: Option<crate::panel::RotationCapability>,
604    /// Location of the zero (origin) pixel in the framebuffer.
605    /// `None` when the Display Device Data Block is absent.
606    pub zero_pixel_location: Option<crate::panel::ZeroPixelLocation>,
607    /// Fast-scan direction relative to H-sync.
608    /// `None` when the Display Device Data Block is absent.
609    pub scan_direction: Option<crate::panel::ScanDirection>,
610    /// Sub-pixel color filter arrangement.
611    /// `None` when the Display Device Data Block is absent.
612    pub subpixel_layout: Option<crate::panel::SubpixelLayout>,
613    /// Pixel pitch `(horizontal_hundredths_mm, vertical_hundredths_mm)` in 0.01 mm units.
614    /// `None` when the Display Device Data Block is absent or either pitch is zero.
615    pub pixel_pitch_hundredths_mm: Option<(u8, u8)>,
616    /// Pixel response time in milliseconds.
617    /// `None` when the Display Device Data Block is absent or the value is zero.
618    pub pixel_response_time_ms: Option<u8>,
619    /// Interface power sequencing timing parameters.
620    /// `None` when the Interface Power Sequencing Block is absent.
621    pub power_sequencing: Option<crate::panel::PowerSequencing>,
622    /// Display luminance transfer function.
623    /// `None` when the Transfer Characteristics Block is absent.
624    #[cfg(any(feature = "alloc", feature = "std"))]
625    pub transfer_characteristic: Option<crate::transfer::DisplayIdTransferCharacteristic>,
626    /// Physical display interface capabilities.
627    /// `None` when the Display Interface Data Block is absent.
628    pub display_id_interface: Option<crate::panel::DisplayIdInterface>,
629    /// Stereo display interface parameters.
630    /// `None` when the Stereo Display Interface Data Block is absent.
631    pub stereo_interface: Option<crate::panel::DisplayIdStereoInterface>,
632    /// Tiled display topology.
633    /// `None` when the Tiled Display Topology Data Block is absent.
634    pub tiled_topology: Option<crate::panel::DisplayIdTiledTopology>,
635    /// CIE xy chromaticity coordinates for the color primaries and white point.
636    pub chromaticity: crate::color::Chromaticity,
637    /// Display gamma. `None` if the display did not specify a gamma value.
638    pub gamma: Option<crate::color::DisplayGamma>,
639    /// Display feature support flags.
640    pub display_features: Option<crate::features::DisplayFeatureFlags>,
641    /// Supported color encoding formats. Only populated for EDID 1.4+ digital displays.
642    pub digital_color_encoding: Option<crate::color::DigitalColorEncoding>,
643    /// Color type for analog displays; `None` for the undefined value (`0b11`).
644    pub analog_color_type: Option<crate::color::AnalogColorType>,
645    /// Video interface type.
646    /// `None` for analog displays or when the field is undefined or reserved.
647    pub video_interface: Option<crate::input::VideoInterface>,
648    /// Analog sync and video white levels. Only populated for analog displays.
649    pub analog_sync_level: Option<crate::input::AnalogSyncLevel>,
650    /// Physical screen dimensions or aspect ratio.
651    /// `None` when both bytes are zero (undefined).
652    pub screen_size: Option<crate::screen::ScreenSize>,
653    /// Minimum supported vertical refresh rate in Hz.
654    pub min_v_rate: Option<u16>,
655    /// Maximum supported vertical refresh rate in Hz.
656    pub max_v_rate: Option<u16>,
657    /// Minimum supported horizontal scan rate in kHz.
658    pub min_h_rate_khz: Option<u16>,
659    /// Maximum supported horizontal scan rate in kHz.
660    pub max_h_rate_khz: Option<u16>,
661    /// Maximum pixel clock in MHz.
662    pub max_pixel_clock_mhz: Option<u16>,
663    /// Physical image area dimensions in millimetres `(width_mm, height_mm)`.
664    ///
665    /// More precise than [`screen_size`][Self::screen_size] (which is in cm).
666    /// `None` when all DTD image-size fields are zero.
667    pub preferred_image_size_mm: Option<(u16, u16)>,
668    /// Video timing formula reported in the display range limits descriptor.
669    pub timing_formula: Option<crate::timing::TimingFormula>,
670    /// DCM polynomial coefficients.
671    pub color_management: Option<crate::color::ColorManagementData>,
672    /// Video modes decoded from the display data.
673    #[cfg(any(feature = "alloc", feature = "std"))]
674    pub supported_modes: crate::prelude::Vec<VideoMode>,
675    /// Non-fatal conditions collected from the parser and all handlers.
676    ///
677    /// Not serialized — use a custom handler to map warnings to a serializable form.
678    #[cfg(any(feature = "alloc", feature = "std"))]
679    #[cfg_attr(feature = "serde", serde(skip))]
680    pub warnings: crate::prelude::Vec<ParseWarning>,
681    /// Typed data attached by extension handlers, keyed by extension tag byte.
682    ///
683    /// Uses a `Vec` of `(tag, data)` pairs rather than a `HashMap` so that this field is
684    /// available in `alloc`-only (no_std) builds. The number of distinct extension tags in
685    /// any real EDID is small enough that linear scan is negligible.
686    ///
687    /// Not serialized — use a custom handler to map this to a serializable form.
688    #[cfg(any(feature = "alloc", feature = "std"))]
689    #[cfg_attr(feature = "serde", serde(skip))]
690    pub extension_data: crate::prelude::Vec<(u8, crate::prelude::Arc<dyn ExtensionData>)>,
691}
692
693#[cfg(any(feature = "alloc", feature = "std"))]
694impl DisplayCapabilities {
695    /// Returns an iterator over all collected warnings.
696    pub fn iter_warnings(&self) -> impl Iterator<Item = &ParseWarning> {
697        self.warnings.iter()
698    }
699
700    /// Appends a warning, wrapping it in a [`ParseWarning`].
701    pub fn push_warning(&mut self, w: impl core::error::Error + Send + Sync + 'static) {
702        self.warnings.push(crate::prelude::Arc::new(w));
703    }
704
705    /// Store typed data from a handler, keyed by an extension tag.
706    /// Replaces any previously stored entry for the same tag.
707    pub fn set_extension_data<T: ExtensionData>(&mut self, tag: u8, data: T) {
708        if let Some(entry) = self.extension_data.iter_mut().find(|(t, _)| *t == tag) {
709            entry.1 = crate::prelude::Arc::new(data);
710        } else {
711            self.extension_data
712                .push((tag, crate::prelude::Arc::new(data)));
713        }
714    }
715
716    /// Retrieve typed data previously stored by a handler for the given tag.
717    /// Returns `None` if no data is stored for the tag or the type does not match.
718    pub fn get_extension_data<T: core::any::Any>(&self, tag: u8) -> Option<&T> {
719        self.extension_data
720            .iter()
721            .find(|(t, _)| *t == tag)
722            // `**data` deref-chains through `&` then through Arc's Deref to reach
723            // `dyn ExtensionData`, forcing vtable dispatch for `as_any()`.
724            // Calling `.as_any()` on `&Arc<dyn ExtensionData>` would hit the blanket
725            // `ExtensionData` impl for Arc itself and return the wrong TypeId.
726            .and_then(|(_, data)| (**data).as_any().downcast_ref::<T>())
727    }
728
729    /// Removes the extension data entry for `tag` and returns it as `T`.
730    ///
731    /// Intended for take-mutate-restore patterns where multiple input sources contribute
732    /// to a single extension's capability struct (e.g. CTA-861 data delivered both via
733    /// the CEA-861 extension block and via the DisplayID 2.x CTA DisplayID block 0x81).
734    /// The caller mutates the returned value and stores it back with
735    /// [`set_extension_data`][Self::set_extension_data].
736    ///
737    /// Returns `None` if no entry exists for `tag` or the stored type is not `T`.
738    /// When the type does not match, the entry is left in place.
739    pub fn take_extension_data<T: ExtensionData + Clone>(&mut self, tag: u8) -> Option<T> {
740        let pos = self.extension_data.iter().position(|(t, _)| *t == tag)?;
741        // Peek before removing — type mismatch must not destroy the entry.
742        (*self.extension_data[pos].1).as_any().downcast_ref::<T>()?;
743        let (_, arc) = self.extension_data.remove(pos);
744        (*arc).as_any().downcast_ref::<T>().cloned()
745    }
746}
747
748#[cfg(test)]
749mod refresh_rate_tests {
750    use super::*;
751
752    #[test]
753    fn integral_has_unit_denominator() {
754        let r = RefreshRate::integral(60);
755        assert_eq!(r.numer(), 60);
756        assert_eq!(r.denom(), 1);
757    }
758
759    #[test]
760    fn fractional_reduces_to_lowest_terms() {
761        let r = RefreshRate::fractional(120, 2);
762        assert_eq!(r.numer(), 60);
763        assert_eq!(r.denom(), 1);
764
765        let ntsc = RefreshRate::fractional(60000, 1001);
766        assert_eq!(ntsc.numer(), 60000);
767        assert_eq!(ntsc.denom(), 1001);
768    }
769
770    #[test]
771    #[should_panic(expected = "RefreshRate denominator must not be zero")]
772    fn fractional_panics_on_zero_denominator() {
773        let _ = RefreshRate::fractional(60, 0);
774    }
775
776    #[test]
777    fn equality_is_canonical() {
778        assert_eq!(RefreshRate::integral(60), RefreshRate::fractional(120, 2));
779        assert_ne!(
780            RefreshRate::integral(60),
781            RefreshRate::fractional(60000, 1001)
782        );
783    }
784
785    #[test]
786    fn ord_uses_cross_multiplication() {
787        let ntsc = RefreshRate::fractional(60000, 1001);
788        let sixty = RefreshRate::integral(60);
789        let fiftynine = RefreshRate::integral(59);
790        assert!(ntsc < sixty);
791        assert!(ntsc > fiftynine);
792        assert_eq!(
793            sixty.cmp(&RefreshRate::fractional(120, 2)),
794            core::cmp::Ordering::Equal
795        );
796    }
797
798    #[test]
799    #[cfg(any(feature = "alloc", feature = "std"))]
800    fn display_formats_integer_and_fractional() {
801        extern crate alloc;
802        use alloc::format;
803        assert_eq!(format!("{}", RefreshRate::integral(60)), "60 Hz");
804        assert_eq!(
805            format!("{}", RefreshRate::fractional(60000, 1001)),
806            "60000/1001 Hz"
807        );
808    }
809
810    #[test]
811    fn from_integer_uses_integral() {
812        let from_u32: RefreshRate = 144u32.into();
813        let from_u16: RefreshRate = 60u16.into();
814        assert_eq!(from_u32, RefreshRate::integral(144));
815        assert_eq!(from_u16, RefreshRate::integral(60));
816    }
817
818    #[test]
819    fn as_f64_normalises() {
820        let delta = RefreshRate::fractional(60000, 1001).as_f64() - 59.94;
821        assert!(delta.abs() < 0.01);
822    }
823
824    #[test]
825    fn from_ratio_reduces_large_values() {
826        // 1080p@59.94: pc = 148_352 kHz, h_total × v_total = 2200 × 1125 = 2_475_000
827        // 148_352_000 / 2_475_000 = 59.9402… (non-canonical reduction).
828        let r = RefreshRate::from_ratio(148_352_000, 2_475_000).unwrap();
829        // gcd(148_352_000, 2_475_000) = 1000 → 148_352 / 2_475
830        assert_eq!(r.numer(), 148_352);
831        assert_eq!(r.denom(), 2_475);
832    }
833
834    #[test]
835    fn from_ratio_canonicalises_integer_rate() {
836        // 60 Hz exact: pc = 148_500 kHz, total = 2_475_000 → 148_500_000 / 2_475_000 = 60.
837        let r = RefreshRate::from_ratio(148_500_000, 2_475_000).unwrap();
838        assert_eq!(r, RefreshRate::integral(60));
839    }
840
841    #[test]
842    fn from_ratio_returns_none_on_zero_denominator() {
843        assert_eq!(RefreshRate::from_ratio(60, 0), None);
844    }
845
846    #[test]
847    fn from_ratio_handles_zero_numerator() {
848        let r = RefreshRate::from_ratio(0, 1000).unwrap();
849        assert_eq!(r, RefreshRate::integral(0));
850    }
851
852    #[test]
853    fn from_ratio_returns_none_when_reduced_exceeds_u32() {
854        // Coprime values both ≥ 2^32 cannot reduce into u32.
855        let big = u64::from(u32::MAX) + 2;
856        assert_eq!(RefreshRate::from_ratio(big, big - 1), None);
857    }
858}
859
860#[cfg(test)]
861#[cfg(any(feature = "alloc", feature = "std"))]
862mod extension_data_tests {
863    use super::*;
864
865    #[derive(Debug, Clone, PartialEq)]
866    struct Foo(u32);
867
868    #[derive(Debug, Clone, PartialEq)]
869    struct Bar(u32);
870
871    #[test]
872    fn take_extension_data_returns_and_removes_entry() {
873        let mut caps = DisplayCapabilities::default();
874        caps.set_extension_data(0x70, Foo(42));
875        let taken: Foo = caps.take_extension_data(0x70).expect("entry present");
876        assert_eq!(taken, Foo(42));
877        assert!(caps.get_extension_data::<Foo>(0x70).is_none());
878    }
879
880    #[test]
881    fn take_extension_data_returns_none_for_missing_tag() {
882        let mut caps = DisplayCapabilities::default();
883        assert!(caps.take_extension_data::<Foo>(0x70).is_none());
884    }
885
886    #[test]
887    fn take_extension_data_leaves_entry_on_type_mismatch() {
888        let mut caps = DisplayCapabilities::default();
889        caps.set_extension_data(0x70, Foo(7));
890        // Wrong type — must return None and not destroy the entry.
891        assert!(caps.take_extension_data::<Bar>(0x70).is_none());
892        // Entry still retrievable as the original type.
893        assert_eq!(caps.get_extension_data::<Foo>(0x70), Some(&Foo(7)));
894    }
895
896    #[test]
897    fn take_extension_data_round_trip_via_set() {
898        let mut caps = DisplayCapabilities::default();
899        caps.set_extension_data(0x70, Foo(1));
900        let mut foo: Foo = caps.take_extension_data(0x70).unwrap();
901        foo.0 += 1;
902        caps.set_extension_data(0x70, foo);
903        assert_eq!(caps.get_extension_data::<Foo>(0x70), Some(&Foo(2)));
904    }
905}