#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RectPx {
pub left: i32,
pub top: i32,
pub right: i32,
pub bottom: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct PointPx {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SizePx {
pub width: i32,
pub height: i32,
}
impl RectPx {
fn is_empty(&self) -> bool {
self.right <= self.left || self.bottom <= self.top
}
fn contains(&self, p: PointPx) -> bool {
self.left <= p.x && p.x < self.right && self.top <= p.y && p.y < self.bottom
}
}
pub(crate) fn in_safe_zone(p: PointPx, range: RectPx, tip: Option<RectPx>) -> bool {
let Some(tip) = tip else {
return range.contains(p);
};
if range.is_empty() || tip.is_empty() {
return range.contains(p) || tip.contains(p);
}
let a = Inclusive::from(range);
let b = Inclusive::from(tip);
let (x, y) = (i64::from(p.x), i64::from(p.y));
let in_bounds =
a.l.min(b.l) <= x && x <= a.r.max(b.r) && a.t.min(b.t) <= y && y <= a.b.max(b.b);
in_bounds
&& corner_ok((x, y), a, b)
&& corner_ok((-x, y), a.flip_x(), b.flip_x())
&& corner_ok((x, -y), a.flip_y(), b.flip_y())
&& corner_ok((-x, -y), a.flip_x().flip_y(), b.flip_x().flip_y())
}
#[derive(Clone, Copy)]
struct Inclusive {
l: i64,
t: i64,
r: i64,
b: i64,
}
impl From<RectPx> for Inclusive {
fn from(rc: RectPx) -> Self {
Self {
l: rc.left.into(),
t: rc.top.into(),
r: i64::from(rc.right) - 1,
b: i64::from(rc.bottom) - 1,
}
}
}
impl Inclusive {
fn flip_x(self) -> Self {
Self {
l: -self.r,
r: -self.l,
..self
}
}
fn flip_y(self) -> Self {
Self {
t: -self.b,
b: -self.t,
..self
}
}
}
fn corner_ok(p: (i64, i64), a: Inclusive, b: Inclusive) -> bool {
let corner = (a.l.min(b.l), a.t.min(b.t));
if (a.l, a.t) == corner || (b.l, b.t) == corner {
return true;
}
let (leftmost, topmost) = if a.l < b.l { (a, b) } else { (b, a) };
let from = (leftmost.l, leftmost.t);
let to = (topmost.l, topmost.t);
let side = |q: (i64, i64)| (to.0 - from.0) * (q.1 - from.1) - (to.1 - from.1) * (q.0 - from.0);
side(p) * side(corner).signum() <= 0
}
pub(crate) struct PlaceInput {
pub anchor: PointPx,
pub tip: SizePx,
pub work_area: RectPx,
pub offset_above: i32,
pub offset_below: i32,
}
pub(crate) fn place(input: &PlaceInput) -> PointPx {
let PlaceInput {
anchor,
tip,
work_area: wa,
offset_above,
offset_below,
} = *input;
let above = anchor.y - offset_above - tip.height;
let below = anchor.y + offset_below;
let room_above = above - wa.top;
let room_below = wa.bottom - (below + tip.height);
let y = if room_above >= 0 || (room_below < 0 && room_above >= room_below) {
above
} else {
below
};
PointPx {
x: clamp_into(anchor.x - tip.width / 2, tip.width, wa.left, wa.right),
y: clamp_into(y, tip.height, wa.top, wa.bottom),
}
}
fn clamp_into(start: i32, size: i32, lo: i32, hi: i32) -> i32 {
start.min(hi - size).max(lo)
}
pub(crate) fn force_break(text: &str, fit: &dyn Fn(&str) -> usize) -> String {
let mut out = String::with_capacity(text.len());
let mut rest = text;
while !rest.is_empty() {
let end = rest.find(['\r', '\n']).unwrap_or(rest.len());
let mut line = &rest[..end];
while !line.is_empty() {
let n = fit(line).max(1);
let cut = line.char_indices().nth(n).map_or(line.len(), |(i, _)| i);
out.push_str(&line[..cut]);
line = &line[cut..];
if !line.is_empty() {
out.push_str("\r\n");
}
}
let newline = if rest[end..].starts_with("\r\n") {
2
} else {
rest[end..].chars().next().map_or(0, char::len_utf8)
};
out.push_str(&rest[end..end + newline]);
rest = &rest[end + newline..];
}
out
}
pub(crate) fn normalize_newlines(text: &str) -> String {
text.replace("\r\n", "\n")
.replace('\r', "\n")
.replace('\n', "\r\n")
}
pub(crate) fn max_tip_width(dpi: u32, work_area: RectPx) -> i32 {
let scaled = (320 * i64::from(dpi) + 48) / 96;
let work = i64::from(work_area.right) - i64::from(work_area.left);
scaled.min(work) as i32
}
pub(crate) fn logical_max_width(physical: i32, window_dpi: u32) -> i32 {
(i64::from(physical) * 96 / i64::from(window_dpi.max(1))) as i32
}
#[cfg(test)]
#[path = "geometry_tests.rs"]
mod geometry_tests;