lgui_core/platform/
dpi.rs1use 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;