use std::panic::{AssertUnwindSafe, catch_unwind};
use frust_core::WindowMetrics;
use frust_core::insets::{CornerInset, CornerInsets, EdgeInsets, WindowInsets};
use kurbo::Size;
#[inline]
pub fn sanitize_scale(raw: f32) -> f64 {
if raw.is_finite() && raw > 0.0 {
raw as f64
} else {
1.0
}
}
#[inline]
pub fn logical_size(physical_width: u32, physical_height: u32, scale: f64) -> (f64, f64) {
(
physical_width as f64 / scale,
physical_height as f64 / scale,
)
}
#[inline]
pub fn logical_insets(physical: [f64; 8], scale: f64) -> WindowInsets {
let sanitize_edge = |px: f64| if px.is_finite() && px >= 0.0 { px } else { 0.0 };
let to_logical = |px: f64| sanitize_edge(px) / scale;
WindowInsets::new(
EdgeInsets::new(
to_logical(physical[0]),
to_logical(physical[1]),
to_logical(physical[2]),
to_logical(physical[3]),
),
EdgeInsets::new(
to_logical(physical[4]),
to_logical(physical[5]),
to_logical(physical[6]),
to_logical(physical[7]),
),
)
}
#[inline]
pub fn logical_corner_insets(physical: [f64; 8], scale: f64) -> CornerInsets {
let to_logical = |px: f64| {
let px = if px.is_finite() && px >= 0.0 { px } else { 0.0 };
px / scale
};
let corner = |w: f64, h: f64| CornerInset::new(to_logical(w), to_logical(h));
CornerInsets::new(
corner(physical[0], physical[1]),
corner(physical[2], physical[3]),
corner(physical[4], physical[5]),
corner(physical[6], physical[7]),
)
}
#[inline]
pub fn window_metrics(physical: (u32, u32), scale: f64, insets: WindowInsets) -> WindowMetrics {
let (logical_w, logical_h) = logical_size(physical.0, physical.1, scale);
WindowMetrics::new(Size::new(logical_w, logical_h), scale, insets)
}
#[derive(Debug, Default)]
pub struct WindowMetricsPublisher {
last: Option<WindowMetrics>,
}
impl WindowMetricsPublisher {
pub fn new() -> Self {
Self { last: None }
}
pub fn poll(
&mut self,
physical: (u32, u32),
scale: f64,
insets: WindowInsets,
) -> Option<WindowMetrics> {
let metrics = window_metrics(physical, scale, insets);
if self.last == Some(metrics) {
return None;
}
self.last = Some(metrics);
Some(metrics)
}
pub fn last(&self) -> Option<WindowMetrics> {
self.last
}
}
pub fn guard<T>(what: &str, default: T, f: impl FnOnce() -> T) -> T {
match catch_unwind(AssertUnwindSafe(f)) {
Ok(value) => value,
Err(_) => {
log::error!("frust-shell: panic caught at FFI boundary in {what}");
default
}
}
}
pub fn run_guarded_thread(what: &str, f: impl FnOnce()) {
if catch_unwind(AssertUnwindSafe(f)).is_err() {
log::error!(
"frust-shell: panic caught in render thread {what}; thread exiting cleanly \
(surface teardown + ack drain run via RenderReceiver drop)"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn logical_size_divides_by_scale() {
let (w, h) = logical_size(800, 600, 2.0);
assert_eq!((w, h), (400.0, 300.0));
}
#[test]
fn logical_size_identity_at_scale_one() {
assert_eq!(logical_size(1080, 1920, 1.0), (1080.0, 1920.0));
}
#[test]
fn sanitize_scale_passes_through_normal_values() {
assert_eq!(sanitize_scale(2.0), 2.0);
assert_eq!(sanitize_scale(1.0), 1.0);
assert_eq!(sanitize_scale(0.75), 0.75_f64);
}
#[test]
fn sanitize_scale_falls_back_on_bogus_values() {
for bad in [0.0_f32, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert_eq!(
sanitize_scale(bad),
1.0,
"scale {bad} should fall back to 1.0"
);
}
}
#[test]
fn logical_size_falls_back_on_bogus_scale_once_sanitized() {
for bad in [0.0_f32, -1.0, f32::NAN, f32::INFINITY] {
let scale = sanitize_scale(bad);
let (w, h) = logical_size(100, 200, scale);
assert_eq!(
(w, h),
(100.0, 200.0),
"scale {bad} should fall back to 1.0"
);
}
}
#[test]
fn logical_insets_divides_each_edge_by_scale() {
let insets = logical_insets([0.0, 48.0, 0.0, 68.0, 0.0, 0.0, 0.0, 680.0], 2.0);
assert_eq!(
insets,
WindowInsets::new(
EdgeInsets::new(0.0, 24.0, 0.0, 34.0),
EdgeInsets::new(0.0, 0.0, 0.0, 340.0),
)
);
assert_eq!(insets.padding(), EdgeInsets::new(0.0, 24.0, 0.0, 0.0));
}
#[test]
fn logical_insets_identity_at_scale_one() {
let physical = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let insets = logical_insets(physical, 1.0);
assert_eq!(insets.view_padding, EdgeInsets::new(1.0, 2.0, 3.0, 4.0));
assert_eq!(insets.view_insets, EdgeInsets::new(5.0, 6.0, 7.0, 8.0));
}
#[test]
fn logical_insets_uses_sanitized_scale_from_caller() {
let scale = sanitize_scale(f32::NAN); let insets = logical_insets([0.0, 44.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], scale);
assert_eq!(insets.view_padding, EdgeInsets::new(0.0, 44.0, 0.0, 0.0));
}
#[test]
fn logical_insets_sanitizes_nan_edges_to_zero() {
let physical = [f64::NAN, 44.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0];
let insets = logical_insets(physical, 2.0);
assert_eq!(insets.view_padding, EdgeInsets::new(0.0, 22.0, 0.0, 0.0));
}
#[test]
fn logical_insets_sanitizes_infinite_edges_to_zero() {
let physical = [
f64::INFINITY,
44.0,
f64::NEG_INFINITY,
0.0,
0.0,
0.0,
0.0,
0.0,
];
let insets = logical_insets(physical, 2.0);
assert_eq!(insets.view_padding, EdgeInsets::new(0.0, 22.0, 0.0, 0.0));
}
#[test]
fn logical_insets_sanitizes_negative_edges_to_zero() {
let physical = [-10.0, 44.0, 0.0, -5.0, 0.0, 0.0, 0.0, 0.0];
let insets = logical_insets(physical, 2.0);
assert_eq!(insets.view_padding, EdgeInsets::new(0.0, 22.0, 0.0, 0.0));
}
#[test]
fn logical_insets_normal_values_unchanged() {
let physical = [8.0, 44.0, 16.0, 34.0, 0.0, 0.0, 0.0, 340.0];
let insets = logical_insets(physical, 2.0);
assert_eq!(insets.view_padding, EdgeInsets::new(4.0, 22.0, 8.0, 17.0));
assert_eq!(insets.view_insets, EdgeInsets::new(0.0, 0.0, 0.0, 170.0));
}
#[test]
fn logical_corner_insets_divides_each_value_by_scale() {
let c = logical_corner_insets([0.0, 0.0, 144.0, 48.0, 2.0, 4.0, 6.0, 8.0], 2.0);
assert_eq!(
c,
CornerInsets::new(
CornerInset::new(0.0, 0.0),
CornerInset::new(72.0, 24.0),
CornerInset::new(1.0, 2.0),
CornerInset::new(3.0, 4.0),
)
);
}
#[test]
fn logical_corner_insets_identity_at_scale_one() {
let c = logical_corner_insets([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0], 1.0);
assert_eq!(c.top_left, CornerInset::new(1.0, 2.0));
assert_eq!(c.top_right, CornerInset::new(3.0, 4.0));
assert_eq!(c.bottom_left, CornerInset::new(5.0, 6.0));
assert_eq!(c.bottom_right, CornerInset::new(7.0, 8.0));
}
#[test]
fn logical_corner_insets_sanitizes_nan_inf_negative_to_zero() {
let c = logical_corner_insets(
[
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
-4.0,
10.0,
-0.5,
f64::NAN,
20.0,
],
2.0,
);
assert_eq!(c.top_left, CornerInset::ZERO);
assert_eq!(c.top_right, CornerInset::ZERO);
assert_eq!(c.bottom_left, CornerInset::new(5.0, 0.0));
assert_eq!(c.bottom_right, CornerInset::new(0.0, 10.0));
}
use frust_core::Orientation;
#[test]
fn window_metrics_size_is_logical_on_every_shell() {
let android = window_metrics((1080, 2400), 3.0, WindowInsets::default());
assert_eq!(android.size, Size::new(360.0, 800.0));
assert_eq!(android.scale, 3.0);
let ios = window_metrics((1170, 2532), 3.0, WindowInsets::default());
assert_eq!(ios.size, Size::new(390.0, 844.0));
let desktop = window_metrics((1600, 1200), 2.0, WindowInsets::default());
assert_eq!(desktop.size, Size::new(800.0, 600.0));
let unit = window_metrics((800, 600), 1.0, WindowInsets::default());
assert_eq!(unit.size, Size::new(800.0, 600.0));
}
#[test]
fn window_metrics_passes_already_logical_insets_through_untouched() {
let logical = logical_insets([0.0, 48.0, 0.0, 68.0, 0.0, 0.0, 0.0, 0.0], 2.0);
assert_eq!(logical.view_padding, EdgeInsets::new(0.0, 24.0, 0.0, 34.0));
let metrics = window_metrics((800, 1600), 2.0, logical);
assert_eq!(metrics.insets, logical);
assert_eq!(
metrics.insets.view_padding,
EdgeInsets::new(0.0, 24.0, 0.0, 34.0)
);
}
#[test]
fn window_metrics_orientation_flips_when_width_and_height_cross_over() {
let portrait = window_metrics((1080, 2400), 3.0, WindowInsets::default());
assert_eq!(portrait.orientation, Orientation::Portrait);
let landscape = window_metrics((2400, 1080), 3.0, WindowInsets::default());
assert_eq!(landscape.orientation, Orientation::Landscape);
let square = window_metrics((1000, 1000), 2.0, WindowInsets::default());
assert_eq!(square.orientation, Orientation::Portrait);
}
#[test]
fn window_metrics_uses_sanitized_scale_from_caller() {
let scale = sanitize_scale(f32::NAN); let metrics = window_metrics((400, 800), scale, WindowInsets::default());
assert_eq!(metrics.size, Size::new(400.0, 800.0));
assert_eq!(metrics.scale, 1.0);
}
#[test]
fn window_metrics_publisher_reports_only_actual_changes() {
let mut pub_ = WindowMetricsPublisher::new();
assert_eq!(pub_.last(), None, "nothing published before the first poll");
let first = pub_
.poll((1080, 2400), 3.0, WindowInsets::default())
.expect("the seeding poll must publish");
assert_eq!(first.size, Size::new(360.0, 800.0));
assert_eq!(pub_.last(), Some(first));
for _ in 0..100 {
assert_eq!(
pub_.poll((1080, 2400), 3.0, WindowInsets::default()),
None,
"unchanged metrics must never be re-provided"
);
}
assert_eq!(
pub_.last(),
Some(first),
"a no-op poll leaves the last value"
);
let rotated = pub_
.poll((2400, 1080), 3.0, WindowInsets::default())
.expect("a rotation is a real change");
assert_eq!(rotated.orientation, Orientation::Landscape);
assert_eq!(pub_.poll((2400, 1080), 3.0, WindowInsets::default()), None);
}
#[test]
fn window_metrics_publisher_detects_each_input_independently() {
let base_insets = WindowInsets::default();
let mut pub_ = WindowMetricsPublisher::new();
assert!(pub_.poll((1080, 2400), 3.0, base_insets).is_some());
assert!(pub_.poll((1080, 2000), 3.0, base_insets).is_some());
assert!(pub_.poll((1080, 2000), 3.0, base_insets).is_none());
assert!(pub_.poll((1080, 2000), 2.0, base_insets).is_some());
assert!(pub_.poll((1080, 2000), 2.0, base_insets).is_none());
let ime_up = logical_insets([0.0, 48.0, 0.0, 0.0, 0.0, 0.0, 0.0, 680.0], 2.0);
assert!(pub_.poll((1080, 2000), 2.0, ime_up).is_some());
assert!(pub_.poll((1080, 2000), 2.0, ime_up).is_none());
assert!(pub_.poll((1080, 2000), 2.0, base_insets).is_some());
assert!(pub_.poll((1080, 2000), 2.0, base_insets).is_none());
}
#[test]
fn guard_returns_value_on_success() {
assert_eq!(guard("ok", 0, || 42), 42);
}
#[test]
fn guard_returns_default_on_panic() {
let out = guard("boom", -1, || panic!("simulated FFI callback panic"));
assert_eq!(
out, -1,
"a panic must be swallowed and the default returned"
);
}
#[test]
fn run_guarded_thread_runs_the_body_to_completion() {
use std::sync::atomic::{AtomicBool, Ordering};
let ran = AtomicBool::new(false);
run_guarded_thread("ok", || ran.store(true, Ordering::SeqCst));
assert!(ran.load(Ordering::SeqCst), "the body must run");
}
#[test]
fn run_guarded_thread_swallows_a_panic() {
run_guarded_thread("boom", || panic!("simulated render-thread panic"));
}
}