Skip to main content

lgui_core/platform/
dpi.rs

1use crate::core::{PhysicalPoint, PhysicalRect, PhysicalSize, Size, UiScale};
2
3pub const BASE_DPI: u32 = 96;
4pub const MIN_READABLE_SCALE: f32 = 0.80;
5const WORK_AREA_MARGIN_LOGICAL: f32 = 12.0;
6const AUTO_TARGET_WIDTH_RATIO: f32 = 0.72;
7const AUTO_TARGET_HEIGHT_RATIO: f32 = 0.75;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq)]
10pub enum ScalePreference {
11    #[default]
12    Auto,
13    Multiplier(f32),
14}
15
16impl ScalePreference {
17    pub fn multiplier(self) -> Option<f32> {
18        match self {
19            Self::Auto => None,
20            Self::Multiplier(value) => Some(value.max(0.1)),
21        }
22    }
23}
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub struct WorkArea {
27    pub rect: PhysicalRect,
28}
29
30impl WorkArea {
31    pub fn size(self) -> PhysicalSize {
32        PhysicalSize::new(self.rect.width().max(1), self.rect.height().max(1))
33    }
34}
35
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct ScaleContext {
38    pub dpi: u32,
39    pub os_scale: f32,
40    pub preference: ScalePreference,
41    pub scale: UiScale,
42    pub work_area: WorkArea,
43    pub preferred_logical_size: Size,
44    pub physical_window_size: PhysicalSize,
45    pub compact: bool,
46}
47
48impl ScaleContext {
49    pub fn resolve(
50        dpi: u32,
51        work_area: WorkArea,
52        preferred_logical_size: Size,
53        preference: ScalePreference,
54    ) -> Self {
55        let dpi = dpi.max(BASE_DPI);
56        let os_scale = dpi as f32 / BASE_DPI as f32;
57        let margin = (WORK_AREA_MARGIN_LOGICAL * os_scale).round() as i32;
58        let available = PhysicalSize::new(
59            (work_area.size().width - margin * 2).max(1),
60            (work_area.size().height - margin * 2).max(1),
61        );
62        let fit_scale = (available.width as f32 / preferred_logical_size.width.max(1.0))
63            .min(available.height as f32 / preferred_logical_size.height.max(1.0));
64        let requested_scale = match preference.multiplier() {
65            Some(multiplier) => os_scale * multiplier,
66            None => {
67                let work_size = work_area.size();
68                let auto_scale = (work_size.width as f32 * AUTO_TARGET_WIDTH_RATIO
69                    / preferred_logical_size.width.max(1.0))
70                .min(
71                    work_size.height as f32 * AUTO_TARGET_HEIGHT_RATIO
72                        / preferred_logical_size.height.max(1.0),
73                );
74                auto_scale.clamp(MIN_READABLE_SCALE, os_scale)
75            }
76        };
77        let effective = requested_scale.min(fit_scale).max(0.1);
78        let scale = UiScale::new(effective);
79        Self {
80            dpi,
81            os_scale,
82            preference,
83            scale,
84            work_area,
85            preferred_logical_size,
86            physical_window_size: scale.physical_size(preferred_logical_size),
87            compact: effective < MIN_READABLE_SCALE,
88        }
89    }
90
91    pub fn same_monitor_metrics(self, other: Self) -> bool {
92        self.dpi == other.dpi
93            && self.work_area == other.work_area
94            && self.preference == other.preference
95    }
96
97    pub fn clamp_origin(self, proposed: PhysicalPoint, size: PhysicalSize) -> PhysicalPoint {
98        let max_x = (self.work_area.rect.right - size.width).max(self.work_area.rect.left);
99        let max_y = (self.work_area.rect.bottom - size.height).max(self.work_area.rect.top);
100        PhysicalPoint::new(
101            proposed.x.clamp(self.work_area.rect.left, max_x),
102            proposed.y.clamp(self.work_area.rect.top, max_y),
103        )
104    }
105}
106
107#[cfg(test)]
108#[path = "dpi_test.rs"]
109mod tests;