use crate::layout::Rect;
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Anchor {
#[default]
TopLeft,
TopCenter,
TopRight,
MiddleLeft,
Center,
MiddleRight,
BottomLeft,
BottomCenter,
BottomRight,
}
pub struct Positioned {
child: Box<dyn View>,
x: Option<i16>,
y: Option<i16>,
percent_x: Option<f32>,
percent_y: Option<f32>,
width: Option<u16>,
height: Option<u16>,
anchor: Anchor,
min_width: u16,
min_height: u16,
max_width: u16,
max_height: u16,
props: WidgetProps,
}
impl Positioned {
pub fn new<V: View + 'static>(child: V) -> Self {
Self {
child: Box::new(child),
x: None,
y: None,
percent_x: None,
percent_y: None,
width: None,
height: None,
anchor: Anchor::default(),
min_width: 0,
min_height: 0,
max_width: 0,
max_height: 0,
props: WidgetProps::new(),
}
}
pub fn x(mut self, x: i16) -> Self {
self.x = Some(x);
self.percent_x = None;
self
}
pub fn y(mut self, y: i16) -> Self {
self.y = Some(y);
self.percent_y = None;
self
}
pub fn at(self, x: i16, y: i16) -> Self {
self.x(x).y(y)
}
pub fn percent_x(mut self, percent: f32) -> Self {
self.percent_x = Some(percent);
self.x = None;
self
}
pub fn percent_y(mut self, percent: f32) -> Self {
self.percent_y = Some(percent);
self.y = None;
self
}
pub fn percent(self, x: f32, y: f32) -> Self {
self.percent_x(x).percent_y(y)
}
pub fn width(mut self, width: u16) -> Self {
self.width = Some(width);
self
}
pub fn height(mut self, height: u16) -> Self {
self.height = Some(height);
self
}
pub fn size(self, width: u16, height: u16) -> Self {
self.width(width).height(height)
}
pub fn anchor(mut self, anchor: Anchor) -> Self {
self.anchor = anchor;
self
}
pub fn min_width(mut self, width: u16) -> Self {
self.min_width = width;
self
}
pub fn min_height(mut self, height: u16) -> Self {
self.min_height = height;
self
}
pub fn max_width(mut self, width: u16) -> Self {
self.max_width = width;
self
}
pub fn max_height(mut self, height: u16) -> Self {
self.max_height = height;
self
}
pub fn min_size(self, width: u16, height: u16) -> Self {
self.min_width(width).min_height(height)
}
pub fn max_size(self, width: u16, height: u16) -> Self {
self.max_width(width).max_height(height)
}
pub fn constrain(self, min_w: u16, min_h: u16, max_w: u16, max_h: u16) -> Self {
self.min_width(min_w)
.min_height(min_h)
.max_width(max_w)
.max_height(max_h)
}
fn apply_constraints(&self, area: Rect) -> Rect {
let eff_max_w = if self.max_width > 0 {
self.max_width.max(self.min_width)
} else {
u16::MAX
};
let eff_max_h = if self.max_height > 0 {
self.max_height.max(self.min_height)
} else {
u16::MAX
};
let width = area.width.clamp(self.min_width, eff_max_w);
let height = area.height.clamp(self.min_height, eff_max_h);
Rect::new(area.x, area.y, width, height)
}
fn calculate_position_relative(
&self,
parent_width: u16,
parent_height: u16,
child_width: u16,
child_height: u16,
) -> (u16, u16) {
let base_x = if let Some(x) = self.x {
if x >= 0 {
x as u16
} else {
0u16 }
} else if let Some(percent) = self.percent_x {
(parent_width as f32 * percent / 100.0)
.max(0.0)
.min(parent_width as f32) as u16
} else {
0
};
let base_y = if let Some(y) = self.y {
if y >= 0 {
y as u16
} else {
0u16
}
} else if let Some(percent) = self.percent_y {
(parent_height as f32 * percent / 100.0)
.max(0.0)
.min(parent_height as f32) as u16
} else {
0
};
let (x, y) = match self.anchor {
Anchor::TopLeft => (base_x, base_y),
Anchor::TopCenter => (base_x.saturating_sub(child_width / 2), base_y),
Anchor::TopRight => (base_x.saturating_sub(child_width), base_y),
Anchor::MiddleLeft => (base_x, base_y.saturating_sub(child_height / 2)),
Anchor::Center => (
base_x.saturating_sub(child_width / 2),
base_y.saturating_sub(child_height / 2),
),
Anchor::MiddleRight => (
base_x.saturating_sub(child_width),
base_y.saturating_sub(child_height / 2),
),
Anchor::BottomLeft => (base_x, base_y.saturating_sub(child_height)),
Anchor::BottomCenter => (
base_x.saturating_sub(child_width / 2),
base_y.saturating_sub(child_height),
),
Anchor::BottomRight => (
base_x.saturating_sub(child_width),
base_y.saturating_sub(child_height),
),
};
(x, y)
}
}
impl View for Positioned {
crate::impl_view_meta!("Positioned");
fn render(&self, ctx: &mut RenderContext) {
let parent = self.apply_constraints(ctx.area);
if parent.width == 0 || parent.height == 0 {
return;
}
let child_width = self.width.unwrap_or(parent.width);
let child_height = self.height.unwrap_or(parent.height);
let (rel_x, rel_y) = self.calculate_position_relative(
parent.width,
parent.height,
child_width,
child_height,
);
let clamped_x = rel_x.min(parent.width);
let clamped_y = rel_y.min(parent.height);
let bounded_w = child_width.min(parent.width.saturating_sub(clamped_x));
let bounded_h = child_height.min(parent.height.saturating_sub(clamped_y));
let child_area = ctx.sub_area(clamped_x, clamped_y, bounded_w, bounded_h);
let mut child_ctx = RenderContext::new(ctx.buffer, child_area);
self.child.render(&mut child_ctx);
}
}
impl_styled_view!(Positioned);
impl_props_builders!(Positioned);
pub fn positioned<V: View + 'static>(child: V) -> Positioned {
Positioned::new(child)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::Buffer;
use crate::widget::Text;
#[test]
fn test_positioned_new() {
let p = Positioned::new(Text::new("Hi"));
assert_eq!(p.anchor, Anchor::TopLeft);
assert!(p.x.is_none());
assert!(p.y.is_none());
}
#[test]
fn test_positioned_absolute() {
let p = Positioned::new(Text::new("Hi")).at(5, 10);
assert_eq!(p.x, Some(5));
assert_eq!(p.y, Some(10));
}
#[test]
fn test_positioned_percent() {
let p = Positioned::new(Text::new("Hi")).percent(50.0, 50.0);
assert_eq!(p.percent_x, Some(50.0));
assert_eq!(p.percent_y, Some(50.0));
assert!(p.x.is_none()); }
#[test]
fn test_positioned_anchor() {
let p = Positioned::new(Text::new("Hi")).anchor(Anchor::Center);
assert_eq!(p.anchor, Anchor::Center);
}
#[test]
fn test_positioned_calculate_position_top_left() {
let p = Positioned::new(Text::new("Hi")).at(10, 5);
let (x, y) = p.calculate_position_relative(80, 24, 10, 3);
assert_eq!(x, 10);
assert_eq!(y, 5);
}
#[test]
fn test_positioned_calculate_position_center() {
let p = Positioned::new(Text::new("Hi"))
.percent(50.0, 50.0)
.anchor(Anchor::Center)
.size(10, 4);
let (x, y) = p.calculate_position_relative(80, 24, 10, 4);
assert_eq!(x, 35);
assert_eq!(y, 10);
}
#[test]
fn test_positioned_render_absolute() {
let mut buf = Buffer::new(20, 10);
let area = Rect::new(0, 0, 20, 10);
let mut ctx = RenderContext::new(&mut buf, area);
let p = Positioned::new(Text::new("XY")).at(5, 3);
p.render(&mut ctx);
assert_eq!(buf.get(5, 3).unwrap().symbol, 'X');
assert_eq!(buf.get(6, 3).unwrap().symbol, 'Y');
}
#[test]
fn test_positioned_render_zero_area_no_panic() {
let mut buf = Buffer::new(10, 10);
let area = Rect::new(0, 0, 0, 0);
let mut ctx = RenderContext::new(&mut buf, area);
let p = Positioned::new(Text::new("X"));
p.render(&mut ctx);
}
#[test]
fn test_positioned_size() {
let p = Positioned::new(Text::new("Hi")).size(20, 10);
assert_eq!(p.width, Some(20));
assert_eq!(p.height, Some(10));
}
#[test]
fn test_positioned_helper_fn() {
let p = positioned(Text::new("X"));
assert_eq!(p.anchor, Anchor::TopLeft);
}
#[test]
fn test_anchor_default() {
assert_eq!(Anchor::default(), Anchor::TopLeft);
}
}