1use serde::{Deserialize, Serialize};
10
11pub const OVERLAY_OFFSET: f64 = 4.0;
12pub const OVERLAY_PADDING: f64 = 8.0;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "lowercase")]
16pub enum OverlaySide {
17 Top,
18 Right,
19 Bottom,
20 Left,
21}
22
23impl OverlaySide {
24 pub fn parse(s: &str) -> Option<OverlaySide> {
25 Some(match s {
26 "top" => OverlaySide::Top,
27 "right" => OverlaySide::Right,
28 "bottom" => OverlaySide::Bottom,
29 "left" => OverlaySide::Left,
30 _ => return None,
31 })
32 }
33
34 pub fn as_str(&self) -> &'static str {
35 match self {
36 OverlaySide::Top => "top",
37 OverlaySide::Right => "right",
38 OverlaySide::Bottom => "bottom",
39 OverlaySide::Left => "left",
40 }
41 }
42
43 fn opposite(self) -> OverlaySide {
44 match self {
45 OverlaySide::Top => OverlaySide::Bottom,
46 OverlaySide::Bottom => OverlaySide::Top,
47 OverlaySide::Left => OverlaySide::Right,
48 OverlaySide::Right => OverlaySide::Left,
49 }
50 }
51
52 fn vertical(self) -> bool {
53 matches!(self, OverlaySide::Top | OverlaySide::Bottom)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
58#[serde(rename_all = "lowercase")]
59pub enum OverlayAlign {
60 Start,
61 #[default]
62 Center,
63 End,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
67pub struct Rect {
68 pub x: f64,
69 pub y: f64,
70 pub width: f64,
71 pub height: f64,
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize)]
75pub struct Size {
76 pub width: f64,
77 pub height: f64,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
81pub struct OverlayPlacement {
82 pub x: f64,
83 pub y: f64,
84 pub side: OverlaySide,
85 pub flipped: bool,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct PlaceOptions {
91 pub side: OverlaySide,
92 pub align: OverlayAlign,
93 pub offset: f64,
94 pub padding: f64,
95}
96
97impl Default for PlaceOptions {
98 fn default() -> Self {
99 PlaceOptions { side: OverlaySide::Bottom, align: OverlayAlign::Center, offset: OVERLAY_OFFSET, padding: OVERLAY_PADDING }
100 }
101}
102
103impl PlaceOptions {
104 pub fn side(side: OverlaySide) -> Self {
105 PlaceOptions { side, ..Default::default() }
106 }
107}
108
109fn room(side: OverlaySide, anchor: &Rect, viewport: &Size, padding: f64) -> f64 {
110 match side {
111 OverlaySide::Top => anchor.y - padding,
112 OverlaySide::Bottom => viewport.height - (anchor.y + anchor.height) - padding,
113 OverlaySide::Left => anchor.x - padding,
114 OverlaySide::Right => viewport.width - (anchor.x + anchor.width) - padding,
115 }
116}
117
118fn round3(n: f64) -> f64 {
119 (n * 1000.0).round() / 1000.0
120}
121
122pub fn place_overlay(anchor: &Rect, size: &Size, viewport: &Size, options: &PlaceOptions) -> OverlayPlacement {
124 let preferred = options.side;
125 let offset = options.offset;
126 let padding = options.padding;
127 let vertical = preferred.vertical();
128 let needed = if vertical { size.height } else { size.width } + offset;
129 let mut side = preferred;
130 let mut flipped = false;
131 let preferred_room = room(preferred, anchor, viewport, padding);
132 if preferred_room < needed && room(preferred.opposite(), anchor, viewport, padding) > preferred_room {
133 side = preferred.opposite();
134 flipped = true;
135 }
136 let along_x = || match options.align {
137 OverlayAlign::Start => anchor.x,
138 OverlayAlign::End => anchor.x + anchor.width - size.width,
139 OverlayAlign::Center => anchor.x + anchor.width / 2.0 - size.width / 2.0,
140 };
141 let along_y = || match options.align {
142 OverlayAlign::Start => anchor.y,
143 OverlayAlign::End => anchor.y + anchor.height - size.height,
144 OverlayAlign::Center => anchor.y + anchor.height / 2.0 - size.height / 2.0,
145 };
146 let (mut x, mut y) = match side {
147 OverlaySide::Top => (along_x(), anchor.y - offset - size.height),
148 OverlaySide::Bottom => (along_x(), anchor.y + anchor.height + offset),
149 OverlaySide::Left => (anchor.x - offset - size.width, along_y()),
150 OverlaySide::Right => (anchor.x + anchor.width + offset, along_y()),
151 };
152 if side.vertical() {
153 x = x.max(padding).min((viewport.width - padding - size.width).max(padding));
154 } else {
155 y = y.max(padding).min((viewport.height - padding - size.height).max(padding));
156 }
157 OverlayPlacement { x: round3(x), y: round3(y), side, flipped }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn a_flip_happens_only_when_the_opposite_side_has_more_room() {
166 let tight = place_overlay(
167 &Rect { x: 0.0, y: 290.0, width: 100.0, height: 20.0 },
168 &Size { width: 100.0, height: 400.0 },
169 &Size { width: 400.0, height: 600.0 },
170 &PlaceOptions::side(OverlaySide::Bottom),
171 );
172 assert!(!tight.flipped);
173 assert_eq!(tight.side, OverlaySide::Bottom);
174 }
175}