1use crate::layout::Rect;
6use crate::widget::traits::{RenderContext, View, WidgetProps};
7use crate::{impl_props_builders, impl_styled_view};
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum Anchor {
12 #[default]
14 TopLeft,
15 TopCenter,
17 TopRight,
19 MiddleLeft,
21 Center,
23 MiddleRight,
25 BottomLeft,
27 BottomCenter,
29 BottomRight,
31}
32
33pub struct Positioned {
57 child: Box<dyn View>,
58 x: Option<i16>,
59 y: Option<i16>,
60 percent_x: Option<f32>,
61 percent_y: Option<f32>,
62 width: Option<u16>,
63 height: Option<u16>,
64 anchor: Anchor,
65 props: WidgetProps,
67}
68
69impl Positioned {
70 pub fn new<V: View + 'static>(child: V) -> Self {
72 Self {
73 child: Box::new(child),
74 x: None,
75 y: None,
76 percent_x: None,
77 percent_y: None,
78 width: None,
79 height: None,
80 anchor: Anchor::default(),
81 props: WidgetProps::new(),
82 }
83 }
84
85 pub fn x(mut self, x: i16) -> Self {
87 self.x = Some(x);
88 self.percent_x = None;
89 self
90 }
91
92 pub fn y(mut self, y: i16) -> Self {
94 self.y = Some(y);
95 self.percent_y = None;
96 self
97 }
98
99 pub fn at(self, x: i16, y: i16) -> Self {
101 self.x(x).y(y)
102 }
103
104 pub fn percent_x(mut self, percent: f32) -> Self {
106 self.percent_x = Some(percent);
107 self.x = None;
108 self
109 }
110
111 pub fn percent_y(mut self, percent: f32) -> Self {
113 self.percent_y = Some(percent);
114 self.y = None;
115 self
116 }
117
118 pub fn percent(self, x: f32, y: f32) -> Self {
120 self.percent_x(x).percent_y(y)
121 }
122
123 pub fn width(mut self, width: u16) -> Self {
125 self.width = Some(width);
126 self
127 }
128
129 pub fn height(mut self, height: u16) -> Self {
131 self.height = Some(height);
132 self
133 }
134
135 pub fn size(self, width: u16, height: u16) -> Self {
137 self.width(width).height(height)
138 }
139
140 pub fn anchor(mut self, anchor: Anchor) -> Self {
142 self.anchor = anchor;
143 self
144 }
145
146 fn calculate_position(&self, parent: &Rect, child_width: u16, child_height: u16) -> (u16, u16) {
148 let base_x = if let Some(x) = self.x {
150 if x >= 0 {
151 parent.x.saturating_add(x as u16)
152 } else {
153 parent.x.saturating_sub((-x) as u16)
154 }
155 } else if let Some(percent) = self.percent_x {
156 let offset = (parent.width as f32 * percent / 100.0)
157 .max(0.0)
158 .min(parent.width as f32) as u16;
159 parent.x.saturating_add(offset)
160 } else {
161 parent.x
162 };
163
164 let base_y = if let Some(y) = self.y {
165 if y >= 0 {
166 parent.y.saturating_add(y as u16)
167 } else {
168 parent.y.saturating_sub((-y) as u16)
169 }
170 } else if let Some(percent) = self.percent_y {
171 let offset = (parent.height as f32 * percent / 100.0)
172 .max(0.0)
173 .min(parent.height as f32) as u16;
174 parent.y.saturating_add(offset)
175 } else {
176 parent.y
177 };
178
179 let (x, y) = match self.anchor {
181 Anchor::TopLeft => (base_x, base_y),
182 Anchor::TopCenter => (base_x.saturating_sub(child_width / 2), base_y),
183 Anchor::TopRight => (base_x.saturating_sub(child_width), base_y),
184 Anchor::MiddleLeft => (base_x, base_y.saturating_sub(child_height / 2)),
185 Anchor::Center => (
186 base_x.saturating_sub(child_width / 2),
187 base_y.saturating_sub(child_height / 2),
188 ),
189 Anchor::MiddleRight => (
190 base_x.saturating_sub(child_width),
191 base_y.saturating_sub(child_height / 2),
192 ),
193 Anchor::BottomLeft => (base_x, base_y.saturating_sub(child_height)),
194 Anchor::BottomCenter => (
195 base_x.saturating_sub(child_width / 2),
196 base_y.saturating_sub(child_height),
197 ),
198 Anchor::BottomRight => (
199 base_x.saturating_sub(child_width),
200 base_y.saturating_sub(child_height),
201 ),
202 };
203
204 (x, y)
205 }
206}
207
208impl View for Positioned {
209 crate::impl_view_meta!("Positioned");
210
211 fn render(&self, ctx: &mut RenderContext) {
212 let parent = ctx.area;
213 if parent.width == 0 || parent.height == 0 {
214 return;
215 }
216
217 let child_width = self.width.unwrap_or(parent.width);
219 let child_height = self.height.unwrap_or(parent.height);
220
221 let (x, y) = self.calculate_position(&parent, child_width, child_height);
223
224 let child_area = Rect::new(
226 x.max(parent.x).min(parent.x + parent.width),
227 y.max(parent.y).min(parent.y + parent.height),
228 child_width.min(parent.x + parent.width - x.min(parent.x + parent.width)),
229 child_height.min(parent.y + parent.height - y.min(parent.y + parent.height)),
230 );
231
232 let mut child_ctx = RenderContext::new(ctx.buffer, child_area);
234 self.child.render(&mut child_ctx);
235 }
236}
237
238impl_styled_view!(Positioned);
239impl_props_builders!(Positioned);
240
241pub fn positioned<V: View + 'static>(child: V) -> Positioned {
243 Positioned::new(child)
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use crate::render::Buffer;
250 use crate::widget::Text;
251
252 #[test]
253 fn test_positioned_new() {
254 let p = Positioned::new(Text::new("Test"));
255 assert_eq!(p.x, None);
256 assert_eq!(p.y, None);
257 assert_eq!(p.anchor, Anchor::TopLeft);
258 }
259
260 #[test]
261 fn test_positioned_absolute() {
262 let p = Positioned::new(Text::new("Test")).x(10).y(5);
263
264 assert_eq!(p.x, Some(10));
265 assert_eq!(p.y, Some(5));
266 }
267
268 #[test]
269 fn test_positioned_at() {
270 let p = Positioned::new(Text::new("Test")).at(15, 20);
271
272 assert_eq!(p.x, Some(15));
273 assert_eq!(p.y, Some(20));
274 }
275
276 #[test]
277 fn test_positioned_percent() {
278 let p = Positioned::new(Text::new("Test"))
279 .percent_x(50.0)
280 .percent_y(25.0);
281
282 assert_eq!(p.percent_x, Some(50.0));
283 assert_eq!(p.percent_y, Some(25.0));
284 assert_eq!(p.x, None);
285 assert_eq!(p.y, None);
286 }
287
288 #[test]
289 fn test_positioned_size() {
290 let p = Positioned::new(Text::new("Test")).width(20).height(10);
291
292 assert_eq!(p.width, Some(20));
293 assert_eq!(p.height, Some(10));
294 }
295
296 #[test]
297 fn test_positioned_anchor() {
298 let p = Positioned::new(Text::new("Test")).anchor(Anchor::Center);
299
300 assert_eq!(p.anchor, Anchor::Center);
301 }
302
303 #[test]
304 fn test_positioned_render() {
305 let p = Positioned::new(Text::new("Hello")).at(5, 2).size(10, 1);
306
307 let mut buffer = Buffer::new(30, 10);
308 let area = Rect::new(0, 0, 30, 10);
309 let mut ctx = RenderContext::new(&mut buffer, area);
310
311 p.render(&mut ctx);
312 }
314
315 #[test]
316 fn test_positioned_center() {
317 let p = Positioned::new(Text::new("Centered"))
318 .anchor(Anchor::Center)
319 .percent(50.0, 50.0)
320 .size(10, 1);
321
322 let mut buffer = Buffer::new(40, 20);
323 let area = Rect::new(0, 0, 40, 20);
324 let mut ctx = RenderContext::new(&mut buffer, area);
325
326 p.render(&mut ctx);
327 }
329
330 #[test]
331 fn test_positioned_helper() {
332 let p = positioned(Text::new("Test"));
333 assert_eq!(p.x, None);
334 }
335
336 #[test]
337 fn test_calculate_position_top_left() {
338 let p = Positioned::new(Text::new("Test"))
339 .at(10, 5)
340 .anchor(Anchor::TopLeft);
341
342 let parent = Rect::new(0, 0, 100, 50);
343 let (x, y) = p.calculate_position(&parent, 20, 3);
344
345 assert_eq!(x, 10);
346 assert_eq!(y, 5);
347 }
348
349 #[test]
350 fn test_calculate_position_center() {
351 let p = Positioned::new(Text::new("Test"))
352 .percent(50.0, 50.0)
353 .anchor(Anchor::Center);
354
355 let parent = Rect::new(0, 0, 100, 50);
356 let (x, y) = p.calculate_position(&parent, 20, 4);
357
358 assert_eq!(x, 40);
360 assert_eq!(y, 23);
362 }
363}