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