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}