use super::Layout;
use crate::compat::{Any, Vec};
use crate::core::{ObjectId, Rect};
#[derive(Debug)]
pub struct AspectRatioLayout {
child: Option<ObjectId>,
aspect_ratio: f32,
respect_parent: bool,
}
impl AspectRatioLayout {
pub fn new(aspect_ratio: f32, respect_parent: bool) -> Self {
assert!(
aspect_ratio.is_finite() && aspect_ratio > 0.0,
"AspectRatioLayout: aspect_ratio must be finite and positive"
);
Self { child: None, aspect_ratio, respect_parent }
}
pub fn set_aspect_ratio(&mut self, ratio: f32) {
assert!(
ratio.is_finite() && ratio > 0.0,
"AspectRatioLayout: aspect_ratio must be finite and positive"
);
self.aspect_ratio = ratio;
}
pub fn aspect_ratio(&self) -> f32 {
self.aspect_ratio
}
pub fn set_respect_parent(&mut self, respect: bool) {
self.respect_parent = respect;
}
pub fn respect_parent(&self) -> bool {
self.respect_parent
}
fn compute_child_rect(&self, parent: Rect) -> Rect {
let parent_w = parent.width as f32;
let parent_h = parent.height as f32;
let ratio = self.aspect_ratio;
fn to_px(extent: f32) -> u32 {
if extent <= 0.0 {
return 0;
}
let rounded = extent.round();
if rounded >= 1.0 {
rounded as u32
} else {
1
}
}
let (child_w, child_h) = if self.respect_parent {
let by_width = (parent_w, parent_w / ratio);
let by_height = (parent_h * ratio, parent_h);
if by_width.1 <= parent_h {
(to_px(by_width.0), to_px(by_width.1))
} else {
(to_px(by_height.0), to_px(by_height.1))
}
} else {
let w = parent_w;
let h = w / ratio;
(to_px(w), to_px(h))
};
let x_offset = (parent.width.saturating_sub(child_w) / 2) as i32;
let y_offset = (parent.height.saturating_sub(child_h) / 2) as i32;
Rect::new(parent.x + x_offset, parent.y + y_offset, child_w, child_h)
}
}
impl Layout for AspectRatioLayout {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn add_widget(&mut self, widget_id: ObjectId, _stretch: u32) {
self.child = Some(widget_id);
}
fn remove_widget(&mut self, widget_id: ObjectId) {
if self.child == Some(widget_id) {
self.child = None;
}
}
fn child_ids(&self) -> Vec<ObjectId> {
self.child.into_iter().collect()
}
fn has_child(&self, id: ObjectId) -> bool {
self.child == Some(id)
}
fn clear(&mut self) {
self.child = None;
}
fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
if let Some(child_id) = self.child {
let child_rect = self.compute_child_rect(rect);
widgets(child_id, child_rect);
}
}
}
#[cfg(test)]
#[allow(clippy::items_after_test_module)]
mod tests {
use super::*;
#[test]
fn test_aspect_ratio_fit_width() {
let layout = AspectRatioLayout::new_with_child(42, 2.0, true);
let mut out = None;
layout.update(Rect::new(0, 0, 200, 100), &mut |id, rect| {
if id == 42 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!(rect.width, 200);
assert_eq!(rect.height, 100);
}
#[test]
fn test_aspect_ratio_fit_height() {
let layout = AspectRatioLayout::new_with_child(42, 1.0, true);
let mut out = None;
layout.update(Rect::new(0, 0, 200, 50), &mut |id, rect| {
if id == 42 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!(rect.width, 50);
assert_eq!(rect.height, 50);
assert_eq!(rect.x, 75);
assert_eq!(rect.y, 0);
}
#[test]
fn test_aspect_ratio_does_not_respect_parent() {
let layout = AspectRatioLayout::new_with_child(1, 1.0, false);
let mut out = None;
layout.update(Rect::new(0, 0, 100, 100), &mut |id, rect| {
if id == 1 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!(rect.width, 100);
assert_eq!(rect.height, 100);
}
#[test]
fn test_aspect_ratio_remove_and_clear() {
let mut layout = AspectRatioLayout::new(2.0, true);
layout.add_widget(10, 0);
assert!(layout.has_child(10));
layout.remove_widget(10);
assert!(!layout.has_child(10));
layout.add_widget(20, 0);
layout.clear();
assert!(layout.child_ids().is_empty());
}
#[test]
fn a_positive_sub_pixel_extent_keeps_one_pixel() {
let layout = AspectRatioLayout::new_with_child(42, 4.0, true);
let mut out = None;
layout.update(Rect::new(0, 0, 1, 1), &mut |id, rect| {
if id == 42 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!(rect.width, 1);
assert_eq!(rect.height, 1, "a positive extent must not collapse to zero");
}
#[test]
fn an_empty_parent_still_yields_a_zero_sized_child() {
let layout = AspectRatioLayout::new_with_child(42, 2.0, true);
let mut out = None;
layout.update(Rect::new(0, 0, 0, 0), &mut |id, rect| {
if id == 42 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!((rect.width, rect.height), (0, 0));
}
#[test]
fn non_finite_ratios_are_rejected() {
assert!(std::panic::catch_unwind(|| AspectRatioLayout::new(f32::INFINITY, true)).is_err());
assert!(std::panic::catch_unwind(|| AspectRatioLayout::new(f32::NAN, true)).is_err());
assert!(std::panic::catch_unwind(|| AspectRatioLayout::new(0.0, true)).is_err());
assert!(std::panic::catch_unwind(|| AspectRatioLayout::new(-1.0, true)).is_err());
let mut layout = AspectRatioLayout::new(2.0, true);
assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
layout.set_aspect_ratio(f32::INFINITY)
}))
.is_err());
}
}
#[cfg(test)]
impl AspectRatioLayout {
pub fn new_with_child(child_id: ObjectId, aspect_ratio: f32, respect_parent: bool) -> Self {
assert!(
aspect_ratio.is_finite() && aspect_ratio > 0.0,
"AspectRatioLayout: aspect_ratio must be finite and positive"
);
Self { child: Some(child_id), aspect_ratio, respect_parent }
}
}