use super::Layout;
use crate::compat::{Any, Vec};
use crate::core::{ObjectId, Rect};
const NON_FINITE_FACTOR_DEFAULT: f32 = 1.0;
fn normalise_factor(factor: f32) -> f32 {
if factor.is_finite() {
factor.clamp(0.0, 1.0)
} else {
NON_FINITE_FACTOR_DEFAULT
}
}
fn visible_extent(parent: u32, factor: f32) -> u32 {
if factor == 0.0 || parent == 0 {
return 0;
}
let scaled = (parent as f32 * factor).round() as u32;
scaled.min(parent).max(1)
}
#[derive(Debug)]
pub struct CenterLayout {
child: Option<ObjectId>,
width_factor: f32,
height_factor: f32,
}
impl CenterLayout {
pub fn new() -> Self {
Self { child: None, width_factor: 1.0, height_factor: 1.0 }
}
pub fn with_factors(width_factor: f32, height_factor: f32) -> Self {
Self {
child: None,
width_factor: normalise_factor(width_factor),
height_factor: normalise_factor(height_factor),
}
}
pub fn set_width_factor(&mut self, factor: f32) {
if factor.is_finite() {
self.width_factor = factor.clamp(0.0, 1.0);
}
}
pub fn set_height_factor(&mut self, factor: f32) {
if factor.is_finite() {
self.height_factor = factor.clamp(0.0, 1.0);
}
}
pub fn width_factor(&self) -> f32 {
self.width_factor
}
pub fn height_factor(&self) -> f32 {
self.height_factor
}
pub fn child(&self) -> Option<ObjectId> {
self.child
}
}
crate::impl_default_via_new!(CenterLayout);
impl Layout for CenterLayout {
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_width = visible_extent(rect.width, self.width_factor);
let child_height = visible_extent(rect.height, self.height_factor);
let x_offset = (rect.width.saturating_sub(child_width) / 2) as i32;
let y_offset = (rect.height.saturating_sub(child_height) / 2) as i32;
let child_rect =
Rect::new(rect.x + x_offset, rect.y + y_offset, child_width, child_height);
widgets(child_id, child_rect);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_center_full_size() {
let mut layout = CenterLayout::new();
layout.add_widget(42, 0);
let mut out = None;
layout.update(Rect::new(10, 20, 200, 100), &mut |id, rect| {
if id == 42 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!(rect.x, 10);
assert_eq!(rect.y, 20);
assert_eq!(rect.width, 200);
assert_eq!(rect.height, 100);
}
#[test]
fn test_center_half_size() {
let mut layout = CenterLayout::with_factors(0.5, 0.5);
layout.add_widget(7, 0);
let mut out = None;
layout.update(Rect::new(0, 0, 200, 100), &mut |id, rect| {
if id == 7 {
out = Some(rect);
}
});
let rect = out.expect("child should be positioned");
assert_eq!(rect.x, 50);
assert_eq!(rect.y, 25);
assert_eq!(rect.width, 100);
assert_eq!(rect.height, 50);
}
fn child_rect(layout: &CenterLayout, rect: Rect) -> Option<Rect> {
let mut out = None;
layout.update(rect, &mut |_, r| out = Some(r));
out
}
#[test]
fn a_positive_sub_pixel_factor_keeps_a_minimum_visible_pixel() {
let mut layout = CenterLayout::with_factors(0.5, 0.5);
layout.add_widget(1, 0);
let rect = child_rect(&layout, Rect::new(0, 0, 1, 1)).expect("child positioned");
assert_eq!(rect.width, 1, "0.5 of a 1px parent must round up to a visible pixel");
assert_eq!(rect.height, 1, "0.5 of a 1px parent must round up to a visible pixel");
}
#[test]
fn factors_zero_half_and_one_round_and_stay_within_the_parent() {
for (factor, expected) in [(0.0f32, 0u32), (0.5, 2), (1.0, 3)] {
let mut layout = CenterLayout::with_factors(factor, factor);
layout.add_widget(1, 0);
let rect = child_rect(&layout, Rect::new(0, 0, 3, 3)).expect("child positioned");
assert_eq!(rect.width, expected, "width for factor {factor}");
assert_eq!(rect.height, expected, "height for factor {factor}");
assert!(rect.width <= 3 && rect.height <= 3, "never exceeds the parent: {rect:?}");
}
}
#[test]
fn an_empty_parent_yields_a_zero_sized_child() {
let mut layout = CenterLayout::with_factors(0.5, 0.5);
layout.add_widget(1, 0);
let rect = child_rect(&layout, Rect::new(5, 5, 0, 0)).expect("child positioned");
assert_eq!(rect.width, 0);
assert_eq!(rect.height, 0);
}
#[test]
fn a_non_finite_constructor_factor_falls_back_to_the_documented_default() {
let layout = CenterLayout::with_factors(f32::NAN, f32::INFINITY);
assert_eq!(layout.width_factor(), 1.0, "NaN falls back to the documented default");
assert_eq!(layout.height_factor(), 1.0, "+Infinity falls back to the default too");
let layout = CenterLayout::with_factors(f32::NEG_INFINITY, f32::NAN);
assert_eq!(layout.width_factor(), 1.0, "-Infinity falls back to the default");
}
#[test]
fn non_finite_setters_are_rejected_and_finite_values_clamp() {
let mut layout = CenterLayout::with_factors(0.25, 0.75);
layout.set_width_factor(f32::NAN);
assert_eq!(layout.width_factor(), 0.25, "NaN must not overwrite the previous value");
layout.set_height_factor(f32::NEG_INFINITY);
assert_eq!(layout.height_factor(), 0.75, "-Infinity must not overwrite the previous value");
layout.set_width_factor(5.0);
assert_eq!(layout.width_factor(), 1.0, "a finite value above range clamps up");
layout.set_height_factor(-2.0);
assert_eq!(layout.height_factor(), 0.0, "a finite value below range clamps down");
layout.set_width_factor(f32::INFINITY);
assert_eq!(layout.width_factor(), 1.0, "+Infinity is still rejected");
}
#[test]
fn test_center_replace_child() {
let mut layout = CenterLayout::new();
layout.add_widget(1, 0);
layout.add_widget(2, 0);
assert!(!layout.has_child(1));
assert!(layout.has_child(2));
}
#[test]
fn test_center_remove_and_clear() {
let mut layout = CenterLayout::new();
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.has_child(20));
assert!(layout.child_ids().is_empty());
}
}