1use crate::parser::WasmaConfig;
11use crate::wasma_protocol_universal_client_unix_posix_window::{ArchKind, CURRENT_ARCH};
12use crate::window_handling::WindowGeometry;
13use std::sync::Arc;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
21pub enum CoordSpace {
22 LogicalPx,
25
26 PhysicalPx,
29
30 DpiNormalized,
33
34 Points,
36
37 Millimeters,
39}
40
41impl CoordSpace {
42 pub fn name(&self) -> &'static str {
43 match self {
44 Self::LogicalPx => "LogicalPx",
45 Self::PhysicalPx => "PhysicalPx",
46 Self::DpiNormalized => "DpiNormalized",
47 Self::Points => "Points",
48 Self::Millimeters => "Millimeters",
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum DpiPreset {
60 Standard,
62 MacOsStandard,
64 HiDpi2x,
66 HiDpi3x,
68 UltraHd,
70 Custom(f64),
72}
73
74impl DpiPreset {
75 pub fn dpi(&self) -> f64 {
76 match self {
77 Self::Standard => 96.0,
78 Self::MacOsStandard => 96.0,
79 Self::HiDpi2x => 192.0,
80 Self::HiDpi3x => 288.0,
81 Self::UltraHd => 200.0,
82 Self::Custom(v) => *v,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct DpiProfile {
90 pub dpi_x: f64,
92 pub dpi_y: f64,
94 pub scale_factor: f64,
97 pub physical_width_mm: Option<f64>,
99 pub physical_height_mm: Option<f64>,
100 pub screen_width_px: u32,
102 pub screen_height_px: u32,
103}
104
105impl DpiProfile {
106 pub fn new(dpi_x: f64, dpi_y: f64, screen_width_px: u32, screen_height_px: u32) -> Self {
107 let scale_factor = dpi_x / 96.0;
108 Self {
109 dpi_x,
110 dpi_y,
111 scale_factor,
112 physical_width_mm: None,
113 physical_height_mm: None,
114 screen_width_px,
115 screen_height_px,
116 }
117 }
118
119 pub fn from_preset(preset: DpiPreset, screen_width_px: u32, screen_height_px: u32) -> Self {
120 Self::new(
121 preset.dpi(),
122 preset.dpi(),
123 screen_width_px,
124 screen_height_px,
125 )
126 }
127
128 pub fn with_physical_size(mut self) -> Self {
130 self.physical_width_mm = Some((self.screen_width_px as f64 / self.dpi_x) * 25.4);
132 self.physical_height_mm = Some((self.screen_height_px as f64 / self.dpi_y) * 25.4);
133 self
134 }
135
136 pub fn logical_width(&self) -> f64 {
138 self.screen_width_px as f64 / self.scale_factor
139 }
140
141 pub fn logical_height(&self) -> f64 {
142 self.screen_height_px as f64 / self.scale_factor
143 }
144
145 pub fn device_pixel_ratio(&self) -> f64 {
147 self.scale_factor
148 }
149}
150
151impl Default for DpiProfile {
152 fn default() -> Self {
153 Self::new(96.0, 96.0, 1920, 1080)
155 }
156}
157
158#[derive(Debug, Clone, Copy)]
164pub struct CoordValue {
165 pub x: f64,
166 pub y: f64,
167 pub space: CoordSpace,
168}
169
170impl CoordValue {
171 pub fn new(x: f64, y: f64, space: CoordSpace) -> Self {
172 Self { x, y, space }
173 }
174
175 pub fn logical(x: f64, y: f64) -> Self {
176 Self::new(x, y, CoordSpace::LogicalPx)
177 }
178
179 pub fn physical(x: f64, y: f64) -> Self {
180 Self::new(x, y, CoordSpace::PhysicalPx)
181 }
182
183 pub fn dpi_normalized(x: f64, y: f64) -> Self {
184 Self::new(x, y, CoordSpace::DpiNormalized)
185 }
186
187 pub fn round(self) -> Self {
189 Self::new(self.x.round(), self.y.round(), self.space)
190 }
191
192 pub fn floor(self) -> Self {
194 Self::new(self.x.floor(), self.y.floor(), self.space)
195 }
196
197 pub fn ceil(self) -> Self {
199 Self::new(self.x.ceil(), self.y.ceil(), self.space)
200 }
201}
202
203#[derive(Debug, Clone, Copy)]
205pub struct SizeValue {
206 pub width: f64,
207 pub height: f64,
208 pub space: CoordSpace,
209}
210
211impl SizeValue {
212 pub fn new(width: f64, height: f64, space: CoordSpace) -> Self {
213 Self {
214 width,
215 height,
216 space,
217 }
218 }
219
220 pub fn logical(width: f64, height: f64) -> Self {
221 Self::new(width, height, CoordSpace::LogicalPx)
222 }
223
224 pub fn physical(width: f64, height: f64) -> Self {
225 Self::new(width, height, CoordSpace::PhysicalPx)
226 }
227
228 pub fn aspect_ratio(&self) -> f64 {
229 if self.height == 0.0 {
230 0.0
231 } else {
232 self.width / self.height
233 }
234 }
235}
236
237#[derive(Debug, Clone, Copy)]
239pub struct RectValue {
240 pub x: f64,
241 pub y: f64,
242 pub width: f64,
243 pub height: f64,
244 pub space: CoordSpace,
245}
246
247impl RectValue {
248 pub fn new(x: f64, y: f64, width: f64, height: f64, space: CoordSpace) -> Self {
249 Self {
250 x,
251 y,
252 width,
253 height,
254 space,
255 }
256 }
257
258 pub fn from_geometry(geo: &WindowGeometry, space: CoordSpace) -> Self {
259 Self::new(
260 geo.x as f64,
261 geo.y as f64,
262 geo.width as f64,
263 geo.height as f64,
264 space,
265 )
266 }
267
268 pub fn to_geometry(&self) -> WindowGeometry {
269 WindowGeometry {
270 x: self.x.round() as i32,
271 y: self.y.round() as i32,
272 width: self.width.round() as u32,
273 height: self.height.round() as u32,
274 }
275 }
276
277 pub fn right(&self) -> f64 {
278 self.x + self.width
279 }
280 pub fn bottom(&self) -> f64 {
281 self.y + self.height
282 }
283
284 pub fn contains_point(&self, x: f64, y: f64) -> bool {
285 x >= self.x && x <= self.right() && y >= self.y && y <= self.bottom()
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq)]
295pub enum ConversionEngineMode {
296 Soft,
298 Hardware,
300}
301
302impl ConversionEngineMode {
303 pub fn auto() -> Self {
305 if CURRENT_ARCH.supports_simd() {
306 Self::Hardware
307 } else {
308 Self::Soft
309 }
310 }
311}
312
313pub struct ConversionEngine {
315 pub profile: DpiProfile,
316 pub mode: ConversionEngineMode,
317}
318
319impl ConversionEngine {
320 pub fn new(profile: DpiProfile, mode: ConversionEngineMode) -> Self {
321 Self { profile, mode }
322 }
323
324 pub fn with_auto_mode(profile: DpiProfile) -> Self {
325 Self::new(profile, ConversionEngineMode::auto())
326 }
327
328 pub fn logical_to_physical(&self, v: CoordValue) -> CoordValue {
334 debug_assert_eq!(v.space, CoordSpace::LogicalPx);
335 let result = self.apply_scale(
336 v.x * self.profile.scale_factor,
337 v.y * self.profile.scale_factor,
338 );
339 CoordValue::new(result.0, result.1, CoordSpace::PhysicalPx)
340 }
341
342 pub fn physical_to_logical(&self, v: CoordValue) -> CoordValue {
344 debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
345 let result = self.apply_scale(
346 v.x / self.profile.scale_factor,
347 v.y / self.profile.scale_factor,
348 );
349 CoordValue::new(result.0, result.1, CoordSpace::LogicalPx)
350 }
351
352 pub fn logical_to_dpi_normalized(&self, v: CoordValue) -> CoordValue {
354 debug_assert_eq!(v.space, CoordSpace::LogicalPx);
355 let scale = self.profile.dpi_x / 96.0;
357 let result = self.apply_scale(v.x * scale, v.y * scale);
358 CoordValue::new(result.0, result.1, CoordSpace::DpiNormalized)
359 }
360
361 pub fn dpi_normalized_to_logical(&self, v: CoordValue) -> CoordValue {
363 debug_assert_eq!(v.space, CoordSpace::DpiNormalized);
364 let scale = 96.0 / self.profile.dpi_x;
365 let result = self.apply_scale(v.x * scale, v.y * scale);
366 CoordValue::new(result.0, result.1, CoordSpace::LogicalPx)
367 }
368
369 pub fn physical_to_dpi_normalized(&self, v: CoordValue) -> CoordValue {
371 debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
372 let logical = self.physical_to_logical(v);
374 self.logical_to_dpi_normalized(CoordValue::new(logical.x, logical.y, CoordSpace::LogicalPx))
375 }
376
377 pub fn dpi_normalized_to_physical(&self, v: CoordValue) -> CoordValue {
379 debug_assert_eq!(v.space, CoordSpace::DpiNormalized);
380 let logical = self.dpi_normalized_to_logical(v);
381 self.logical_to_physical(CoordValue::new(logical.x, logical.y, CoordSpace::LogicalPx))
382 }
383
384 pub fn logical_to_points(&self, v: CoordValue) -> CoordValue {
386 debug_assert_eq!(v.space, CoordSpace::LogicalPx);
387 let pts_per_px = self.profile.dpi_x / 72.0;
389 CoordValue::new(v.x * pts_per_px, v.y * pts_per_px, CoordSpace::Points)
390 }
391
392 pub fn points_to_logical(&self, v: CoordValue) -> CoordValue {
394 debug_assert_eq!(v.space, CoordSpace::Points);
395 let px_per_pt = 72.0 / self.profile.dpi_x;
396 CoordValue::new(v.x * px_per_pt, v.y * px_per_pt, CoordSpace::LogicalPx)
397 }
398
399 pub fn physical_to_mm(&self, v: CoordValue) -> CoordValue {
401 debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
402 CoordValue::new(
404 (v.x / self.profile.dpi_x) * 25.4,
405 (v.y / self.profile.dpi_y) * 25.4,
406 CoordSpace::Millimeters,
407 )
408 }
409
410 pub fn mm_to_physical(&self, v: CoordValue) -> CoordValue {
412 debug_assert_eq!(v.space, CoordSpace::Millimeters);
413 CoordValue::new(
414 (v.x / 25.4) * self.profile.dpi_x,
415 (v.y / 25.4) * self.profile.dpi_y,
416 CoordSpace::PhysicalPx,
417 )
418 }
419
420 pub fn convert_rect(&self, rect: RectValue, target: CoordSpace) -> RectValue {
426 let origin = CoordValue::new(rect.x, rect.y, rect.space);
427 let size_end = CoordValue::new(rect.x + rect.width, rect.y + rect.height, rect.space);
428
429 let conv_origin = self.convert_coord(origin, target);
430 let conv_size_end = self.convert_coord(size_end, target);
431
432 RectValue::new(
433 conv_origin.x,
434 conv_origin.y,
435 conv_size_end.x - conv_origin.x,
436 conv_size_end.y - conv_origin.y,
437 target,
438 )
439 }
440
441 pub fn convert_size(&self, size: SizeValue, target: CoordSpace) -> SizeValue {
443 let as_coord = CoordValue::new(size.width, size.height, size.space);
445 let converted = self.convert_coord(as_coord, target);
446 SizeValue::new(converted.x, converted.y, target)
447 }
448
449 pub fn convert_coord(&self, v: CoordValue, target: CoordSpace) -> CoordValue {
451 if v.space == target {
452 return v;
453 }
454
455 match (v.space, target) {
456 (CoordSpace::LogicalPx, CoordSpace::PhysicalPx) => self.logical_to_physical(v),
457 (CoordSpace::PhysicalPx, CoordSpace::LogicalPx) => self.physical_to_logical(v),
458 (CoordSpace::LogicalPx, CoordSpace::DpiNormalized) => self.logical_to_dpi_normalized(v),
459 (CoordSpace::DpiNormalized, CoordSpace::LogicalPx) => self.dpi_normalized_to_logical(v),
460 (CoordSpace::PhysicalPx, CoordSpace::DpiNormalized) => {
461 self.physical_to_dpi_normalized(v)
462 }
463 (CoordSpace::DpiNormalized, CoordSpace::PhysicalPx) => {
464 self.dpi_normalized_to_physical(v)
465 }
466 (CoordSpace::LogicalPx, CoordSpace::Points) => self.logical_to_points(v),
467 (CoordSpace::Points, CoordSpace::LogicalPx) => self.points_to_logical(v),
468 (CoordSpace::PhysicalPx, CoordSpace::Millimeters) => self.physical_to_mm(v),
469 (CoordSpace::Millimeters, CoordSpace::PhysicalPx) => self.mm_to_physical(v),
470 _ => {
472 let as_logical = self.convert_coord(v, CoordSpace::LogicalPx);
473 self.convert_coord(as_logical, target)
474 }
475 }
476 }
477
478 pub fn convert_geometry(
484 &self,
485 geo: &WindowGeometry,
486 from: CoordSpace,
487 to: CoordSpace,
488 ) -> WindowGeometry {
489 let rect = RectValue::from_geometry(geo, from);
490 self.convert_rect(rect, to).to_geometry()
491 }
492
493 fn apply_scale(&self, x: f64, y: f64) -> (f64, f64) {
499 match self.mode {
500 ConversionEngineMode::Soft => {
501 (x, y)
503 }
504 ConversionEngineMode::Hardware => {
505 (x, y)
511 }
512 }
513 }
514
515 pub fn bulk_convert_pixels(&self, data: &[u8], from: CoordSpace, to: CoordSpace) -> Vec<u8> {
519 if from == to {
520 return data.to_vec();
521 }
522
523 let scale = self.space_scale_factor(from, to);
524
525 match self.mode {
526 ConversionEngineMode::Hardware => self.bulk_convert_hw(data, scale),
527 ConversionEngineMode::Soft => self.bulk_convert_soft(data, scale),
528 }
529 }
530
531 fn space_scale_factor(&self, from: CoordSpace, to: CoordSpace) -> f32 {
533 let from_dpi = self.space_dpi(from);
534 let to_dpi = self.space_dpi(to);
535 (to_dpi / from_dpi) as f32
536 }
537
538 fn space_dpi(&self, space: CoordSpace) -> f64 {
539 match space {
540 CoordSpace::LogicalPx => 96.0,
541 CoordSpace::PhysicalPx => self.profile.dpi_x,
542 CoordSpace::DpiNormalized => 96.0,
543 CoordSpace::Points => 72.0,
544 CoordSpace::Millimeters => 25.4,
545 }
546 }
547
548 fn bulk_convert_soft(&self, data: &[u8], scale: f32) -> Vec<u8> {
550 let mut out = Vec::with_capacity(data.len());
551 let chunks = data.chunks_exact(4);
552 let remainder = chunks.remainder();
553 for chunk in chunks {
554 let f = f32::from_le_bytes(chunk.try_into().unwrap());
555 out.extend_from_slice(&(f * scale).to_le_bytes());
556 }
557 out.extend_from_slice(remainder);
559 out
560 }
561
562 fn bulk_convert_hw(&self, data: &[u8], scale: f32) -> Vec<u8> {
564 match CURRENT_ARCH {
567 ArchKind::Amd64 => {
568 self.bulk_convert_chunked(data, scale, 32)
570 }
571 ArchKind::Aarch64 | ArchKind::PowerPc64 => {
572 self.bulk_convert_chunked(data, scale, 16)
574 }
575 ArchKind::Sparc64 => {
576 self.bulk_convert_chunked(data, scale, 8)
578 }
579 ArchKind::RiscV64 | ArchKind::OpenRisc => {
580 self.bulk_convert_soft(data, scale)
582 }
583 ArchKind::Sisd | ArchKind::Unknown => {
584 self.bulk_convert_soft(data, scale)
586 }
587 }
588 }
589
590 fn bulk_convert_chunked(&self, data: &[u8], scale: f32, chunk_size: usize) -> Vec<u8> {
591 let mut out = Vec::with_capacity(data.len());
592 let chunks = data.chunks(chunk_size);
593 for chunk in chunks {
594 let f32_chunks = chunk.chunks_exact(4);
596 let rem = f32_chunks.remainder();
597 for f32_bytes in f32_chunks {
598 let f = f32::from_le_bytes(f32_bytes.try_into().unwrap());
599 out.extend_from_slice(&(f * scale).to_le_bytes());
600 }
601 out.extend_from_slice(rem);
602 }
603 out
604 }
605}
606
607#[derive(Debug, Clone)]
613pub struct ConversionStep {
614 pub from: CoordSpace,
615 pub to: CoordSpace,
616 pub label: String,
617}
618
619impl ConversionStep {
620 pub fn new(from: CoordSpace, to: CoordSpace) -> Self {
621 Self {
622 label: format!("{} → {}", from.name(), to.name()),
623 from,
624 to,
625 }
626 }
627
628 pub fn with_label(mut self, label: impl Into<String>) -> Self {
629 self.label = label.into();
630 self
631 }
632}
633
634#[derive(Debug, Clone)]
640pub struct PipelineResult {
641 pub value: CoordValue,
643 pub trace: Vec<(String, CoordValue)>,
645 pub steps_executed: usize,
647}
648
649#[derive(Debug, Clone)]
651pub struct RectPipelineResult {
652 pub rect: RectValue,
653 pub trace: Vec<(String, RectValue)>,
654 pub steps_executed: usize,
655}
656
657pub struct ConversionPipeline {
659 steps: Vec<ConversionStep>,
660 engine: Arc<ConversionEngine>,
661 pub trace_enabled: bool,
663}
664
665impl ConversionPipeline {
666 pub fn new(engine: Arc<ConversionEngine>) -> Self {
667 Self {
668 steps: Vec::new(),
669 engine,
670 trace_enabled: false,
671 }
672 }
673
674 pub fn step(mut self, from: CoordSpace, to: CoordSpace) -> Self {
676 self.steps.push(ConversionStep::new(from, to));
677 self
678 }
679
680 pub fn step_labeled(
682 mut self,
683 from: CoordSpace,
684 to: CoordSpace,
685 label: impl Into<String>,
686 ) -> Self {
687 self.steps
688 .push(ConversionStep::new(from, to).with_label(label));
689 self
690 }
691
692 pub fn with_trace(mut self) -> Self {
694 self.trace_enabled = true;
695 self
696 }
697
698 pub fn validate(&self) -> Result<(), String> {
700 for pair in self.steps.windows(2) {
701 if pair[0].to != pair[1].from {
702 return Err(format!(
703 "Pipeline break: step '{}' outputs {:?} but next step '{}' expects {:?}",
704 pair[0].label, pair[0].to, pair[1].label, pair[1].from,
705 ));
706 }
707 }
708 Ok(())
709 }
710
711 pub fn execute(&self, input: CoordValue) -> Result<PipelineResult, String> {
713 if self.steps.is_empty() {
714 return Ok(PipelineResult {
715 value: input,
716 trace: vec![],
717 steps_executed: 0,
718 });
719 }
720
721 if input.space != self.steps[0].from {
723 return Err(format!(
724 "Input space {:?} does not match first step from {:?}",
725 input.space, self.steps[0].from
726 ));
727 }
728
729 let mut current = input;
730 let mut trace = Vec::new();
731
732 for step in &self.steps {
733 if self.trace_enabled {
734 trace.push((step.label.clone(), current));
735 }
736 current = self.engine.convert_coord(current, step.to);
737 }
738
739 if self.trace_enabled {
740 trace.push(("output".to_string(), current));
741 }
742
743 Ok(PipelineResult {
744 value: current,
745 trace,
746 steps_executed: self.steps.len(),
747 })
748 }
749
750 pub fn execute_rect(&self, input: RectValue) -> Result<RectPipelineResult, String> {
752 if self.steps.is_empty() {
753 return Ok(RectPipelineResult {
754 rect: input,
755 trace: vec![],
756 steps_executed: 0,
757 });
758 }
759
760 if input.space != self.steps[0].from {
761 return Err(format!(
762 "Input space {:?} does not match first step from {:?}",
763 input.space, self.steps[0].from
764 ));
765 }
766
767 let mut current = input;
768 let mut trace = Vec::new();
769
770 for step in &self.steps {
771 if self.trace_enabled {
772 trace.push((step.label.clone(), current));
773 }
774 current = self.engine.convert_rect(current, step.to);
775 }
776
777 if self.trace_enabled {
778 trace.push(("output".to_string(), current));
779 }
780
781 Ok(RectPipelineResult {
782 rect: current,
783 trace,
784 steps_executed: self.steps.len(),
785 })
786 }
787
788 pub fn execute_bulk(&self, data: &[u8]) -> Result<Vec<u8>, String> {
790 if self.steps.is_empty() {
791 return Ok(data.to_vec());
792 }
793 let mut current = data.to_vec();
794 for step in &self.steps {
795 current = self
796 .engine
797 .bulk_convert_pixels(¤t, step.from, step.to);
798 }
799 Ok(current)
800 }
801
802 pub fn step_count(&self) -> usize {
803 self.steps.len()
804 }
805}
806
807pub struct DirectConverter {
814 engine: Arc<ConversionEngine>,
815}
816
817impl DirectConverter {
818 pub fn new(engine: Arc<ConversionEngine>) -> Self {
819 Self { engine }
820 }
821
822 pub fn convert(&self, v: CoordValue, to: CoordSpace) -> CoordValue {
824 self.engine.convert_coord(v, to)
825 }
826
827 pub fn convert_rect(&self, rect: RectValue, to: CoordSpace) -> RectValue {
829 self.engine.convert_rect(rect, to)
830 }
831
832 pub fn convert_size(&self, size: SizeValue, to: CoordSpace) -> SizeValue {
834 self.engine.convert_size(size, to)
835 }
836
837 pub fn convert_geometry(
839 &self,
840 geo: &WindowGeometry,
841 from: CoordSpace,
842 to: CoordSpace,
843 ) -> WindowGeometry {
844 self.engine.convert_geometry(geo, from, to)
845 }
846
847 pub fn logical_to_physical_geo(&self, geo: &WindowGeometry) -> WindowGeometry {
849 self.convert_geometry(geo, CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
850 }
851
852 pub fn physical_to_logical_geo(&self, geo: &WindowGeometry) -> WindowGeometry {
854 self.convert_geometry(geo, CoordSpace::PhysicalPx, CoordSpace::LogicalPx)
855 }
856
857 pub fn scale_factor(&self) -> f64 {
859 self.engine.profile.scale_factor
860 }
861
862 pub fn dpi(&self) -> (f64, f64) {
864 (self.engine.profile.dpi_x, self.engine.profile.dpi_y)
865 }
866}
867
868pub struct ConversionManager {
875 engine: Arc<ConversionEngine>,
876 pub direct: DirectConverter,
877}
878
879impl ConversionManager {
880 pub fn new(profile: DpiProfile, mode: ConversionEngineMode) -> Self {
881 let engine = Arc::new(ConversionEngine::new(profile, mode));
882 let direct = DirectConverter::new(engine.clone());
883 Self { engine, direct }
884 }
885
886 pub fn with_auto_mode(profile: DpiProfile) -> Self {
887 Self::new(profile, ConversionEngineMode::auto())
888 }
889
890 pub fn from_config(config: &WasmaConfig) -> Self {
891 let dpi = match config.resource_limits.scope_level {
893 0 => 96.0,
894 1..=50 => 96.0,
895 51..=100 => 192.0, _ => 96.0,
897 };
898 let profile = DpiProfile::new(dpi, dpi, 1920, 1080).with_physical_size();
899 let mode = ConversionEngineMode::auto();
900 Self::new(profile, mode)
901 }
902
903 pub fn pipeline(&self) -> ConversionPipeline {
905 ConversionPipeline::new(self.engine.clone())
906 }
907
908 pub fn pipeline_logical_to_dpi_norm(&self) -> ConversionPipeline {
911 self.pipeline()
912 .step_labeled(
913 CoordSpace::LogicalPx,
914 CoordSpace::PhysicalPx,
915 "logical→physical",
916 )
917 .step_labeled(
918 CoordSpace::PhysicalPx,
919 CoordSpace::DpiNormalized,
920 "physical→dpi_norm",
921 )
922 }
923
924 pub fn pipeline_physical_to_points(&self) -> ConversionPipeline {
926 self.pipeline()
927 .step_labeled(
928 CoordSpace::PhysicalPx,
929 CoordSpace::LogicalPx,
930 "physical→logical",
931 )
932 .step_labeled(CoordSpace::LogicalPx, CoordSpace::Points, "logical→points")
933 }
934
935 pub fn profile(&self) -> &DpiProfile {
936 &self.engine.profile
937 }
938 pub fn mode(&self) -> ConversionEngineMode {
939 self.engine.mode
940 }
941 pub fn engine(&self) -> Arc<ConversionEngine> {
942 self.engine.clone()
943 }
944}
945
946#[cfg(test)]
951mod tests {
952 use super::*;
953 use crate::parser::ConfigParser;
954
955 fn make_manager(dpi: f64) -> ConversionManager {
956 let profile = DpiProfile::new(dpi, dpi, 1920, 1080);
957 ConversionManager::new(profile, ConversionEngineMode::Soft)
958 }
959
960 #[test]
961 fn test_logical_to_physical_2x() {
962 let mgr = make_manager(192.0); let logical = CoordValue::logical(100.0, 200.0);
964 let physical = mgr.direct.convert(logical, CoordSpace::PhysicalPx);
965
966 assert!((physical.x - 200.0).abs() < f64::EPSILON);
967 assert!((physical.y - 400.0).abs() < f64::EPSILON);
968 assert_eq!(physical.space, CoordSpace::PhysicalPx);
969 println!(
970 "✅ Logical→Physical 2x: ({}, {}) → ({}, {})",
971 logical.x, logical.y, physical.x, physical.y
972 );
973 }
974
975 #[test]
976 fn test_physical_to_logical_2x() {
977 let mgr = make_manager(192.0);
978 let physical = CoordValue::physical(200.0, 400.0);
979 let logical = mgr.direct.convert(physical, CoordSpace::LogicalPx);
980
981 assert!((logical.x - 100.0).abs() < f64::EPSILON);
982 assert!((logical.y - 200.0).abs() < f64::EPSILON);
983 println!(
984 "✅ Physical→Logical 2x: ({}, {}) → ({}, {})",
985 physical.x, physical.y, logical.x, logical.y
986 );
987 }
988
989 #[test]
990 fn test_roundtrip_logical_physical() {
991 let mgr = make_manager(144.0); let original = CoordValue::logical(333.0, 444.0);
993 let physical = mgr.direct.convert(original, CoordSpace::PhysicalPx);
994 let back = mgr.direct.convert(physical, CoordSpace::LogicalPx);
995
996 assert!((back.x - original.x).abs() < 0.001);
997 assert!((back.y - original.y).abs() < 0.001);
998 println!(
999 "✅ Roundtrip logical↔physical: ({:.2}, {:.2})",
1000 back.x, back.y
1001 );
1002 }
1003
1004 #[test]
1005 fn test_logical_to_dpi_normalized() {
1006 let mgr = make_manager(192.0);
1007 let logical = CoordValue::logical(100.0, 100.0);
1008 let norm = mgr.direct.convert(logical, CoordSpace::DpiNormalized);
1009 assert!((norm.x - 200.0).abs() < 0.001);
1011 println!("✅ Logical→DpiNormalized: {} → {}", logical.x, norm.x);
1012 }
1013
1014 #[test]
1015 fn test_logical_to_points() {
1016 let mgr = make_manager(96.0); let logical = CoordValue::logical(72.0, 72.0);
1018 let points = mgr.direct.convert(logical, CoordSpace::Points);
1019 assert!((points.x - 96.0).abs() < 0.001);
1021 println!("✅ Logical→Points: {} px → {} pt", logical.x, points.x);
1022 }
1023
1024 #[test]
1025 fn test_physical_to_mm() {
1026 let mgr = make_manager(96.0);
1027 let physical = CoordValue::physical(96.0, 96.0);
1028 let mm = mgr.direct.convert(physical, CoordSpace::Millimeters);
1029 assert!((mm.x - 25.4).abs() < 0.001);
1031 println!(
1032 "✅ Physical→Millimeters: {} px → {:.1} mm",
1033 physical.x, mm.x
1034 );
1035 }
1036
1037 #[test]
1038 fn test_geometry_conversion() {
1039 let mgr = make_manager(192.0);
1040 let geo = WindowGeometry {
1041 x: 10,
1042 y: 20,
1043 width: 800,
1044 height: 600,
1045 };
1046 let phys_geo = mgr.direct.logical_to_physical_geo(&geo);
1047
1048 assert_eq!(phys_geo.x, 20);
1049 assert_eq!(phys_geo.y, 40);
1050 assert_eq!(phys_geo.width, 1600);
1051 assert_eq!(phys_geo.height, 1200);
1052 println!(
1053 "✅ Geometry conversion: {}x{} → {}x{}",
1054 geo.width, geo.height, phys_geo.width, phys_geo.height
1055 );
1056 }
1057
1058 #[test]
1059 fn test_rect_conversion() {
1060 let mgr = make_manager(192.0);
1061 let rect = RectValue::new(0.0, 0.0, 100.0, 50.0, CoordSpace::LogicalPx);
1062 let phys = mgr.direct.convert_rect(rect, CoordSpace::PhysicalPx);
1063
1064 assert!((phys.width - 200.0).abs() < 0.001);
1065 assert!((phys.height - 100.0).abs() < 0.001);
1066 println!(
1067 "✅ Rect conversion: {}×{} → {}×{}",
1068 rect.width, rect.height, phys.width, phys.height
1069 );
1070 }
1071
1072 #[test]
1073 fn test_pipeline_two_steps() {
1074 let mgr = make_manager(192.0);
1075 let pipeline = mgr
1076 .pipeline()
1077 .step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
1078 .step(CoordSpace::PhysicalPx, CoordSpace::DpiNormalized)
1079 .with_trace();
1080
1081 assert!(pipeline.validate().is_ok());
1082
1083 let input = CoordValue::logical(50.0, 50.0);
1084 let result = pipeline.execute(input).unwrap();
1085
1086 assert_eq!(result.steps_executed, 2);
1087 assert_eq!(result.value.space, CoordSpace::DpiNormalized);
1088 println!(
1089 "✅ Pipeline 2-step: ({}, {}) → ({:.1}, {:.1}) [{}]",
1090 input.x,
1091 input.y,
1092 result.value.x,
1093 result.value.y,
1094 result.value.space.name()
1095 );
1096 }
1097
1098 #[test]
1099 fn test_pipeline_validation_fail() {
1100 let mgr = make_manager(96.0);
1101 let pipeline = mgr
1102 .pipeline()
1103 .step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
1104 .step(CoordSpace::LogicalPx, CoordSpace::Points);
1106
1107 assert!(pipeline.validate().is_err());
1108 println!("✅ Pipeline validation correctly rejects broken chain");
1109 }
1110
1111 #[test]
1112 fn test_pipeline_rect() {
1113 let mgr = make_manager(192.0);
1114 let pipeline = mgr.pipeline_logical_to_dpi_norm();
1115
1116 let rect = RectValue::new(10.0, 20.0, 100.0, 50.0, CoordSpace::LogicalPx);
1117 let result = pipeline.execute_rect(rect).unwrap();
1118
1119 assert_eq!(result.steps_executed, 2);
1120 assert_eq!(result.rect.space, CoordSpace::DpiNormalized);
1121 println!(
1122 "✅ Pipeline rect: {} steps, final space: {}",
1123 result.steps_executed,
1124 result.rect.space.name()
1125 );
1126 }
1127
1128 #[test]
1129 fn test_direct_converter_scale_factor() {
1130 let mgr = make_manager(192.0);
1131 assert!((mgr.direct.scale_factor() - 2.0).abs() < f64::EPSILON);
1132 let (dpi_x, dpi_y) = mgr.direct.dpi();
1133 assert!((dpi_x - 192.0).abs() < f64::EPSILON);
1134 assert!((dpi_y - 192.0).abs() < f64::EPSILON);
1135 println!(
1136 "✅ DirectConverter scale_factor: {}",
1137 mgr.direct.scale_factor()
1138 );
1139 }
1140
1141 #[test]
1142 fn test_bulk_convert_soft() {
1143 let profile = DpiProfile::new(192.0, 192.0, 1920, 1080);
1144 let engine = ConversionEngine::new(profile, ConversionEngineMode::Soft);
1145
1146 let mut input = Vec::new();
1148 input.extend_from_slice(&100.0f32.to_le_bytes());
1149 input.extend_from_slice(&200.0f32.to_le_bytes());
1150
1151 let output =
1152 engine.bulk_convert_pixels(&input, CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
1153 assert_eq!(output.len(), 8);
1154
1155 let x = f32::from_le_bytes(output[0..4].try_into().unwrap());
1156 let y = f32::from_le_bytes(output[4..8].try_into().unwrap());
1157 assert!((x - 200.0).abs() < 0.01);
1158 assert!((y - 400.0).abs() < 0.01);
1159 println!(
1160 "✅ Bulk convert soft: [{}, {}] → [{}, {}]",
1161 100.0, 200.0, x, y
1162 );
1163 }
1164
1165 #[test]
1166 fn test_bulk_convert_hw() {
1167 let profile = DpiProfile::new(192.0, 192.0, 1920, 1080);
1168 let engine = ConversionEngine::new(profile, ConversionEngineMode::Hardware);
1169
1170 let mut input = Vec::new();
1171 for _ in 0..8 {
1172 input.extend_from_slice(&50.0f32.to_le_bytes());
1173 }
1174
1175 let output =
1176 engine.bulk_convert_pixels(&input, CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
1177 assert_eq!(output.len(), input.len());
1178 let first = f32::from_le_bytes(output[0..4].try_into().unwrap());
1179 assert!((first - 100.0).abs() < 0.01);
1180 println!(
1181 "✅ Bulk convert HW [{}]: {} → {}",
1182 CURRENT_ARCH.name(),
1183 50.0,
1184 first
1185 );
1186 }
1187
1188 #[test]
1189 fn test_pipeline_bulk() {
1190 let mgr = make_manager(192.0);
1191 let pipeline = mgr
1192 .pipeline()
1193 .step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
1194
1195 let mut input = Vec::new();
1196 input.extend_from_slice(&10.0f32.to_le_bytes());
1197 input.extend_from_slice(&20.0f32.to_le_bytes());
1198
1199 let output = pipeline.execute_bulk(&input).unwrap();
1200 let x = f32::from_le_bytes(output[0..4].try_into().unwrap());
1201 let y = f32::from_le_bytes(output[4..8].try_into().unwrap());
1202 assert!((x - 20.0).abs() < 0.01);
1203 assert!((y - 40.0).abs() < 0.01);
1204 println!("✅ Pipeline bulk: [{}, {}] → [{}, {}]", 10.0, 20.0, x, y);
1205 }
1206
1207 #[test]
1208 fn test_dpi_profile_physical_size() {
1209 let profile = DpiProfile::new(96.0, 96.0, 1920, 1080).with_physical_size();
1210 let w_mm = profile.physical_width_mm.unwrap();
1212 assert!((w_mm - 508.0).abs() < 0.1);
1213 println!("✅ DpiProfile physical size: {:.1} mm wide", w_mm);
1214 }
1215
1216 #[test]
1217 fn test_from_config() {
1218 let parser = ConfigParser::new(None);
1219 let content = parser.generate_default_config();
1220 let config = parser.parse(&content).unwrap();
1221 let mgr = ConversionManager::from_config(&config);
1222 assert!(mgr.profile().dpi_x > 0.0);
1223 println!(
1224 "✅ ConversionManager::from_config working, DPI: {}",
1225 mgr.profile().dpi_x
1226 );
1227 }
1228
1229 #[test]
1230 fn test_coord_value_helpers() {
1231 let v = CoordValue::logical(3.7, 2.3);
1232 assert_eq!(v.round().x, 4.0);
1233 assert_eq!(v.floor().x, 3.0);
1234 assert_eq!(v.ceil().x, 4.0);
1235 println!("✅ CoordValue helpers working");
1236 }
1237
1238 #[test]
1239 fn test_rect_contains_point() {
1240 let rect = RectValue::new(10.0, 10.0, 100.0, 50.0, CoordSpace::LogicalPx);
1241 assert!(rect.contains_point(50.0, 30.0));
1242 assert!(!rect.contains_point(5.0, 30.0));
1243 assert!(!rect.contains_point(50.0, 70.0));
1244 println!("✅ RectValue::contains_point working");
1245 }
1246}