Skip to main content

revue/widget/layout/
positioned.rs

1//! Positioned widget for absolute positioning
2//!
3//! Allows placing widgets at specific coordinates within their parent area.
4
5use crate::layout::Rect;
6use crate::widget::traits::{RenderContext, View, WidgetProps};
7use crate::{impl_props_builders, impl_styled_view};
8
9/// Position anchor point
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum Anchor {
12    /// Top-left corner (default)
13    #[default]
14    TopLeft,
15    /// Top-center
16    TopCenter,
17    /// Top-right corner
18    TopRight,
19    /// Middle-left
20    MiddleLeft,
21    /// Center of the widget
22    Center,
23    /// Middle-right
24    MiddleRight,
25    /// Bottom-left corner
26    BottomLeft,
27    /// Bottom-center
28    BottomCenter,
29    /// Bottom-right corner
30    BottomRight,
31}
32
33/// A widget that positions its child at specific coordinates
34///
35/// The position can be specified as:
36/// - Absolute pixels from top-left
37/// - Percentage of parent area
38/// - Relative to different anchor points
39///
40/// # Example
41///
42/// ```rust,ignore
43/// use revue::prelude::*;
44///
45/// // Position at absolute coordinates
46/// let pos = Positioned::new(Text::new("Hello"))
47///     .x(10)
48///     .y(5);
49///
50/// // Position at center
51/// let centered = Positioned::new(Text::new("Centered"))
52///     .anchor(Anchor::Center)
53///     .percent_x(50.0)
54///     .percent_y(50.0);
55/// ```
56pub 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    /// CSS styling properties (id, classes)
66    props: WidgetProps,
67}
68
69impl Positioned {
70    /// Create a new positioned widget
71    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    /// Set absolute X position
86    pub fn x(mut self, x: i16) -> Self {
87        self.x = Some(x);
88        self.percent_x = None;
89        self
90    }
91
92    /// Set absolute Y position
93    pub fn y(mut self, y: i16) -> Self {
94        self.y = Some(y);
95        self.percent_y = None;
96        self
97    }
98
99    /// Set both X and Y position
100    pub fn at(self, x: i16, y: i16) -> Self {
101        self.x(x).y(y)
102    }
103
104    /// Set X position as percentage of parent width
105    pub fn percent_x(mut self, percent: f32) -> Self {
106        self.percent_x = Some(percent);
107        self.x = None;
108        self
109    }
110
111    /// Set Y position as percentage of parent height
112    pub fn percent_y(mut self, percent: f32) -> Self {
113        self.percent_y = Some(percent);
114        self.y = None;
115        self
116    }
117
118    /// Set both positions as percentages
119    pub fn percent(self, x: f32, y: f32) -> Self {
120        self.percent_x(x).percent_y(y)
121    }
122
123    /// Set fixed width for the child
124    pub fn width(mut self, width: u16) -> Self {
125        self.width = Some(width);
126        self
127    }
128
129    /// Set fixed height for the child
130    pub fn height(mut self, height: u16) -> Self {
131        self.height = Some(height);
132        self
133    }
134
135    /// Set both width and height
136    pub fn size(self, width: u16, height: u16) -> Self {
137        self.width(width).height(height)
138    }
139
140    /// Set the anchor point for positioning
141    pub fn anchor(mut self, anchor: Anchor) -> Self {
142        self.anchor = anchor;
143        self
144    }
145
146    /// Calculate final position based on settings and parent area
147    fn calculate_position(&self, parent: &Rect, child_width: u16, child_height: u16) -> (u16, u16) {
148        // Calculate base position
149        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        // Adjust for anchor point
180        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        // Determine child size
218        let child_width = self.width.unwrap_or(parent.width);
219        let child_height = self.height.unwrap_or(parent.height);
220
221        // Calculate position
222        let (x, y) = self.calculate_position(&parent, child_width, child_height);
223
224        // Create bounded child area
225        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        // Render child in calculated area
233        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
241/// Create a positioned widget
242pub 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        // Text should be rendered at position (5, 2)
313    }
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        // Text should be centered in the area
328    }
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        // 50% of 100 = 50, minus half of 20 = 40
359        assert_eq!(x, 40);
360        // 50% of 50 = 25, minus half of 4 = 23
361        assert_eq!(y, 23);
362    }
363}