#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Affine {
pub m: [f32; 6],
}
impl Default for Affine {
fn default() -> Self {
Self::IDENTITY
}
}
impl Affine {
pub const IDENTITY: Affine = Affine {
m: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
};
pub const fn translation(tx: f32, ty: f32) -> Affine {
Affine {
m: [1.0, 0.0, 0.0, 1.0, tx, ty],
}
}
pub const fn scale(sx: f32, sy: f32) -> Affine {
Affine {
m: [sx, 0.0, 0.0, sy, 0.0, 0.0],
}
}
pub fn rotation(angle: f32) -> Affine {
let (s, c) = angle.sin_cos();
Affine {
m: [c, s, -s, c, 0.0, 0.0],
}
}
pub fn then(self, next: Affine) -> Affine {
let [a, b, c, d, e, f] = self.m;
let [a2, b2, c2, d2, e2, f2] = next.m;
Affine {
m: [
a2 * a + c2 * b,
b2 * a + d2 * b,
a2 * c + c2 * d,
b2 * c + d2 * d,
a2 * e + c2 * f + e2,
b2 * e + d2 * f + f2,
],
}
}
pub fn about(self, pivot: Point) -> Affine {
Affine::translation(-pivot.x, -pivot.y)
.then(self)
.then(Affine::translation(pivot.x, pivot.y))
}
pub fn apply(self, p: Point) -> Point {
let [a, b, c, d, e, f] = self.m;
Point::new(a * p.x + c * p.y + e, b * p.x + d * p.y + f)
}
pub fn is_axis_aligned(self) -> bool {
self.m[1].abs() < 1e-4 && self.m[2].abs() < 1e-4
}
pub fn apply_rect(self, r: Rect) -> Rect {
let a = self.apply(Point::new(r.x, r.y));
let b = self.apply(Point::new(r.x + r.width, r.y + r.height));
Rect::new(
a.x.min(b.x),
a.y.min(b.y),
(a.x - b.x).abs(),
(a.y - b.y).abs(),
)
}
pub fn inverse(self) -> Affine {
let [a, b, c, d, e, f] = self.m;
let det = a * d - b * c;
if det.abs() < f32::EPSILON {
return Affine::IDENTITY;
}
let inv = 1.0 / det;
Affine {
m: [
d * inv,
-b * inv,
-c * inv,
a * inv,
(c * f - d * e) * inv,
(b * e - a * f) * inv,
],
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Point {
pub x: f32,
pub y: f32,
}
impl Point {
pub const ZERO: Self = Self::new(0.0, 0.0);
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn scale(self, factor: f32) -> Self {
Self::new(self.x * factor, self.y * factor)
}
pub fn scale_xy(self, sx: f32, sy: f32) -> Self {
Self::new(self.x * sx, self.y * sy)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum TextDirection {
#[default]
Ltr,
Rtl,
}
impl TextDirection {
pub fn is_rtl(self) -> bool {
matches!(self, TextDirection::Rtl)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Insets {
pub top: f32,
pub right: f32,
pub bottom: f32,
pub left: f32,
}
impl Insets {
pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
pub const fn new(top: f32, right: f32, bottom: f32, left: f32) -> Self {
Self {
top,
right,
bottom,
left,
}
}
pub const fn uniform(value: f32) -> Self {
Self::new(value, value, value, value)
}
pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
Self::new(vertical, horizontal, vertical, horizontal)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct InsetsDirectional {
pub top: f32,
pub end: f32,
pub bottom: f32,
pub start: f32,
}
impl InsetsDirectional {
pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);
pub const fn new(top: f32, end: f32, bottom: f32, start: f32) -> Self {
Self {
top,
end,
bottom,
start,
}
}
pub const fn horizontal(value: f32) -> Self {
Self::new(0.0, value, 0.0, value)
}
pub const fn only_start(value: f32) -> Self {
Self::new(0.0, 0.0, 0.0, value)
}
pub const fn only_end(value: f32) -> Self {
Self::new(0.0, value, 0.0, 0.0)
}
pub fn resolve(self, direction: TextDirection) -> Insets {
let (left, right) = if direction.is_rtl() {
(self.end, self.start)
} else {
(self.start, self.end)
};
Insets::new(self.top, right, self.bottom, left)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum InsetsGeometry {
Physical(Insets),
Directional(InsetsDirectional),
}
impl InsetsGeometry {
pub fn resolve(self, direction: TextDirection) -> Insets {
match self {
InsetsGeometry::Physical(insets) => insets,
InsetsGeometry::Directional(insets) => insets.resolve(direction),
}
}
pub fn laid_out(self, direction: TextDirection) -> Insets {
match self {
InsetsGeometry::Directional(insets) => insets.resolve(TextDirection::Ltr),
InsetsGeometry::Physical(insets) if direction.is_rtl() => Insets {
left: insets.right,
right: insets.left,
..insets
},
InsetsGeometry::Physical(insets) => insets,
}
}
}
impl From<Insets> for InsetsGeometry {
fn from(insets: Insets) -> Self {
InsetsGeometry::Physical(insets)
}
}
impl From<InsetsDirectional> for InsetsGeometry {
fn from(insets: InsetsDirectional) -> Self {
InsetsGeometry::Directional(insets)
}
}
impl From<f32> for InsetsGeometry {
fn from(value: f32) -> Self {
InsetsGeometry::Physical(Insets::uniform(value))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Size {
pub width: f32,
pub height: f32,
}
impl Size {
pub const ZERO: Self = Self::new(0.0, 0.0);
pub const fn new(width: f32, height: f32) -> Self {
Self { width, height }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct Alignment {
pub x: f32,
pub y: f32,
}
impl Alignment {
pub const TOP_LEFT: Self = Self::new(-1.0, -1.0);
pub const TOP_CENTER: Self = Self::new(0.0, -1.0);
pub const TOP_RIGHT: Self = Self::new(1.0, -1.0);
pub const CENTER_LEFT: Self = Self::new(-1.0, 0.0);
pub const CENTER: Self = Self::new(0.0, 0.0);
pub const CENTER_RIGHT: Self = Self::new(1.0, 0.0);
pub const BOTTOM_LEFT: Self = Self::new(-1.0, 1.0);
pub const BOTTOM_CENTER: Self = Self::new(0.0, 1.0);
pub const BOTTOM_RIGHT: Self = Self::new(1.0, 1.0);
pub const fn new(x: f32, y: f32) -> Self {
Self { x, y }
}
pub fn fraction_x(self) -> f32 {
((self.x + 1.0) * 0.5).clamp(0.0, 1.0)
}
pub fn fraction_y(self) -> f32 {
((self.y + 1.0) * 0.5).clamp(0.0, 1.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct AlignmentDirectional {
pub x_start: f32,
pub y: f32,
}
impl AlignmentDirectional {
pub const TOP_START: Self = Self::new(-1.0, -1.0);
pub const TOP_CENTER: Self = Self::new(0.0, -1.0);
pub const TOP_END: Self = Self::new(1.0, -1.0);
pub const CENTER_START: Self = Self::new(-1.0, 0.0);
pub const CENTER: Self = Self::new(0.0, 0.0);
pub const CENTER_END: Self = Self::new(1.0, 0.0);
pub const BOTTOM_START: Self = Self::new(-1.0, 1.0);
pub const BOTTOM_CENTER: Self = Self::new(0.0, 1.0);
pub const BOTTOM_END: Self = Self::new(1.0, 1.0);
pub const fn new(x_start: f32, y: f32) -> Self {
Self { x_start, y }
}
pub fn resolve(self, direction: TextDirection) -> Alignment {
let x = if direction.is_rtl() {
-self.x_start
} else {
self.x_start
};
Alignment::new(x, self.y)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum AlignmentGeometry {
Physical(Alignment),
Directional(AlignmentDirectional),
}
impl AlignmentGeometry {
pub fn resolve(self, direction: TextDirection) -> Alignment {
match self {
AlignmentGeometry::Physical(a) => a,
AlignmentGeometry::Directional(d) => d.resolve(direction),
}
}
}
impl AlignmentGeometry {
pub fn fractions(self) -> (f32, f32) {
match self {
AlignmentGeometry::Physical(a) => (a.x, a.y),
AlignmentGeometry::Directional(d) => (d.x_start, d.y),
}
}
pub fn with_fractions(self, x: f32, y: f32) -> Self {
match self {
AlignmentGeometry::Physical(_) => AlignmentGeometry::Physical(Alignment::new(x, y)),
AlignmentGeometry::Directional(_) => {
AlignmentGeometry::Directional(AlignmentDirectional::new(x, y))
}
}
}
}
impl From<Alignment> for AlignmentGeometry {
fn from(a: Alignment) -> Self {
AlignmentGeometry::Physical(a)
}
}
impl From<AlignmentDirectional> for AlignmentGeometry {
fn from(d: AlignmentDirectional) -> Self {
AlignmentGeometry::Directional(d)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WindowInsets {
pub padding: Insets,
pub view_insets: Insets,
pub view_padding: Insets,
}
impl WindowInsets {
pub const ZERO: Self = Self {
padding: Insets::ZERO,
view_insets: Insets::ZERO,
view_padding: Insets::ZERO,
};
pub const fn bars(intrusions: Insets) -> Self {
Self {
padding: intrusions,
view_insets: Insets::ZERO,
view_padding: intrusions,
}
}
pub fn safe(&self) -> Insets {
Insets::new(
self.padding.top.max(self.view_insets.top),
self.padding.right.max(self.view_insets.right),
self.padding.bottom.max(self.view_insets.bottom),
self.padding.left.max(self.view_insets.left),
)
}
pub fn from_baseline(baseline: Insets, current: Insets) -> WindowInsets {
let keyboard = (current.bottom - baseline.bottom).max(0.0);
let view_padding = Insets::new(
current.top,
current.right,
current.bottom - keyboard,
current.left,
);
let view_insets = Insets::new(
0.0,
0.0,
if keyboard > 0.0 { current.bottom } else { 0.0 },
0.0,
);
WindowInsets {
padding: subtract(view_padding, view_insets),
view_insets,
view_padding,
}
}
}
fn subtract(a: Insets, b: Insets) -> Insets {
Insets::new(
(a.top - b.top).max(0.0),
(a.right - b.right).max(0.0),
(a.bottom - b.bottom).max(0.0),
(a.left - b.left).max(0.0),
)
}
impl Default for WindowInsets {
fn default() -> Self {
Self::ZERO
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Rect {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
impl Rect {
pub const UNBOUNDED: Rect = Rect::new(-1.0e7, -1.0e7, 2.0e7, 2.0e7);
pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub fn translate(self, dx: f32, dy: f32) -> Self {
Self::new(self.x + dx, self.y + dy, self.width, self.height)
}
pub fn scale(self, factor: f32) -> Self {
Self::new(
self.x * factor,
self.y * factor,
self.width * factor,
self.height * factor,
)
}
pub fn scale_about(self, pivot: Point, factor: f32) -> Self {
self.scale_about_xy(pivot, factor, factor)
}
pub fn scale_about_xy(self, pivot: Point, sx: f32, sy: f32) -> Self {
Self::new(
pivot.x + (self.x - pivot.x) * sx,
pivot.y + (self.y - pivot.y) * sy,
self.width * sx,
self.height * sy,
)
}
pub fn scale_xy(self, sx: f32, sy: f32) -> Self {
Self::new(self.x * sx, self.y * sy, self.width * sx, self.height * sy)
}
pub fn intersect(self, other: Rect) -> Self {
let x0 = self.x.max(other.x);
let y0 = self.y.max(other.y);
let x1 = (self.x + self.width).min(other.x + other.width);
let y1 = (self.y + self.height).min(other.y + other.height);
Self::new(x0, y0, (x1 - x0).max(0.0), (y1 - y0).max(0.0))
}
pub fn union(self, other: Rect) -> Self {
let x0 = self.x.min(other.x);
let y0 = self.y.min(other.y);
let x1 = (self.x + self.width).max(other.x + other.width);
let y1 = (self.y + self.height).max(other.y + other.height);
Self::new(x0, y0, x1 - x0, y1 - y0)
}
pub fn contains(self, point: Point) -> bool {
point.x >= self.x
&& point.x < self.x + self.width
&& point.y >= self.y
&& point.y < self.y + self.height
}
pub const fn from_point_size(origin: Point, size: Size) -> Self {
Self::new(origin.x, origin.y, size.width, size.height)
}
pub const fn origin(self) -> Point {
Point::new(self.x, self.y)
}
pub const fn size(self) -> Size {
Size::new(self.width, self.height)
}
pub const fn to_array(self) -> [f32; 4] {
[self.x, self.y, self.width, self.height]
}
}
pub fn fits(extent: f32) -> f32 {
extent.ceil()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn affine_composes_scale_then_rotate_about_a_pivot() {
use std::f32::consts::FRAC_PI_2;
let pivot = Point::new(10.0, 10.0);
let m = Affine::scale(2.0, 2.0)
.about(pivot)
.then(Affine::rotation(FRAC_PI_2).about(pivot));
let c = m.apply(pivot);
assert!(
(c.x - 10.0).abs() < 1e-3 && (c.y - 10.0).abs() < 1e-3,
"pivot fixe : {c:?}"
);
assert!(m.m[0].abs() < 1e-3 && (m.m[1] - 2.0).abs() < 1e-3);
assert!((m.m[2] + 2.0).abs() < 1e-3 && m.m[3].abs() < 1e-3);
}
#[test]
fn affine_inverse_round_trips() {
use std::f32::consts::FRAC_PI_3;
let m = Affine::scale(1.5, 0.5)
.about(Point::new(4.0, 7.0))
.then(Affine::rotation(FRAC_PI_3).about(Point::new(2.0, 3.0)))
.then(Affine::translation(5.0, -2.0));
let inv = m.inverse();
for p in [
Point::new(0.0, 0.0),
Point::new(12.0, -3.0),
Point::new(-5.0, 8.0),
] {
let back = inv.apply(m.apply(p));
assert!(
(back.x - p.x).abs() < 1e-2 && (back.y - p.y).abs() < 1e-2,
"{p:?} -> {back:?}"
);
}
}
#[test]
fn directional_alignment_resolves_by_direction() {
let start = AlignmentDirectional::CENTER_START;
assert_eq!(start.resolve(TextDirection::Ltr), Alignment::CENTER_LEFT);
assert_eq!(start.resolve(TextDirection::Rtl), Alignment::CENTER_RIGHT);
let top = AlignmentDirectional::TOP_CENTER;
assert_eq!(top.resolve(TextDirection::Rtl), Alignment::TOP_CENTER);
}
#[test]
fn alignment_geometry_unifies_physical_and_directional() {
let phys: AlignmentGeometry = Alignment::CENTER_LEFT.into();
assert_eq!(phys.resolve(TextDirection::Rtl), Alignment::CENTER_LEFT);
let dir: AlignmentGeometry = AlignmentDirectional::CENTER_START.into();
assert_eq!(dir.resolve(TextDirection::Ltr), Alignment::CENTER_LEFT);
assert_eq!(dir.resolve(TextDirection::Rtl), Alignment::CENTER_RIGHT);
}
#[test]
fn directional_insets_flip_start_end() {
let d = InsetsDirectional::new(1.0, 4.0, 2.0, 10.0);
let ltr = d.resolve(TextDirection::Ltr);
assert_eq!(
(ltr.left, ltr.right, ltr.top, ltr.bottom),
(10.0, 4.0, 1.0, 2.0)
);
let rtl = d.resolve(TextDirection::Rtl);
assert_eq!(
(rtl.left, rtl.right),
(4.0, 10.0),
"start moves to the right under RTL"
);
assert_eq!((rtl.top, rtl.bottom), (1.0, 2.0));
}
#[test]
fn window_insets_split_and_safe_area() {
let baseline = Insets::new(84.0, 0.0, 45.0, 0.0);
let closed = WindowInsets::from_baseline(baseline, baseline);
assert_eq!(closed.padding, baseline);
assert_eq!(closed.view_padding, baseline);
assert_eq!(closed.view_insets, Insets::ZERO);
assert_eq!(closed.safe(), baseline);
let open = WindowInsets::from_baseline(baseline, Insets::new(84.0, 0.0, 345.0, 0.0));
assert_eq!(open.view_insets, Insets::new(0.0, 0.0, 345.0, 0.0));
assert_eq!(open.view_padding, baseline);
assert_eq!(open.padding, Insets::new(84.0, 0.0, 0.0, 0.0));
assert_eq!(open.safe(), Insets::new(84.0, 0.0, 345.0, 0.0));
let hidden = WindowInsets::from_baseline(baseline, Insets::new(84.0, 0.0, 10.0, 0.0));
assert_eq!(hidden.view_insets, Insets::ZERO);
assert_eq!(hidden.padding.bottom, 10.0);
assert_eq!(hidden.view_padding.bottom, 10.0);
}
#[test]
fn rect_from_point_size_roundtrips() {
let r = Rect::from_point_size(Point::new(3.0, 4.0), Size::new(10.0, 20.0));
assert_eq!(r, Rect::new(3.0, 4.0, 10.0, 20.0));
assert_eq!(r.origin(), Point::new(3.0, 4.0));
assert_eq!(r.size(), Size::new(10.0, 20.0));
assert_eq!(r.to_array(), [3.0, 4.0, 10.0, 20.0]);
}
}