use crate::parser::WasmaConfig;
use crate::wasma_protocol_universal_client_unix_posix_window::{ArchKind, CURRENT_ARCH};
use crate::window_handling::WindowGeometry;
use std::sync::Arc;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CoordSpace {
LogicalPx,
PhysicalPx,
DpiNormalized,
Points,
Millimeters,
}
impl CoordSpace {
pub fn name(&self) -> &'static str {
match self {
Self::LogicalPx => "LogicalPx",
Self::PhysicalPx => "PhysicalPx",
Self::DpiNormalized => "DpiNormalized",
Self::Points => "Points",
Self::Millimeters => "Millimeters",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DpiPreset {
Standard,
MacOsStandard,
HiDpi2x,
HiDpi3x,
UltraHd,
Custom(f64),
}
impl DpiPreset {
pub fn dpi(&self) -> f64 {
match self {
Self::Standard => 96.0,
Self::MacOsStandard => 96.0,
Self::HiDpi2x => 192.0,
Self::HiDpi3x => 288.0,
Self::UltraHd => 200.0,
Self::Custom(v) => *v,
}
}
}
#[derive(Debug, Clone)]
pub struct DpiProfile {
pub dpi_x: f64,
pub dpi_y: f64,
pub scale_factor: f64,
pub physical_width_mm: Option<f64>,
pub physical_height_mm: Option<f64>,
pub screen_width_px: u32,
pub screen_height_px: u32,
}
impl DpiProfile {
pub fn new(dpi_x: f64, dpi_y: f64, screen_width_px: u32, screen_height_px: u32) -> Self {
let scale_factor = dpi_x / 96.0;
Self {
dpi_x,
dpi_y,
scale_factor,
physical_width_mm: None,
physical_height_mm: None,
screen_width_px,
screen_height_px,
}
}
pub fn from_preset(preset: DpiPreset, screen_width_px: u32, screen_height_px: u32) -> Self {
Self::new(
preset.dpi(),
preset.dpi(),
screen_width_px,
screen_height_px,
)
}
pub fn with_physical_size(mut self) -> Self {
self.physical_width_mm = Some((self.screen_width_px as f64 / self.dpi_x) * 25.4);
self.physical_height_mm = Some((self.screen_height_px as f64 / self.dpi_y) * 25.4);
self
}
pub fn logical_width(&self) -> f64 {
self.screen_width_px as f64 / self.scale_factor
}
pub fn logical_height(&self) -> f64 {
self.screen_height_px as f64 / self.scale_factor
}
pub fn device_pixel_ratio(&self) -> f64 {
self.scale_factor
}
}
impl Default for DpiProfile {
fn default() -> Self {
Self::new(96.0, 96.0, 1920, 1080)
}
}
#[derive(Debug, Clone, Copy)]
pub struct CoordValue {
pub x: f64,
pub y: f64,
pub space: CoordSpace,
}
impl CoordValue {
pub fn new(x: f64, y: f64, space: CoordSpace) -> Self {
Self { x, y, space }
}
pub fn logical(x: f64, y: f64) -> Self {
Self::new(x, y, CoordSpace::LogicalPx)
}
pub fn physical(x: f64, y: f64) -> Self {
Self::new(x, y, CoordSpace::PhysicalPx)
}
pub fn dpi_normalized(x: f64, y: f64) -> Self {
Self::new(x, y, CoordSpace::DpiNormalized)
}
pub fn round(self) -> Self {
Self::new(self.x.round(), self.y.round(), self.space)
}
pub fn floor(self) -> Self {
Self::new(self.x.floor(), self.y.floor(), self.space)
}
pub fn ceil(self) -> Self {
Self::new(self.x.ceil(), self.y.ceil(), self.space)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SizeValue {
pub width: f64,
pub height: f64,
pub space: CoordSpace,
}
impl SizeValue {
pub fn new(width: f64, height: f64, space: CoordSpace) -> Self {
Self {
width,
height,
space,
}
}
pub fn logical(width: f64, height: f64) -> Self {
Self::new(width, height, CoordSpace::LogicalPx)
}
pub fn physical(width: f64, height: f64) -> Self {
Self::new(width, height, CoordSpace::PhysicalPx)
}
pub fn aspect_ratio(&self) -> f64 {
if self.height == 0.0 {
0.0
} else {
self.width / self.height
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct RectValue {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub space: CoordSpace,
}
impl RectValue {
pub fn new(x: f64, y: f64, width: f64, height: f64, space: CoordSpace) -> Self {
Self {
x,
y,
width,
height,
space,
}
}
pub fn from_geometry(geo: &WindowGeometry, space: CoordSpace) -> Self {
Self::new(
geo.x as f64,
geo.y as f64,
geo.width as f64,
geo.height as f64,
space,
)
}
pub fn to_geometry(&self) -> WindowGeometry {
WindowGeometry {
x: self.x.round() as i32,
y: self.y.round() as i32,
width: self.width.round() as u32,
height: self.height.round() as u32,
}
}
pub fn right(&self) -> f64 {
self.x + self.width
}
pub fn bottom(&self) -> f64 {
self.y + self.height
}
pub fn contains_point(&self, x: f64, y: f64) -> bool {
x >= self.x && x <= self.right() && y >= self.y && y <= self.bottom()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConversionEngineMode {
Soft,
Hardware,
}
impl ConversionEngineMode {
pub fn auto() -> Self {
if CURRENT_ARCH.supports_simd() {
Self::Hardware
} else {
Self::Soft
}
}
}
pub struct ConversionEngine {
pub profile: DpiProfile,
pub mode: ConversionEngineMode,
}
impl ConversionEngine {
pub fn new(profile: DpiProfile, mode: ConversionEngineMode) -> Self {
Self { profile, mode }
}
pub fn with_auto_mode(profile: DpiProfile) -> Self {
Self::new(profile, ConversionEngineMode::auto())
}
pub fn logical_to_physical(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::LogicalPx);
let result = self.apply_scale(
v.x * self.profile.scale_factor,
v.y * self.profile.scale_factor,
);
CoordValue::new(result.0, result.1, CoordSpace::PhysicalPx)
}
pub fn physical_to_logical(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
let result = self.apply_scale(
v.x / self.profile.scale_factor,
v.y / self.profile.scale_factor,
);
CoordValue::new(result.0, result.1, CoordSpace::LogicalPx)
}
pub fn logical_to_dpi_normalized(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::LogicalPx);
let scale = self.profile.dpi_x / 96.0;
let result = self.apply_scale(v.x * scale, v.y * scale);
CoordValue::new(result.0, result.1, CoordSpace::DpiNormalized)
}
pub fn dpi_normalized_to_logical(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::DpiNormalized);
let scale = 96.0 / self.profile.dpi_x;
let result = self.apply_scale(v.x * scale, v.y * scale);
CoordValue::new(result.0, result.1, CoordSpace::LogicalPx)
}
pub fn physical_to_dpi_normalized(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
let logical = self.physical_to_logical(v);
self.logical_to_dpi_normalized(CoordValue::new(logical.x, logical.y, CoordSpace::LogicalPx))
}
pub fn dpi_normalized_to_physical(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::DpiNormalized);
let logical = self.dpi_normalized_to_logical(v);
self.logical_to_physical(CoordValue::new(logical.x, logical.y, CoordSpace::LogicalPx))
}
pub fn logical_to_points(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::LogicalPx);
let pts_per_px = self.profile.dpi_x / 72.0;
CoordValue::new(v.x * pts_per_px, v.y * pts_per_px, CoordSpace::Points)
}
pub fn points_to_logical(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::Points);
let px_per_pt = 72.0 / self.profile.dpi_x;
CoordValue::new(v.x * px_per_pt, v.y * px_per_pt, CoordSpace::LogicalPx)
}
pub fn physical_to_mm(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::PhysicalPx);
CoordValue::new(
(v.x / self.profile.dpi_x) * 25.4,
(v.y / self.profile.dpi_y) * 25.4,
CoordSpace::Millimeters,
)
}
pub fn mm_to_physical(&self, v: CoordValue) -> CoordValue {
debug_assert_eq!(v.space, CoordSpace::Millimeters);
CoordValue::new(
(v.x / 25.4) * self.profile.dpi_x,
(v.y / 25.4) * self.profile.dpi_y,
CoordSpace::PhysicalPx,
)
}
pub fn convert_rect(&self, rect: RectValue, target: CoordSpace) -> RectValue {
let origin = CoordValue::new(rect.x, rect.y, rect.space);
let size_end = CoordValue::new(rect.x + rect.width, rect.y + rect.height, rect.space);
let conv_origin = self.convert_coord(origin, target);
let conv_size_end = self.convert_coord(size_end, target);
RectValue::new(
conv_origin.x,
conv_origin.y,
conv_size_end.x - conv_origin.x,
conv_size_end.y - conv_origin.y,
target,
)
}
pub fn convert_size(&self, size: SizeValue, target: CoordSpace) -> SizeValue {
let as_coord = CoordValue::new(size.width, size.height, size.space);
let converted = self.convert_coord(as_coord, target);
SizeValue::new(converted.x, converted.y, target)
}
pub fn convert_coord(&self, v: CoordValue, target: CoordSpace) -> CoordValue {
if v.space == target {
return v;
}
match (v.space, target) {
(CoordSpace::LogicalPx, CoordSpace::PhysicalPx) => self.logical_to_physical(v),
(CoordSpace::PhysicalPx, CoordSpace::LogicalPx) => self.physical_to_logical(v),
(CoordSpace::LogicalPx, CoordSpace::DpiNormalized) => self.logical_to_dpi_normalized(v),
(CoordSpace::DpiNormalized, CoordSpace::LogicalPx) => self.dpi_normalized_to_logical(v),
(CoordSpace::PhysicalPx, CoordSpace::DpiNormalized) => {
self.physical_to_dpi_normalized(v)
}
(CoordSpace::DpiNormalized, CoordSpace::PhysicalPx) => {
self.dpi_normalized_to_physical(v)
}
(CoordSpace::LogicalPx, CoordSpace::Points) => self.logical_to_points(v),
(CoordSpace::Points, CoordSpace::LogicalPx) => self.points_to_logical(v),
(CoordSpace::PhysicalPx, CoordSpace::Millimeters) => self.physical_to_mm(v),
(CoordSpace::Millimeters, CoordSpace::PhysicalPx) => self.mm_to_physical(v),
_ => {
let as_logical = self.convert_coord(v, CoordSpace::LogicalPx);
self.convert_coord(as_logical, target)
}
}
}
pub fn convert_geometry(
&self,
geo: &WindowGeometry,
from: CoordSpace,
to: CoordSpace,
) -> WindowGeometry {
let rect = RectValue::from_geometry(geo, from);
self.convert_rect(rect, to).to_geometry()
}
fn apply_scale(&self, x: f64, y: f64) -> (f64, f64) {
match self.mode {
ConversionEngineMode::Soft => {
(x, y)
}
ConversionEngineMode::Hardware => {
(x, y)
}
}
}
pub fn bulk_convert_pixels(&self, data: &[u8], from: CoordSpace, to: CoordSpace) -> Vec<u8> {
if from == to {
return data.to_vec();
}
let scale = self.space_scale_factor(from, to);
match self.mode {
ConversionEngineMode::Hardware => self.bulk_convert_hw(data, scale),
ConversionEngineMode::Soft => self.bulk_convert_soft(data, scale),
}
}
fn space_scale_factor(&self, from: CoordSpace, to: CoordSpace) -> f32 {
let from_dpi = self.space_dpi(from);
let to_dpi = self.space_dpi(to);
(to_dpi / from_dpi) as f32
}
fn space_dpi(&self, space: CoordSpace) -> f64 {
match space {
CoordSpace::LogicalPx => 96.0,
CoordSpace::PhysicalPx => self.profile.dpi_x,
CoordSpace::DpiNormalized => 96.0,
CoordSpace::Points => 72.0,
CoordSpace::Millimeters => 25.4,
}
}
fn bulk_convert_soft(&self, data: &[u8], scale: f32) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len());
let chunks = data.chunks_exact(4);
let remainder = chunks.remainder();
for chunk in chunks {
let f = f32::from_le_bytes(chunk.try_into().unwrap());
out.extend_from_slice(&(f * scale).to_le_bytes());
}
out.extend_from_slice(remainder);
out
}
fn bulk_convert_hw(&self, data: &[u8], scale: f32) -> Vec<u8> {
match CURRENT_ARCH {
ArchKind::Amd64 => {
self.bulk_convert_chunked(data, scale, 32)
}
ArchKind::Aarch64 | ArchKind::PowerPc64 => {
self.bulk_convert_chunked(data, scale, 16)
}
ArchKind::Sparc64 => {
self.bulk_convert_chunked(data, scale, 8)
}
ArchKind::RiscV64 | ArchKind::OpenRisc => {
self.bulk_convert_soft(data, scale)
}
ArchKind::Sisd | ArchKind::Unknown => {
self.bulk_convert_soft(data, scale)
}
}
}
fn bulk_convert_chunked(&self, data: &[u8], scale: f32, chunk_size: usize) -> Vec<u8> {
let mut out = Vec::with_capacity(data.len());
let chunks = data.chunks(chunk_size);
for chunk in chunks {
let f32_chunks = chunk.chunks_exact(4);
let rem = f32_chunks.remainder();
for f32_bytes in f32_chunks {
let f = f32::from_le_bytes(f32_bytes.try_into().unwrap());
out.extend_from_slice(&(f * scale).to_le_bytes());
}
out.extend_from_slice(rem);
}
out
}
}
#[derive(Debug, Clone)]
pub struct ConversionStep {
pub from: CoordSpace,
pub to: CoordSpace,
pub label: String,
}
impl ConversionStep {
pub fn new(from: CoordSpace, to: CoordSpace) -> Self {
Self {
label: format!("{} → {}", from.name(), to.name()),
from,
to,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = label.into();
self
}
}
#[derive(Debug, Clone)]
pub struct PipelineResult {
pub value: CoordValue,
pub trace: Vec<(String, CoordValue)>,
pub steps_executed: usize,
}
#[derive(Debug, Clone)]
pub struct RectPipelineResult {
pub rect: RectValue,
pub trace: Vec<(String, RectValue)>,
pub steps_executed: usize,
}
pub struct ConversionPipeline {
steps: Vec<ConversionStep>,
engine: Arc<ConversionEngine>,
pub trace_enabled: bool,
}
impl ConversionPipeline {
pub fn new(engine: Arc<ConversionEngine>) -> Self {
Self {
steps: Vec::new(),
engine,
trace_enabled: false,
}
}
pub fn step(mut self, from: CoordSpace, to: CoordSpace) -> Self {
self.steps.push(ConversionStep::new(from, to));
self
}
pub fn step_labeled(
mut self,
from: CoordSpace,
to: CoordSpace,
label: impl Into<String>,
) -> Self {
self.steps
.push(ConversionStep::new(from, to).with_label(label));
self
}
pub fn with_trace(mut self) -> Self {
self.trace_enabled = true;
self
}
pub fn validate(&self) -> Result<(), String> {
for pair in self.steps.windows(2) {
if pair[0].to != pair[1].from {
return Err(format!(
"Pipeline break: step '{}' outputs {:?} but next step '{}' expects {:?}",
pair[0].label, pair[0].to, pair[1].label, pair[1].from,
));
}
}
Ok(())
}
pub fn execute(&self, input: CoordValue) -> Result<PipelineResult, String> {
if self.steps.is_empty() {
return Ok(PipelineResult {
value: input,
trace: vec![],
steps_executed: 0,
});
}
if input.space != self.steps[0].from {
return Err(format!(
"Input space {:?} does not match first step from {:?}",
input.space, self.steps[0].from
));
}
let mut current = input;
let mut trace = Vec::new();
for step in &self.steps {
if self.trace_enabled {
trace.push((step.label.clone(), current));
}
current = self.engine.convert_coord(current, step.to);
}
if self.trace_enabled {
trace.push(("output".to_string(), current));
}
Ok(PipelineResult {
value: current,
trace,
steps_executed: self.steps.len(),
})
}
pub fn execute_rect(&self, input: RectValue) -> Result<RectPipelineResult, String> {
if self.steps.is_empty() {
return Ok(RectPipelineResult {
rect: input,
trace: vec![],
steps_executed: 0,
});
}
if input.space != self.steps[0].from {
return Err(format!(
"Input space {:?} does not match first step from {:?}",
input.space, self.steps[0].from
));
}
let mut current = input;
let mut trace = Vec::new();
for step in &self.steps {
if self.trace_enabled {
trace.push((step.label.clone(), current));
}
current = self.engine.convert_rect(current, step.to);
}
if self.trace_enabled {
trace.push(("output".to_string(), current));
}
Ok(RectPipelineResult {
rect: current,
trace,
steps_executed: self.steps.len(),
})
}
pub fn execute_bulk(&self, data: &[u8]) -> Result<Vec<u8>, String> {
if self.steps.is_empty() {
return Ok(data.to_vec());
}
let mut current = data.to_vec();
for step in &self.steps {
current = self
.engine
.bulk_convert_pixels(¤t, step.from, step.to);
}
Ok(current)
}
pub fn step_count(&self) -> usize {
self.steps.len()
}
}
pub struct DirectConverter {
engine: Arc<ConversionEngine>,
}
impl DirectConverter {
pub fn new(engine: Arc<ConversionEngine>) -> Self {
Self { engine }
}
pub fn convert(&self, v: CoordValue, to: CoordSpace) -> CoordValue {
self.engine.convert_coord(v, to)
}
pub fn convert_rect(&self, rect: RectValue, to: CoordSpace) -> RectValue {
self.engine.convert_rect(rect, to)
}
pub fn convert_size(&self, size: SizeValue, to: CoordSpace) -> SizeValue {
self.engine.convert_size(size, to)
}
pub fn convert_geometry(
&self,
geo: &WindowGeometry,
from: CoordSpace,
to: CoordSpace,
) -> WindowGeometry {
self.engine.convert_geometry(geo, from, to)
}
pub fn logical_to_physical_geo(&self, geo: &WindowGeometry) -> WindowGeometry {
self.convert_geometry(geo, CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
}
pub fn physical_to_logical_geo(&self, geo: &WindowGeometry) -> WindowGeometry {
self.convert_geometry(geo, CoordSpace::PhysicalPx, CoordSpace::LogicalPx)
}
pub fn scale_factor(&self) -> f64 {
self.engine.profile.scale_factor
}
pub fn dpi(&self) -> (f64, f64) {
(self.engine.profile.dpi_x, self.engine.profile.dpi_y)
}
}
pub struct ConversionManager {
engine: Arc<ConversionEngine>,
pub direct: DirectConverter,
}
impl ConversionManager {
pub fn new(profile: DpiProfile, mode: ConversionEngineMode) -> Self {
let engine = Arc::new(ConversionEngine::new(profile, mode));
let direct = DirectConverter::new(engine.clone());
Self { engine, direct }
}
pub fn with_auto_mode(profile: DpiProfile) -> Self {
Self::new(profile, ConversionEngineMode::auto())
}
pub fn from_config(config: &WasmaConfig) -> Self {
let dpi = match config.resource_limits.scope_level {
0 => 96.0,
1..=50 => 96.0,
51..=100 => 192.0, _ => 96.0,
};
let profile = DpiProfile::new(dpi, dpi, 1920, 1080).with_physical_size();
let mode = ConversionEngineMode::auto();
Self::new(profile, mode)
}
pub fn pipeline(&self) -> ConversionPipeline {
ConversionPipeline::new(self.engine.clone())
}
pub fn pipeline_logical_to_dpi_norm(&self) -> ConversionPipeline {
self.pipeline()
.step_labeled(
CoordSpace::LogicalPx,
CoordSpace::PhysicalPx,
"logical→physical",
)
.step_labeled(
CoordSpace::PhysicalPx,
CoordSpace::DpiNormalized,
"physical→dpi_norm",
)
}
pub fn pipeline_physical_to_points(&self) -> ConversionPipeline {
self.pipeline()
.step_labeled(
CoordSpace::PhysicalPx,
CoordSpace::LogicalPx,
"physical→logical",
)
.step_labeled(CoordSpace::LogicalPx, CoordSpace::Points, "logical→points")
}
pub fn profile(&self) -> &DpiProfile {
&self.engine.profile
}
pub fn mode(&self) -> ConversionEngineMode {
self.engine.mode
}
pub fn engine(&self) -> Arc<ConversionEngine> {
self.engine.clone()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::ConfigParser;
fn make_manager(dpi: f64) -> ConversionManager {
let profile = DpiProfile::new(dpi, dpi, 1920, 1080);
ConversionManager::new(profile, ConversionEngineMode::Soft)
}
#[test]
fn test_logical_to_physical_2x() {
let mgr = make_manager(192.0); let logical = CoordValue::logical(100.0, 200.0);
let physical = mgr.direct.convert(logical, CoordSpace::PhysicalPx);
assert!((physical.x - 200.0).abs() < f64::EPSILON);
assert!((physical.y - 400.0).abs() < f64::EPSILON);
assert_eq!(physical.space, CoordSpace::PhysicalPx);
println!(
"✅ Logical→Physical 2x: ({}, {}) → ({}, {})",
logical.x, logical.y, physical.x, physical.y
);
}
#[test]
fn test_physical_to_logical_2x() {
let mgr = make_manager(192.0);
let physical = CoordValue::physical(200.0, 400.0);
let logical = mgr.direct.convert(physical, CoordSpace::LogicalPx);
assert!((logical.x - 100.0).abs() < f64::EPSILON);
assert!((logical.y - 200.0).abs() < f64::EPSILON);
println!(
"✅ Physical→Logical 2x: ({}, {}) → ({}, {})",
physical.x, physical.y, logical.x, logical.y
);
}
#[test]
fn test_roundtrip_logical_physical() {
let mgr = make_manager(144.0); let original = CoordValue::logical(333.0, 444.0);
let physical = mgr.direct.convert(original, CoordSpace::PhysicalPx);
let back = mgr.direct.convert(physical, CoordSpace::LogicalPx);
assert!((back.x - original.x).abs() < 0.001);
assert!((back.y - original.y).abs() < 0.001);
println!(
"✅ Roundtrip logical↔physical: ({:.2}, {:.2})",
back.x, back.y
);
}
#[test]
fn test_logical_to_dpi_normalized() {
let mgr = make_manager(192.0);
let logical = CoordValue::logical(100.0, 100.0);
let norm = mgr.direct.convert(logical, CoordSpace::DpiNormalized);
assert!((norm.x - 200.0).abs() < 0.001);
println!("✅ Logical→DpiNormalized: {} → {}", logical.x, norm.x);
}
#[test]
fn test_logical_to_points() {
let mgr = make_manager(96.0); let logical = CoordValue::logical(72.0, 72.0);
let points = mgr.direct.convert(logical, CoordSpace::Points);
assert!((points.x - 96.0).abs() < 0.001);
println!("✅ Logical→Points: {} px → {} pt", logical.x, points.x);
}
#[test]
fn test_physical_to_mm() {
let mgr = make_manager(96.0);
let physical = CoordValue::physical(96.0, 96.0);
let mm = mgr.direct.convert(physical, CoordSpace::Millimeters);
assert!((mm.x - 25.4).abs() < 0.001);
println!(
"✅ Physical→Millimeters: {} px → {:.1} mm",
physical.x, mm.x
);
}
#[test]
fn test_geometry_conversion() {
let mgr = make_manager(192.0);
let geo = WindowGeometry {
x: 10,
y: 20,
width: 800,
height: 600,
};
let phys_geo = mgr.direct.logical_to_physical_geo(&geo);
assert_eq!(phys_geo.x, 20);
assert_eq!(phys_geo.y, 40);
assert_eq!(phys_geo.width, 1600);
assert_eq!(phys_geo.height, 1200);
println!(
"✅ Geometry conversion: {}x{} → {}x{}",
geo.width, geo.height, phys_geo.width, phys_geo.height
);
}
#[test]
fn test_rect_conversion() {
let mgr = make_manager(192.0);
let rect = RectValue::new(0.0, 0.0, 100.0, 50.0, CoordSpace::LogicalPx);
let phys = mgr.direct.convert_rect(rect, CoordSpace::PhysicalPx);
assert!((phys.width - 200.0).abs() < 0.001);
assert!((phys.height - 100.0).abs() < 0.001);
println!(
"✅ Rect conversion: {}×{} → {}×{}",
rect.width, rect.height, phys.width, phys.height
);
}
#[test]
fn test_pipeline_two_steps() {
let mgr = make_manager(192.0);
let pipeline = mgr
.pipeline()
.step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
.step(CoordSpace::PhysicalPx, CoordSpace::DpiNormalized)
.with_trace();
assert!(pipeline.validate().is_ok());
let input = CoordValue::logical(50.0, 50.0);
let result = pipeline.execute(input).unwrap();
assert_eq!(result.steps_executed, 2);
assert_eq!(result.value.space, CoordSpace::DpiNormalized);
println!(
"✅ Pipeline 2-step: ({}, {}) → ({:.1}, {:.1}) [{}]",
input.x,
input.y,
result.value.x,
result.value.y,
result.value.space.name()
);
}
#[test]
fn test_pipeline_validation_fail() {
let mgr = make_manager(96.0);
let pipeline = mgr
.pipeline()
.step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx)
.step(CoordSpace::LogicalPx, CoordSpace::Points);
assert!(pipeline.validate().is_err());
println!("✅ Pipeline validation correctly rejects broken chain");
}
#[test]
fn test_pipeline_rect() {
let mgr = make_manager(192.0);
let pipeline = mgr.pipeline_logical_to_dpi_norm();
let rect = RectValue::new(10.0, 20.0, 100.0, 50.0, CoordSpace::LogicalPx);
let result = pipeline.execute_rect(rect).unwrap();
assert_eq!(result.steps_executed, 2);
assert_eq!(result.rect.space, CoordSpace::DpiNormalized);
println!(
"✅ Pipeline rect: {} steps, final space: {}",
result.steps_executed,
result.rect.space.name()
);
}
#[test]
fn test_direct_converter_scale_factor() {
let mgr = make_manager(192.0);
assert!((mgr.direct.scale_factor() - 2.0).abs() < f64::EPSILON);
let (dpi_x, dpi_y) = mgr.direct.dpi();
assert!((dpi_x - 192.0).abs() < f64::EPSILON);
assert!((dpi_y - 192.0).abs() < f64::EPSILON);
println!(
"✅ DirectConverter scale_factor: {}",
mgr.direct.scale_factor()
);
}
#[test]
fn test_bulk_convert_soft() {
let profile = DpiProfile::new(192.0, 192.0, 1920, 1080);
let engine = ConversionEngine::new(profile, ConversionEngineMode::Soft);
let mut input = Vec::new();
input.extend_from_slice(&100.0f32.to_le_bytes());
input.extend_from_slice(&200.0f32.to_le_bytes());
let output =
engine.bulk_convert_pixels(&input, CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
assert_eq!(output.len(), 8);
let x = f32::from_le_bytes(output[0..4].try_into().unwrap());
let y = f32::from_le_bytes(output[4..8].try_into().unwrap());
assert!((x - 200.0).abs() < 0.01);
assert!((y - 400.0).abs() < 0.01);
println!(
"✅ Bulk convert soft: [{}, {}] → [{}, {}]",
100.0, 200.0, x, y
);
}
#[test]
fn test_bulk_convert_hw() {
let profile = DpiProfile::new(192.0, 192.0, 1920, 1080);
let engine = ConversionEngine::new(profile, ConversionEngineMode::Hardware);
let mut input = Vec::new();
for _ in 0..8 {
input.extend_from_slice(&50.0f32.to_le_bytes());
}
let output =
engine.bulk_convert_pixels(&input, CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
assert_eq!(output.len(), input.len());
let first = f32::from_le_bytes(output[0..4].try_into().unwrap());
assert!((first - 100.0).abs() < 0.01);
println!(
"✅ Bulk convert HW [{}]: {} → {}",
CURRENT_ARCH.name(),
50.0,
first
);
}
#[test]
fn test_pipeline_bulk() {
let mgr = make_manager(192.0);
let pipeline = mgr
.pipeline()
.step(CoordSpace::LogicalPx, CoordSpace::PhysicalPx);
let mut input = Vec::new();
input.extend_from_slice(&10.0f32.to_le_bytes());
input.extend_from_slice(&20.0f32.to_le_bytes());
let output = pipeline.execute_bulk(&input).unwrap();
let x = f32::from_le_bytes(output[0..4].try_into().unwrap());
let y = f32::from_le_bytes(output[4..8].try_into().unwrap());
assert!((x - 20.0).abs() < 0.01);
assert!((y - 40.0).abs() < 0.01);
println!("✅ Pipeline bulk: [{}, {}] → [{}, {}]", 10.0, 20.0, x, y);
}
#[test]
fn test_dpi_profile_physical_size() {
let profile = DpiProfile::new(96.0, 96.0, 1920, 1080).with_physical_size();
let w_mm = profile.physical_width_mm.unwrap();
assert!((w_mm - 508.0).abs() < 0.1);
println!("✅ DpiProfile physical size: {:.1} mm wide", w_mm);
}
#[test]
fn test_from_config() {
let parser = ConfigParser::new(None);
let content = parser.generate_default_config();
let config = parser.parse(&content).unwrap();
let mgr = ConversionManager::from_config(&config);
assert!(mgr.profile().dpi_x > 0.0);
println!(
"✅ ConversionManager::from_config working, DPI: {}",
mgr.profile().dpi_x
);
}
#[test]
fn test_coord_value_helpers() {
let v = CoordValue::logical(3.7, 2.3);
assert_eq!(v.round().x, 4.0);
assert_eq!(v.floor().x, 3.0);
assert_eq!(v.ceil().x, 4.0);
println!("✅ CoordValue helpers working");
}
#[test]
fn test_rect_contains_point() {
let rect = RectValue::new(10.0, 10.0, 100.0, 50.0, CoordSpace::LogicalPx);
assert!(rect.contains_point(50.0, 30.0));
assert!(!rect.contains_point(5.0, 30.0));
assert!(!rect.contains_point(50.0, 70.0));
println!("✅ RectValue::contains_point working");
}
}