ironrdp_displaycontrol/pdu/
mod.rs

1//! Display Update Virtual Channel Extension PDUs  [MS-RDPEDISP][1] implementation.
2//!
3//! [1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/d2954508-f487-48bc-8731-39743e0854a9
4
5use ironrdp_core::{
6    cast_length, ensure_fixed_part_size, invalid_field_err, Decode, DecodeResult, Encode, EncodeResult, ReadCursor,
7    WriteCursor,
8};
9use ironrdp_dvc::DvcEncode;
10use tracing::warn;
11
12const DISPLAYCONTROL_PDU_TYPE_CAPS: u32 = 0x00000005;
13const DISPLAYCONTROL_PDU_TYPE_MONITOR_LAYOUT: u32 = 0x00000002;
14
15const DISPLAYCONTROL_MONITOR_PRIMARY: u32 = 0x00000001;
16
17// Set out expectations about supported PDU values. 1024 monitors with 8k*8k pixel area is
18// already excessive, (this extension only supports displays up to 8k*8k) therefore we could safely
19// use those limits to detect ill-formed PDUs and set out invariants.
20const MAX_SUPPORTED_MONITORS: u16 = 1024;
21const MAX_MONITOR_AREA_FACTOR: u16 = 1024 * 16;
22
23/// Display Update Virtual Channel message (PDU prefixed with `DISPLAYCONTROL_HEADER`)
24///
25/// INVARIANTS: size of encoded inner PDU is always less than `u32::MAX - Self::FIXED_PART_SIZE`
26///     (See [`DisplayControlCapabilities`] & [`DisplayControlMonitorLayout`] invariants)
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum DisplayControlPdu {
29    Caps(DisplayControlCapabilities),
30    MonitorLayout(DisplayControlMonitorLayout),
31}
32
33impl DisplayControlPdu {
34    const NAME: &'static str = "DISPLAYCONTROL_HEADER";
35    const FIXED_PART_SIZE: usize = 4 /* Type */ + 4 /* Length */;
36}
37
38impl Encode for DisplayControlPdu {
39    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
40        ensure_fixed_part_size!(in: dst);
41
42        let (kind, payload_length) = match self {
43            DisplayControlPdu::Caps(caps) => (DISPLAYCONTROL_PDU_TYPE_CAPS, caps.size()),
44            DisplayControlPdu::MonitorLayout(layout) => (DISPLAYCONTROL_PDU_TYPE_MONITOR_LAYOUT, layout.size()),
45        };
46
47        // This will never overflow as per invariants.
48        #[expect(clippy::arithmetic_side_effects)]
49        let pdu_size = cast_length!("pdu size", payload_length + Self::FIXED_PART_SIZE)?;
50
51        // Write `DISPLAYCONTROL_HEADER` fields.
52        dst.write_u32(kind);
53        dst.write_u32(pdu_size);
54
55        match self {
56            DisplayControlPdu::Caps(caps) => caps.encode(dst),
57            DisplayControlPdu::MonitorLayout(layout) => layout.encode(dst),
58        }?;
59
60        Ok(())
61    }
62
63    fn name(&self) -> &'static str {
64        Self::NAME
65    }
66
67    fn size(&self) -> usize {
68        // As per invariants: This will never overflow.
69        #[expect(clippy::arithmetic_side_effects)]
70        let size = Self::FIXED_PART_SIZE
71            + match self {
72                DisplayControlPdu::Caps(caps) => caps.size(),
73                DisplayControlPdu::MonitorLayout(layout) => layout.size(),
74            };
75
76        size
77    }
78}
79
80impl DvcEncode for DisplayControlPdu {}
81
82impl<'de> Decode<'de> for DisplayControlPdu {
83    fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
84        ensure_fixed_part_size!(in: src);
85
86        // Read `DISPLAYCONTROL_HEADER` fields.
87        let kind = src.read_u32();
88        let pdu_length = src.read_u32();
89
90        let _payload_length = pdu_length
91            .checked_sub(Self::FIXED_PART_SIZE.try_into().expect("always in range"))
92            .ok_or_else(|| invalid_field_err!("Length", "Display control PDU length is too small"))?;
93
94        match kind {
95            DISPLAYCONTROL_PDU_TYPE_CAPS => {
96                let caps = DisplayControlCapabilities::decode(src)?;
97                Ok(DisplayControlPdu::Caps(caps))
98            }
99            DISPLAYCONTROL_PDU_TYPE_MONITOR_LAYOUT => {
100                let layout = DisplayControlMonitorLayout::decode(src)?;
101                Ok(DisplayControlPdu::MonitorLayout(layout))
102            }
103            _ => Err(invalid_field_err!("Type", "Unknown display control PDU type")),
104        }
105    }
106}
107
108impl From<DisplayControlCapabilities> for DisplayControlPdu {
109    fn from(caps: DisplayControlCapabilities) -> Self {
110        Self::Caps(caps)
111    }
112}
113
114impl From<DisplayControlMonitorLayout> for DisplayControlPdu {
115    fn from(layout: DisplayControlMonitorLayout) -> Self {
116        Self::MonitorLayout(layout)
117    }
118}
119
120/// 2.2.2.1 DISPLAYCONTROL_CAPS_PDU
121///
122/// Display control channel capabilities PDU.
123///
124/// INVARIANTS:
125///     0 <= max_num_monitors <= MAX_SUPPORTED_MONITORS
126///     0 <= max_monitor_area_factor_a <= MAX_MONITOR_AREA_FACTOR
127///     0 <= max_monitor_area_factor_b <= MAX_MONITOR_AREA_FACTOR
128///
129/// [2.2.2.1]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/8989a211-984e-4ecc-80f3-60694fc4b476
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct DisplayControlCapabilities {
132    max_num_monitors: u32,
133    max_monitor_area_factor_a: u32,
134    max_monitor_area_factor_b: u32,
135    max_monitor_area: u64,
136}
137
138impl DisplayControlCapabilities {
139    const NAME: &'static str = "DISPLAYCONTROL_CAPS_PDU";
140    const FIXED_PART_SIZE: usize = 4 /* MaxNumMonitors */
141        + 4 /* MaxMonitorAreaFactorA */
142        + 4 /* MaxMonitorAreaFactorB */;
143
144    pub fn new(
145        max_num_monitors: u32,
146        max_monitor_area_factor_a: u32,
147        max_monitor_area_factor_b: u32,
148    ) -> DecodeResult<Self> {
149        let max_monitor_area =
150            calculate_monitor_area(max_num_monitors, max_monitor_area_factor_a, max_monitor_area_factor_b)?;
151
152        Ok(Self {
153            max_num_monitors,
154            max_monitor_area_factor_a,
155            max_monitor_area_factor_b,
156            max_monitor_area,
157        })
158    }
159
160    pub fn max_monitor_area(&self) -> u64 {
161        self.max_monitor_area
162    }
163}
164
165impl Encode for DisplayControlCapabilities {
166    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
167        ensure_fixed_part_size!(in: dst);
168        dst.write_u32(self.max_num_monitors);
169        dst.write_u32(self.max_monitor_area_factor_a);
170        dst.write_u32(self.max_monitor_area_factor_b);
171
172        Ok(())
173    }
174
175    fn name(&self) -> &'static str {
176        Self::NAME
177    }
178
179    fn size(&self) -> usize {
180        Self::FIXED_PART_SIZE
181    }
182}
183
184impl<'de> Decode<'de> for DisplayControlCapabilities {
185    fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
186        ensure_fixed_part_size!(in: src);
187
188        let max_num_monitors = src.read_u32();
189        let max_monitor_area_factor_a = src.read_u32();
190        let max_monitor_area_factor_b = src.read_u32();
191
192        let max_monitor_area =
193            calculate_monitor_area(max_num_monitors, max_monitor_area_factor_a, max_monitor_area_factor_b)?;
194
195        Ok(Self {
196            max_num_monitors,
197            max_monitor_area_factor_a,
198            max_monitor_area_factor_b,
199            max_monitor_area,
200        })
201    }
202}
203
204/// [2.2.2.2] DISPLAYCONTROL_MONITOR_LAYOUT_PDU
205///
206/// Sent from client to server to notify about new monitor layout (e.g screen resize).
207///
208/// INVARIANTS:
209///     0 <= monitors.length() <= MAX_SUPPORTED_MONITORS
210///
211/// [2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/22741217-12a0-4fb8-b5a0-df43905aaf06
212#[derive(Debug, Clone, PartialEq, Eq)]
213pub struct DisplayControlMonitorLayout {
214    monitors: Vec<MonitorLayoutEntry>,
215}
216
217impl DisplayControlMonitorLayout {
218    const NAME: &'static str = "DISPLAYCONTROL_MONITOR_LAYOUT_PDU";
219    const FIXED_PART_SIZE: usize = 4 /* MonitorLayoutSize */ + 4 /* NumMonitors */;
220
221    pub fn new(monitors: &[MonitorLayoutEntry]) -> EncodeResult<Self> {
222        if monitors.len() > MAX_SUPPORTED_MONITORS.into() {
223            return Err(invalid_field_err!("NumMonitors", "Too many monitors",));
224        }
225
226        let primary_monitors_count = monitors.iter().filter(|monitor| monitor.is_primary()).count();
227
228        if primary_monitors_count != 1 {
229            return Err(invalid_field_err!(
230                "PrimaryMonitor",
231                "There must be exactly one primary monitor"
232            ));
233        }
234
235        Ok(Self {
236            monitors: monitors.to_vec(),
237        })
238    }
239
240    /// Creates a new [`DisplayControlMonitorLayout`] with a single primary monitor
241    ///
242    /// Per [2.2.2.2.1]:
243    /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
244    /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
245    /// - The `scale_factor` MUST be ignored if it is less than 100 percent or greater than 500 percent.
246    /// - The `physical_dims` (width, height) MUST be ignored if either is less than 10 mm or greater than 10,000 mm.
247    ///
248    /// Use [`MonitorLayoutEntry::adjust_display_size`] to adjust `width` and `height` before calling this function
249    /// to ensure the display size is within the valid range.
250    ///
251    /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
252    pub fn new_single_primary_monitor(
253        width: u32,
254        height: u32,
255        scale_factor: Option<u32>,
256        physical_dims: Option<(u32, u32)>,
257    ) -> EncodeResult<Self> {
258        let entry = MonitorLayoutEntry::new_primary(width, height)?.with_orientation(if width > height {
259            MonitorOrientation::Landscape
260        } else {
261            MonitorOrientation::Portrait
262        });
263
264        let entry = if let Some(scale_factor) = scale_factor {
265            entry
266                .with_desktop_scale_factor(scale_factor)?
267                .with_device_scale_factor(DeviceScaleFactor::Scale100Percent)
268        } else {
269            entry
270        };
271
272        let entry = if let Some((physical_width, physical_height)) = physical_dims {
273            entry.with_physical_dimensions(physical_width, physical_height)?
274        } else {
275            entry
276        };
277
278        DisplayControlMonitorLayout::new(&[entry])
279    }
280
281    pub fn monitors(&self) -> &[MonitorLayoutEntry] {
282        &self.monitors
283    }
284}
285
286impl Encode for DisplayControlMonitorLayout {
287    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
288        ensure_fixed_part_size!(in: dst);
289
290        dst.write_u32(MonitorLayoutEntry::FIXED_PART_SIZE.try_into().expect("always in range"));
291
292        let monitors_count: u32 = self
293            .monitors
294            .len()
295            .try_into()
296            .map_err(|_| invalid_field_err!("NumMonitors", "Number of monitors is too big"))?;
297
298        dst.write_u32(monitors_count);
299
300        for monitor in &self.monitors {
301            monitor.encode(dst)?;
302        }
303
304        Ok(())
305    }
306
307    fn name(&self) -> &'static str {
308        Self::NAME
309    }
310
311    fn size(&self) -> usize {
312        // As per invariants: This will never overflow:
313        // 0 <= Self::FIXED_PART_SIZE + MAX_SUPPORTED_MONITORS * MonitorLayoutEntry::FIXED_PART_SIZE < u16::MAX
314        #[expect(clippy::arithmetic_side_effects)]
315        let size = Self::FIXED_PART_SIZE + self.monitors.iter().map(|monitor| monitor.size()).sum::<usize>();
316
317        size
318    }
319}
320
321impl<'de> Decode<'de> for DisplayControlMonitorLayout {
322    fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
323        ensure_fixed_part_size!(in: src);
324
325        let monitor_layout_size = src.read_u32();
326
327        if monitor_layout_size != MonitorLayoutEntry::FIXED_PART_SIZE.try_into().expect("always in range") {
328            return Err(invalid_field_err!(
329                "MonitorLayoutSize",
330                "Monitor layout size is invalid"
331            ));
332        }
333
334        let num_monitors = cast_length!("number of monitors", src.read_u32())?;
335
336        if num_monitors > MAX_SUPPORTED_MONITORS.into() {
337            return Err(invalid_field_err!("NumMonitors", "Too many monitors"));
338        }
339
340        let mut monitors = Vec::with_capacity(num_monitors);
341        for _ in 0..num_monitors {
342            let monitor = MonitorLayoutEntry::decode(src)?;
343            monitors.push(monitor);
344        }
345
346        Ok(Self { monitors })
347    }
348}
349
350/// [2.2.2.2.1] DISPLAYCONTROL_MONITOR_LAYOUT_PDU
351///
352/// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
353#[derive(Debug, Clone, PartialEq, Eq)]
354pub struct MonitorLayoutEntry {
355    is_primary: bool,
356    left: i32,
357    top: i32,
358    width: u32,
359    height: u32,
360    physical_width: u32,
361    physical_height: u32,
362    orientation: u32,
363    desktop_scale_factor: u32,
364    device_scale_factor: u32,
365}
366
367macro_rules! validate_dimensions {
368    ($width:expr, $height:expr) => {{
369        if !(200..=8192).contains(&$width) {
370            return Err(invalid_field_err!("Width", "Monitor width is out of range"));
371        }
372        if $width % 2 != 0 {
373            return Err(invalid_field_err!("Width", "Monitor width cannot be odd"));
374        }
375        if !(200..=8192).contains(&$height) {
376            return Err(invalid_field_err!("Height", "Monitor height is out of range"));
377        }
378        Ok(())
379    }};
380}
381
382impl MonitorLayoutEntry {
383    const FIXED_PART_SIZE: usize = 4 /* Flags */
384        + 4 /* Left */
385        + 4 /* Top */
386        + 4 /* Width */
387        + 4 /* Height */
388        + 4 /* PhysicalWidth */
389        + 4 /* PhysicalHeight */
390        + 4 /* Orientation */
391        + 4 /* DesktopScaleFactor */
392        + 4 /* DeviceScaleFactor */;
393
394    const NAME: &'static str = "DISPLAYCONTROL_MONITOR_LAYOUT";
395
396    /// Creates a new [`MonitorLayoutEntry`].
397    ///
398    /// Per [2.2.2.2.1]:
399    /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
400    /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
401    ///
402    /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
403    fn new_impl(mut width: u32, height: u32) -> EncodeResult<Self> {
404        if width % 2 != 0 {
405            let prev_width = width;
406            width = width.saturating_sub(1);
407            warn!(
408                "Monitor width cannot be odd, adjusting from [{}] to [{}]",
409                prev_width, width
410            )
411        }
412
413        validate_dimensions!(width, height)?;
414
415        Ok(Self {
416            is_primary: false,
417            left: 0,
418            top: 0,
419            width,
420            height,
421            physical_width: 0,
422            physical_height: 0,
423            orientation: 0,
424            desktop_scale_factor: 0,
425            device_scale_factor: 0,
426        })
427    }
428
429    /// Adjusts the display size to be within the valid range.
430    ///
431    /// Per [2.2.2.2.1]:
432    /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
433    /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
434    ///
435    /// Functions that create [`MonitorLayoutEntry`] should typically use this function to adjust the display size first.
436    ///
437    /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
438    pub fn adjust_display_size(width: u32, height: u32) -> (u32, u32) {
439        fn constrain(value: u32) -> u32 {
440            value.clamp(200, 8192)
441        }
442
443        let mut width = width;
444        if width % 2 != 0 {
445            width = width.saturating_sub(1);
446        }
447
448        (constrain(width), constrain(height))
449    }
450
451    /// Creates a new primary [`MonitorLayoutEntry`].
452    ///
453    /// Per [2.2.2.2.1]:
454    /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
455    /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
456    ///
457    /// Use [`MonitorLayoutEntry::adjust_display_size`] before calling this function to ensure the display size is within the valid range.
458    ///
459    /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
460    pub fn new_primary(width: u32, height: u32) -> EncodeResult<Self> {
461        let mut entry = Self::new_impl(width, height)?;
462        entry.is_primary = true;
463        Ok(entry)
464    }
465
466    /// Creates a new primary [`MonitorLayoutEntry`].
467    ///
468    /// Per [2.2.2.2.1]:
469    /// - The `width` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels, and MUST NOT be an odd value.
470    /// - The `height` MUST be greater than or equal to 200 pixels and less than or equal to 8192 pixels.
471    ///
472    /// Use [`MonitorLayoutEntry::adjust_display_size`] before calling this function to ensure the display size is within the valid range.
473    ///
474    /// [2.2.2.2.2]: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-rdpedisp/ea2de591-9203-42cd-9908-be7a55237d1c
475    pub fn new_secondary(width: u32, height: u32) -> EncodeResult<Self> {
476        Self::new_impl(width, height)
477    }
478
479    /// Sets the monitor's orientation. (Default is [`MonitorOrientation::Landscape`])
480    #[must_use]
481    pub fn with_orientation(mut self, orientation: MonitorOrientation) -> Self {
482        self.orientation = orientation.angle();
483        self
484    }
485
486    /// Sets the monitor's position (left, top) in pixels. (Default is (0, 0))
487    ///
488    /// Note: The primary monitor position must be always (0, 0).
489    pub fn with_position(mut self, left: i32, top: i32) -> EncodeResult<Self> {
490        validate_position(left, top, self.is_primary)?;
491
492        self.left = left;
493        self.top = top;
494
495        Ok(self)
496    }
497
498    /// Sets the monitor's device scale factor in percent. (Default is [`DeviceScaleFactor::Scale100Percent`])
499    #[must_use]
500    pub fn with_device_scale_factor(mut self, device_scale_factor: DeviceScaleFactor) -> Self {
501        self.device_scale_factor = device_scale_factor.value();
502        self
503    }
504
505    /// Sets the monitor's desktop scale factor in percent.
506    ///
507    /// NOTE: As specified in [MS-RDPEDISP], if the desktop scale factor is not in the valid range
508    /// (100..=500 percent), the monitor desktop scale factor is considered invalid and should be ignored.
509    pub fn with_desktop_scale_factor(mut self, desktop_scale_factor: u32) -> EncodeResult<Self> {
510        validate_desktop_scale_factor(desktop_scale_factor)?;
511
512        self.desktop_scale_factor = desktop_scale_factor;
513        Ok(self)
514    }
515
516    /// Sets the monitor's physical dimensions in millimeters.
517    ///
518    /// NOTE: As specified in [MS-RDPEDISP], if the physical dimensions are not in the valid range
519    /// (10..=10000 millimeters), the monitor physical dimensions are considered invalid and
520    /// should be ignored.
521    pub fn with_physical_dimensions(mut self, physical_width: u32, physical_height: u32) -> EncodeResult<Self> {
522        validate_physical_dimensions(physical_width, physical_height)?;
523
524        self.physical_width = physical_width;
525        self.physical_height = physical_height;
526        Ok(self)
527    }
528
529    pub fn is_primary(&self) -> bool {
530        self.is_primary
531    }
532
533    /// Returns the monitor's position (left, top) in pixels.
534    pub fn position(&self) -> Option<(i32, i32)> {
535        validate_position(self.left, self.top, self.is_primary).ok()?;
536
537        Some((self.left, self.top))
538    }
539
540    /// Returns the monitor's dimensions (width, height) in pixels.
541    pub fn dimensions(&self) -> (u32, u32) {
542        (self.width, self.height)
543    }
544
545    /// Returns the monitor's orientation if it is valid.
546    ///
547    /// NOTE: As specified in [MS-RDPEDISP], if the orientation is not one of the valid values
548    /// (0, 90, 180, 270), the monitor orientation is considered invalid and should be ignored.
549    pub fn orientation(&self) -> Option<MonitorOrientation> {
550        MonitorOrientation::from_angle(self.orientation)
551    }
552
553    /// Returns the monitor's physical dimensions (width, height) in millimeters.
554    ///
555    /// NOTE: As specified in [MS-RDPEDISP], if the physical dimensions are not in the valid range
556    /// (10..=10000 millimeters), the monitor physical dimensions are considered invalid and
557    /// should be ignored.
558    pub fn physical_dimensions(&self) -> Option<(u32, u32)> {
559        validate_physical_dimensions(self.physical_width, self.physical_height).ok()?;
560        Some((self.physical_width, self.physical_height))
561    }
562
563    /// Returns the monitor's device scale factor in percent if it is valid.
564    ///
565    /// NOTE: As specified in [MS-RDPEDISP], if the desktop scale factor is not in the valid range
566    /// (100..=500 percent), the monitor desktop scale factor is considered invalid and should be ignored.
567    ///
568    /// IMPORTANT: When processing scale factors, make sure that both desktop and device scale factors
569    /// are valid, otherwise they both should be ignored.
570    pub fn desktop_scale_factor(&self) -> Option<u32> {
571        validate_desktop_scale_factor(self.desktop_scale_factor).ok()?;
572
573        Some(self.desktop_scale_factor)
574    }
575
576    /// Returns the monitor's device scale factor in percent if it is valid.
577    ///
578    /// IMPORTANT: When processing scale factors, make sure that both desktop and device scale factors
579    /// are valid, otherwise they both should be ignored.
580    pub fn device_scale_factor(&self) -> Option<DeviceScaleFactor> {
581        match self.device_scale_factor {
582            100 => Some(DeviceScaleFactor::Scale100Percent),
583            140 => Some(DeviceScaleFactor::Scale140Percent),
584            180 => Some(DeviceScaleFactor::Scale180Percent),
585            _ => None,
586        }
587    }
588}
589
590impl Encode for MonitorLayoutEntry {
591    fn encode(&self, dst: &mut WriteCursor<'_>) -> EncodeResult<()> {
592        ensure_fixed_part_size!(in: dst);
593
594        let flags = if self.is_primary {
595            DISPLAYCONTROL_MONITOR_PRIMARY
596        } else {
597            0
598        };
599        dst.write_u32(flags);
600        dst.write_i32(self.left);
601        dst.write_i32(self.top);
602        dst.write_u32(self.width);
603        dst.write_u32(self.height);
604        dst.write_u32(self.physical_width);
605        dst.write_u32(self.physical_height);
606        dst.write_u32(self.orientation);
607        dst.write_u32(self.desktop_scale_factor);
608        dst.write_u32(self.device_scale_factor);
609
610        Ok(())
611    }
612
613    fn name(&self) -> &'static str {
614        Self::NAME
615    }
616
617    fn size(&self) -> usize {
618        Self::FIXED_PART_SIZE
619    }
620}
621
622impl<'de> Decode<'de> for MonitorLayoutEntry {
623    fn decode(src: &mut ReadCursor<'de>) -> DecodeResult<Self> {
624        ensure_fixed_part_size!(in: src);
625
626        let flags = src.read_u32();
627        let left = src.read_i32();
628        let top = src.read_i32();
629        let width = src.read_u32();
630        let height = src.read_u32();
631        let physical_width = src.read_u32();
632        let physical_height = src.read_u32();
633        let orientation = src.read_u32();
634        let desktop_scale_factor = src.read_u32();
635        let device_scale_factor = src.read_u32();
636
637        validate_dimensions!(width, height)?;
638
639        Ok(Self {
640            is_primary: flags & DISPLAYCONTROL_MONITOR_PRIMARY != 0,
641            left,
642            top,
643            width,
644            height,
645            physical_width,
646            physical_height,
647            orientation,
648            desktop_scale_factor,
649            device_scale_factor,
650        })
651    }
652}
653
654/// Valid monitor orientations.
655#[derive(Debug, Clone, Copy, PartialEq, Eq)]
656pub enum MonitorOrientation {
657    Landscape,
658    Portrait,
659    LandscapeFlipped,
660    PortraitFlipped,
661}
662
663impl MonitorOrientation {
664    pub fn from_angle(angle: u32) -> Option<Self> {
665        match angle {
666            0 => Some(Self::Landscape),
667            90 => Some(Self::Portrait),
668            180 => Some(Self::LandscapeFlipped),
669            270 => Some(Self::PortraitFlipped),
670            _ => None,
671        }
672    }
673
674    pub fn angle(&self) -> u32 {
675        match self {
676            Self::Landscape => 0,
677            Self::Portrait => 90,
678            Self::LandscapeFlipped => 180,
679            Self::PortraitFlipped => 270,
680        }
681    }
682}
683
684/// Valid device scale factors for monitors.
685#[derive(Debug, Clone, Copy, PartialEq, Eq)]
686pub enum DeviceScaleFactor {
687    Scale100Percent,
688    Scale140Percent,
689    Scale180Percent,
690}
691
692impl DeviceScaleFactor {
693    pub fn value(&self) -> u32 {
694        match self {
695            Self::Scale100Percent => 100,
696            Self::Scale140Percent => 140,
697            Self::Scale180Percent => 180,
698        }
699    }
700}
701
702fn validate_position(left: i32, top: i32, is_primary: bool) -> EncodeResult<()> {
703    if is_primary && (left != 0 || top != 0) {
704        return Err(invalid_field_err!(
705            "Position",
706            "Primary monitor position must be (0, 0)"
707        ));
708    }
709
710    Ok(())
711}
712
713fn validate_desktop_scale_factor(desktop_scale_factor: u32) -> EncodeResult<()> {
714    if !(100..=500).contains(&desktop_scale_factor) {
715        return Err(invalid_field_err!(
716            "DesktopScaleFactor",
717            "Desktop scale factor is out of range"
718        ));
719    }
720
721    Ok(())
722}
723
724fn validate_physical_dimensions(physical_width: u32, physical_height: u32) -> EncodeResult<()> {
725    if !(10..=10000).contains(&physical_width) {
726        return Err(invalid_field_err!("PhysicalWidth", "Physical width is out of range"));
727    }
728    if !(10..=10000).contains(&physical_height) {
729        return Err(invalid_field_err!("PhysicalHeight", "Physical height is out of range"));
730    }
731
732    Ok(())
733}
734
735fn calculate_monitor_area(
736    max_num_monitors: u32,
737    max_monitor_area_factor_a: u32,
738    max_monitor_area_factor_b: u32,
739) -> DecodeResult<u64> {
740    if max_num_monitors > MAX_SUPPORTED_MONITORS.into() {
741        return Err(invalid_field_err!("NumMonitors", "Too many monitors"));
742    }
743
744    if max_monitor_area_factor_a > MAX_MONITOR_AREA_FACTOR.into()
745        || max_monitor_area_factor_b > MAX_MONITOR_AREA_FACTOR.into()
746    {
747        return Err(invalid_field_err!(
748            "MaxMonitorAreaFactor",
749            "Invalid monitor area factor"
750        ));
751    }
752
753    // As per invariants: This multiplication would never overflow.
754    // 0 <= MAX_MONITOR_AREA_FACTOR * MAX_MONITOR_AREA_FACTOR * MAX_SUPPORTED_MONITORS <= u64::MAX
755    #[expect(clippy::arithmetic_side_effects)]
756    Ok(u64::from(max_monitor_area_factor_a) * u64::from(max_monitor_area_factor_b) * u64::from(max_num_monitors))
757}