Skip to main content

biji_ui/utils/
positioning.rs

1/// Specifies where content should be positioned relative to a trigger element.
2///
3/// Each variant places the content along one of the four edges of the trigger,
4/// with optional alignment (`Start` / `End`) along the perpendicular axis.
5/// When no alignment suffix is given, the content is centered along that edge.
6///
7/// ```text
8///         TopStart    Top    TopEnd
9///            ┌─────────────────┐
10/// LeftStart  │                 │  RightStart
11///       Left │     trigger     │  Right
12///  LeftEnd   │                 │  RightEnd
13///            └─────────────────┘
14///      BottomStart  Bottom  BottomEnd
15/// ```
16#[derive(Copy, Clone, Debug, Default)]
17pub enum Positioning {
18    /// Centered above the trigger.
19    #[default]
20    Top,
21    /// Above the trigger, aligned to the start (left) edge.
22    TopStart,
23    /// Above the trigger, aligned to the end (right) edge.
24    TopEnd,
25    /// Centered to the right of the trigger.
26    Right,
27    /// To the right of the trigger, aligned to the start (top) edge.
28    RightStart,
29    /// To the right of the trigger, aligned to the end (bottom) edge.
30    RightEnd,
31    /// Centered below the trigger.
32    Bottom,
33    /// Below the trigger, aligned to the start (left) edge.
34    BottomStart,
35    /// Below the trigger, aligned to the end (right) edge.
36    BottomEnd,
37    /// Centered to the left of the trigger.
38    Left,
39    /// To the left of the trigger, aligned to the start (top) edge.
40    LeftStart,
41    /// To the left of the trigger, aligned to the end (bottom) edge.
42    LeftEnd,
43}
44
45impl Positioning {
46    /// Calculate the position of content relative to a trigger element.
47    ///
48    /// Returns `(top, left)` coordinates in pixels, suitable for use with
49    /// `position: fixed` CSS styling.
50    ///
51    /// # Arguments
52    ///
53    /// * `trigger_top` / `trigger_left` – the trigger element's viewport coordinates.
54    /// * `trigger_width` / `trigger_height` – the trigger element's dimensions.
55    /// * `content_height` / `content_width` – the content element's dimensions.
56    /// * `offset` – additional spacing (in pixels) between the trigger and content.
57    pub fn calculate_position(
58        self,
59        trigger_top: f64,
60        trigger_left: f64,
61        trigger_width: f64,
62        trigger_height: f64,
63        content_height: f64,
64        content_width: f64,
65        offset: f64,
66    ) -> (f64, f64) {
67        match self {
68            Positioning::Top => {
69                let top = trigger_top - content_height - offset;
70                let left = trigger_left + (trigger_width / 2.0) - (content_width / 2.0);
71                (top, left)
72            }
73            Positioning::TopStart => {
74                let top = trigger_top - content_height - offset;
75                (top, trigger_left)
76            }
77            Positioning::TopEnd => {
78                let top = trigger_top - content_height - offset;
79                let left = trigger_left + trigger_width - content_width;
80                (top, left)
81            }
82            Positioning::Right => {
83                let top = trigger_top + (trigger_height / 2.0) - (content_height / 2.0);
84                let left = trigger_left + trigger_width + offset;
85                (top, left)
86            }
87            Positioning::RightStart => {
88                let left = trigger_left + trigger_width + offset;
89                (trigger_top, left)
90            }
91            Positioning::RightEnd => {
92                let top = trigger_top + trigger_height - content_height;
93                let left = trigger_left + trigger_width + offset;
94                (top, left)
95            }
96            Positioning::Bottom => {
97                let top = trigger_top + trigger_height + offset;
98                let left = trigger_left + (trigger_width / 2.0) - (content_width / 2.0);
99                (top, left)
100            }
101            Positioning::BottomStart => {
102                let top = trigger_top + trigger_height + offset;
103                (top, trigger_left)
104            }
105            Positioning::BottomEnd => {
106                let top = trigger_top + trigger_height + offset;
107                let left = trigger_left + trigger_width - content_width;
108                (top, left)
109            }
110            Positioning::Left => {
111                let top = trigger_top + (trigger_height / 2.0) - (content_height / 2.0);
112                let left = trigger_left - content_width - offset;
113                (top, left)
114            }
115            Positioning::LeftStart => {
116                let left = trigger_left - content_width - offset;
117                (trigger_top, left)
118            }
119            Positioning::LeftEnd => {
120                let left = trigger_left - content_width - offset;
121                let top = trigger_top + trigger_height - content_height;
122                (top, left)
123            }
124        }
125    }
126
127    /// Calculate the position and rotation for an arrow indicator.
128    ///
129    /// Returns `(top, left, rotation)` where `top` and `left` are pixel
130    /// coordinates and `rotation` is in degrees. The arrow is intended to
131    /// be a small square element rotated so that one corner points toward
132    /// the trigger.
133    ///
134    /// # Arguments
135    ///
136    /// * `trigger_top` / `trigger_left` – the trigger element's viewport coordinates.
137    /// * `trigger_width` / `trigger_height` – the trigger element's dimensions.
138    /// * `arrow_size` – the width/height of the square arrow element in pixels.
139    pub fn calculate_arrow_position(
140        self,
141        trigger_top: f64,
142        trigger_left: f64,
143        trigger_width: f64,
144        trigger_height: f64,
145        arrow_size: f64,
146    ) -> (f64, f64, i32) {
147        match self {
148            Positioning::Top | Positioning::TopStart | Positioning::TopEnd => {
149                let top = trigger_top - arrow_size - (arrow_size / 2.0);
150                let left = trigger_left + (trigger_width / 2.0) - (arrow_size / 2.0);
151                (top, left, 225)
152            }
153            Positioning::Right | Positioning::RightStart | Positioning::RightEnd => {
154                let top = trigger_top + (trigger_height / 2.0) - (arrow_size / 2.0);
155                let left = trigger_left + trigger_width + (arrow_size / 2.0);
156                (top, left, 315)
157            }
158            Positioning::Bottom | Positioning::BottomStart | Positioning::BottomEnd => {
159                let top = trigger_top + trigger_height + arrow_size - (arrow_size / 2.0);
160                let left = trigger_left + (trigger_width / 2.0) - (arrow_size / 2.0);
161                (top, left, 45)
162            }
163            Positioning::Left | Positioning::LeftStart | Positioning::LeftEnd => {
164                let top = trigger_top + (trigger_height / 2.0) - (arrow_size / 2.0);
165                let left = trigger_left - arrow_size - (arrow_size / 2.0);
166                (top, left, 135)
167            }
168        }
169    }
170
171    /// Calculate position as a CSS `style` attribute string including arrow CSS custom properties.
172    ///
173    /// The returned string sets `position: fixed`, `top`, `left`, and three
174    /// CSS custom properties consumed by the arrow element:
175    ///
176    /// * `--biji-tooltip-arrow-top`
177    /// * `--biji-tooltip-arrow-left`
178    /// * `--biji-tooltip-arrow-rotation`
179    pub fn calculate_position_style(
180        self,
181        trigger_top: f64,
182        trigger_left: f64,
183        trigger_width: f64,
184        trigger_height: f64,
185        content_height: f64,
186        content_width: f64,
187        offset: f64,
188        arrow_size: f64,
189    ) -> String {
190        let position = self.calculate_position(
191            trigger_top,
192            trigger_left,
193            trigger_width,
194            trigger_height,
195            content_height,
196            content_width,
197            offset,
198        );
199        let arrow_position = self.calculate_arrow_position(
200            trigger_top,
201            trigger_left,
202            trigger_width,
203            trigger_height,
204            arrow_size,
205        );
206        format!(
207            "position: fixed; top: {}px; left: {}px; --biji-tooltip-arrow-top: {}px; --biji-tooltip-arrow-left: {}px; --biji-tooltip-arrow-rotation: {}deg;",
208            position.0, position.1, arrow_position.0, arrow_position.1, arrow_position.2
209        )
210    }
211
212    /// Calculate position as a simple CSS `style` attribute string without arrow variables.
213    ///
214    /// Returns a string containing only `position: fixed`, `top`, and `left`.
215    /// Use this when an arrow indicator is not needed (e.g., dropdown menus).
216    pub fn calculate_position_style_simple(
217        self,
218        trigger_top: f64,
219        trigger_left: f64,
220        trigger_width: f64,
221        trigger_height: f64,
222        content_height: f64,
223        content_width: f64,
224        offset: f64,
225    ) -> String {
226        let position = self.calculate_position(
227            trigger_top,
228            trigger_left,
229            trigger_width,
230            trigger_height,
231            content_height,
232            content_width,
233            offset,
234        );
235        format!(
236            "position: fixed; top: {}px; left: {}px;",
237            position.0, position.1
238        )
239    }
240}