use super::Layout;
use crate::compat::{Any, Box, Vec};
use crate::core::{ObjectId, Rect, Size};
use crate::widget::Widget;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AbsolutePosition {
pub x: i32,
pub y: i32,
pub width: Option<u32>,
pub height: Option<u32>,
pub anchor: Anchor,
}
impl AbsolutePosition {
pub fn new(x: i32, y: i32) -> Self {
Self { x, y, width: None, height: None, anchor: Anchor::TopLeft }
}
pub fn with_size(mut self, width: u32, height: u32) -> Self {
self.width = Some(width);
self.height = Some(height);
self
}
pub fn with_anchor(mut self, anchor: Anchor, offset_x: i32, offset_y: i32) -> Self {
self.anchor = anchor;
self.x = offset_x;
self.y = offset_y;
self
}
pub fn with_anchor_only(mut self, anchor: Anchor) -> Self {
self.anchor = anchor;
self
}
pub fn to_rect(&self, _parent_size: Size, child_size: Size) -> Rect {
let width = self.width.unwrap_or(child_size.width);
let height = self.height.unwrap_or(child_size.height);
let (x, y) = match self.anchor {
Anchor::TopLeft => (self.x, self.y),
Anchor::TopCenter => {
let x = self.x - (width as i32) / 2;
(x, self.y)
}
Anchor::TopRight => {
let x = self.x - width as i32;
(x, self.y)
}
Anchor::CenterLeft => {
let y = self.y - (height as i32) / 2;
(self.x, y)
}
Anchor::Center => {
let x = self.x - (width as i32) / 2;
let y = self.y - (height as i32) / 2;
(x, y)
}
Anchor::CenterRight => {
let x = self.x - width as i32;
let y = self.y - (height as i32) / 2;
(x, y)
}
Anchor::BottomLeft => {
let y = self.y - height as i32;
(self.x, y)
}
Anchor::BottomCenter => {
let x = self.x - (width as i32) / 2;
let y = self.y - height as i32;
(x, y)
}
Anchor::BottomRight => {
let x = self.x - width as i32;
let y = self.y - height as i32;
(x, y)
}
};
Rect::new(x, y, width, height)
}
pub fn to_rect_in_parent(&self, parent_size: Size, child_size: Size) -> Rect {
let width = self.width.unwrap_or(child_size.width);
let height = self.height.unwrap_or(child_size.height);
let pw = parent_size.width as i32;
let ph = parent_size.height as i32;
let x = match self.anchor.horizontal() {
HorizontalAnchor::Start => self.x,
HorizontalAnchor::Center => (pw - width as i32) / 2 + self.x,
HorizontalAnchor::End => pw - width as i32 - self.x,
};
let y = match self.anchor.vertical() {
VerticalAnchor::Start => self.y,
VerticalAnchor::Center => (ph - height as i32) / 2 + self.y,
VerticalAnchor::End => ph - height as i32 - self.y,
};
Rect::new(x, y, width, height)
}
}
impl Default for AbsolutePosition {
fn default() -> Self {
Self::new(0, 0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Anchor {
#[default]
TopLeft,
TopCenter,
TopRight,
CenterLeft,
Center,
CenterRight,
BottomLeft,
BottomCenter,
BottomRight,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HorizontalAnchor {
Start,
Center,
End,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerticalAnchor {
Start,
Center,
End,
}
impl Anchor {
pub fn horizontal(self) -> HorizontalAnchor {
match self {
Anchor::TopLeft | Anchor::CenterLeft | Anchor::BottomLeft => HorizontalAnchor::Start,
Anchor::TopCenter | Anchor::Center | Anchor::BottomCenter => HorizontalAnchor::Center,
Anchor::TopRight | Anchor::CenterRight | Anchor::BottomRight => HorizontalAnchor::End,
}
}
pub fn vertical(self) -> VerticalAnchor {
match self {
Anchor::TopLeft | Anchor::TopCenter | Anchor::TopRight => VerticalAnchor::Start,
Anchor::CenterLeft | Anchor::Center | Anchor::CenterRight => VerticalAnchor::Center,
Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight => VerticalAnchor::End,
}
}
}
pub use Anchor as AbsoluteAnchor;
#[derive(Debug, Clone, Copy)]
pub struct Constraint {
pub min_width: Option<u32>,
pub max_width: Option<u32>,
pub min_height: Option<u32>,
pub max_height: Option<u32>,
pub aspect_ratio: Option<f32>,
}
impl Constraint {
pub fn new() -> Self {
Self {
min_width: None,
max_width: None,
min_height: None,
max_height: None,
aspect_ratio: None,
}
}
pub fn with_min_width(mut self, width: u32) -> Self {
self.min_width = Some(width);
self
}
pub fn with_max_width(mut self, width: u32) -> Self {
self.max_width = Some(width);
self
}
pub fn with_min_height(mut self, height: u32) -> Self {
self.min_height = Some(height);
self
}
pub fn with_max_height(mut self, height: u32) -> Self {
self.max_height = Some(height);
self
}
pub fn with_aspect_ratio(mut self, ratio: f32) -> Self {
self.aspect_ratio = Some(ratio);
self
}
pub fn apply(&self, size: Size) -> Size {
let mut width = size.width;
let mut height = size.height;
if let Some(min) = self.min_width {
width = width.max(min);
}
if let Some(max) = self.max_width {
width = width.min(max);
}
if let Some(min) = self.min_height {
height = height.max(min);
}
if let Some(max) = self.max_height {
height = height.min(max);
}
if let Some(ratio) = self.aspect_ratio {
if !(ratio.is_finite() && ratio > 0.0) {
return Size::new(width, height);
}
let height_f = height.max(1) as f32;
let current_ratio = width as f32 / height_f;
if current_ratio > ratio {
width = (height_f * ratio).round() as u32;
} else {
height = (width as f32 / ratio).round() as u32;
}
}
Size::new(width, height)
}
}
crate::impl_default_via_new!(Constraint);
pub struct AbsoluteLayout {
children: Vec<AbsoluteEntry>,
}
struct AbsoluteEntry {
widget_id: ObjectId,
widget: Option<Box<dyn Widget>>,
position: AbsolutePosition,
constraint: Option<Constraint>,
}
impl AbsoluteLayout {
pub fn new() -> Self {
Self { children: Vec::new() }
}
pub fn add_child(&mut self, child: Box<dyn Widget>, position: AbsolutePosition) {
let widget_id = child.id();
self.children.push(AbsoluteEntry {
widget_id,
widget: Some(child),
position,
constraint: None,
});
}
pub fn add_child_with_constraint(
&mut self,
child: Box<dyn Widget>,
position: AbsolutePosition,
constraint: Constraint,
) {
let widget_id = child.id();
self.children.push(AbsoluteEntry {
widget_id,
widget: Some(child),
position,
constraint: Some(constraint),
});
}
pub fn remove_child(&mut self, index: usize) -> Option<Box<dyn Widget>> {
if index < self.children.len() {
self.children.remove(index).widget
} else {
None
}
}
pub fn clear_children(&mut self) {
self.children.clear();
}
pub fn child_count(&self) -> usize {
self.children.len()
}
pub fn layout(&self, parent_rect: Rect) -> Vec<Rect> {
let parent_size = parent_rect.size();
let mut positions = Vec::new();
for entry in &self.children {
let child_size =
entry.widget.as_ref().map(|w| w.size_hint()).unwrap_or(Size::new(0, 0));
let constrained_size = entry
.constraint
.as_ref()
.map(|constraint| constraint.apply(child_size))
.unwrap_or(child_size);
let rect = entry.position.to_rect(parent_size, constrained_size);
positions.push(rect);
}
positions
}
pub fn calculate_positions(
&self,
container: &Rect,
positions: &[AbsolutePosition],
sizes: &[Size],
) -> Vec<Rect> {
let mut rects = Vec::new();
let parent_size = container.size();
for (position, size) in positions.iter().zip(sizes.iter()) {
let rect = position.to_rect(parent_size, *size);
rects.push(rect);
}
rects
}
pub fn set_position(&mut self, index: usize, position: AbsolutePosition) -> bool {
if let Some(entry) = self.children.get_mut(index) {
entry.position = position;
true
} else {
false
}
}
pub fn set_constraint(&mut self, index: usize, constraint: Constraint) -> bool {
if let Some(entry) = self.children.get_mut(index) {
entry.constraint = Some(constraint);
true
} else {
false
}
}
pub fn get_position(&self, index: usize) -> Option<&AbsolutePosition> {
self.children.get(index).map(|entry| &entry.position)
}
pub fn get_constraint(&self, index: usize) -> Option<&Constraint> {
self.children.get(index).and_then(|entry| entry.constraint.as_ref())
}
}
crate::impl_default_via_new!(AbsoluteLayout);
impl Layout for AbsoluteLayout {
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) {
if !self.children.iter().any(|entry| entry.widget_id == widget_id) {
self.children.push(AbsoluteEntry {
widget_id,
widget: None,
position: AbsolutePosition::new(0, 0),
constraint: None,
});
}
}
fn remove_widget(&mut self, widget_id: ObjectId) {
self.children.retain(|entry| entry.widget_id != widget_id);
}
fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
let positions = self.layout(rect);
for (i, child_rect) in positions.iter().enumerate() {
if let Some(entry) = self.children.get(i) {
widgets(entry.widget_id, *child_rect);
}
}
}
fn child_ids(&self) -> Vec<ObjectId> {
let mut ids: Vec<ObjectId> = Vec::new();
for entry in &self.children {
if !ids.contains(&entry.widget_id) {
ids.push(entry.widget_id);
}
}
ids
}
fn has_child(&self, id: ObjectId) -> bool {
self.children.iter().any(|entry| entry.widget_id == id)
}
fn clear(&mut self) {
self.clear_children();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::HashMap;
struct TestWidget {
id: ObjectId,
size: Size,
}
impl TestWidget {
fn new(id: ObjectId, width: u32, height: u32) -> Self {
Self { id, size: Size::new(width, height) }
}
}
impl crate::event::EventHandler for TestWidget {
fn handle_event(&mut self, _event: &crate::event::Event) {}
}
impl Widget for TestWidget {
fn id(&self) -> ObjectId {
self.id
}
fn size_hint(&self) -> Size {
self.size
}
}
#[test]
fn remove_keeps_identity_and_geometry_aligned() {
let mut layout = AbsoluteLayout::new();
layout.add_widget(1, 0);
layout.set_position(0, AbsolutePosition::new(10, 10));
layout.add_child(Box::new(TestWidget::new(2, 20, 20)), AbsolutePosition::new(50, 50));
assert_eq!(layout.child_count(), 2);
layout.remove_widget(1);
assert_eq!(layout.child_count(), 1, "removing an id-only child must drop its slot");
let mut rects = HashMap::new();
layout.update(Rect::new(0, 0, 100, 100), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.len(), 1, "the deleted sibling is gone from the update");
assert_eq!(
rects.get(&2),
Some(&Rect::new(50, 50, 20, 20)),
"the survivor keeps its own geometry, not the deleted sibling's"
);
}
#[test]
fn removing_any_position_keeps_survivors_aligned() {
let mut layout = AbsoluteLayout::new();
for (id, x) in [(1u64, 10i32), (2, 20), (3, 30)] {
layout.add_widget(id, 0);
let index = layout.child_count() - 1;
layout.set_position(index, AbsolutePosition::new(x, 0));
}
layout.remove_widget(2);
assert_eq!(layout.child_ids(), vec![1, 3]);
let mut rects = HashMap::new();
layout.update(Rect::new(0, 0, 100, 100), &mut |id, rect| {
rects.insert(id, rect);
});
assert_eq!(rects.get(&1).map(|r| r.x), Some(10));
assert_eq!(rects.get(&3).map(|r| r.x), Some(30));
}
#[test]
fn duplicate_register_does_not_add_a_second_entry() {
let mut layout = AbsoluteLayout::new();
layout.add_widget(1, 0);
layout.add_widget(1, 0);
assert_eq!(layout.child_count(), 1);
assert_eq!(layout.child_ids(), vec![1]);
}
#[test]
fn dyn_clear_removes_absolute_children() {
let mut layout: Box<dyn Layout> = Box::new(AbsoluteLayout::new());
layout.add_widget(1, 0);
layout.add_widget(2, 0);
assert!(layout.has_child(1));
layout.clear();
assert!(!layout.has_child(1));
assert!(!layout.has_child(2));
assert!(layout.child_ids().is_empty());
layout.clear();
assert!(layout.child_ids().is_empty());
}
#[test]
fn test_absolute_position() {
let pos =
AbsolutePosition::new(100, 100).with_size(50, 50).with_anchor_only(Anchor::Center);
let rect = pos.to_rect(Size::new(200, 200), Size::new(50, 50));
assert_eq!(rect.x, 75);
assert_eq!(rect.y, 75);
assert_eq!(rect.width, 50);
assert_eq!(rect.height, 50);
}
#[test]
fn test_constraint() {
let constraint = Constraint::new()
.with_min_width(50)
.with_max_width(100)
.with_min_height(50)
.with_max_height(100);
let size = constraint.apply(Size::new(200, 200));
assert_eq!(size.width, 100);
assert_eq!(size.height, 100);
let size = constraint.apply(Size::new(30, 30));
assert_eq!(size.width, 50);
assert_eq!(size.height, 50);
}
#[test]
fn test_aspect_ratio() {
let constraint = Constraint::new().with_aspect_ratio(2.0);
let size = constraint.apply(Size::new(200, 100));
assert_eq!(size.width, 200);
assert_eq!(size.height, 100);
let size = constraint.apply(Size::new(200, 200));
assert_eq!(size.width, 200);
assert_eq!(size.height, 100);
}
#[test]
fn anchored_in_parent_pins_to_the_parents_edges() {
let parent = Size::new(400, 300);
let child = Size::new(60, 40);
let corner = AbsolutePosition::new(8, 8)
.with_anchor_only(Anchor::BottomRight)
.to_rect_in_parent(parent, child);
assert_eq!(corner, Rect::new(400 - 60 - 8, 300 - 40 - 8, 60, 40));
let centred = AbsolutePosition::new(0, 0)
.with_anchor_only(Anchor::Center)
.to_rect_in_parent(parent, child);
assert_eq!(centred, Rect::new(170, 130, 60, 40));
let top_left = AbsolutePosition::new(5, 6)
.with_anchor_only(Anchor::TopLeft)
.to_rect_in_parent(parent, child);
assert_eq!(top_left, Rect::new(5, 6, 60, 40));
let position = AbsolutePosition::new(400, 300).with_anchor_only(Anchor::BottomRight);
assert_eq!(
position.to_rect(parent, child),
Rect::new(400 - 60, 300 - 40, 60, 40),
"the old spelling places the child's own corner at the given point"
);
assert_eq!(
position.to_rect_in_parent(parent, child),
Rect::new(400 - 60 - 400, 300 - 40 - 300, 60, 40),
"the new spelling treats the point as an inset from the parent's edge"
);
}
#[test]
fn every_anchor_decomposes_onto_its_axes() {
for (anchor, horizontal, vertical) in [
(Anchor::TopLeft, HorizontalAnchor::Start, VerticalAnchor::Start),
(Anchor::TopCenter, HorizontalAnchor::Center, VerticalAnchor::Start),
(Anchor::TopRight, HorizontalAnchor::End, VerticalAnchor::Start),
(Anchor::CenterLeft, HorizontalAnchor::Start, VerticalAnchor::Center),
(Anchor::Center, HorizontalAnchor::Center, VerticalAnchor::Center),
(Anchor::CenterRight, HorizontalAnchor::End, VerticalAnchor::Center),
(Anchor::BottomLeft, HorizontalAnchor::Start, VerticalAnchor::End),
(Anchor::BottomCenter, HorizontalAnchor::Center, VerticalAnchor::End),
(Anchor::BottomRight, HorizontalAnchor::End, VerticalAnchor::End),
] {
assert_eq!(anchor.horizontal(), horizontal, "{anchor:?}");
assert_eq!(anchor.vertical(), vertical, "{anchor:?}");
}
}
}