rae 0.1.10

Renderer-neutral widget, layout, sanitization, and GLSL shader primitives for Rust desktop tools.
Documentation
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Rect {
    pub x: f32,
    pub y: f32,
    pub width: f32,
    pub height: f32,
}

impl Rect {
    pub const ZERO: Self = Self::new(0.0, 0.0, 0.0, 0.0);

    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
        Self {
            x,
            y,
            width,
            height,
        }
    }

    pub fn right(self) -> f32 {
        self.x + self.width
    }

    pub fn bottom(self) -> f32 {
        self.y + self.height
    }

    pub fn is_empty(self) -> bool {
        self.width <= 0.0 || self.height <= 0.0
    }

    pub fn inset(self, inset: Insets) -> Self {
        let x = self.x + inset.left;
        let y = self.y + inset.top;
        let width = (self.width - inset.left - inset.right).max(0.0);
        let height = (self.height - inset.top - inset.bottom).max(0.0);
        Self::new(x, y, width, height)
    }

    pub fn contains(self, x: f32, y: f32) -> bool {
        x >= self.x && x <= self.right() && y >= self.y && y <= self.bottom()
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Insets {
    pub top: f32,
    pub right: f32,
    pub bottom: f32,
    pub left: f32,
}

impl Insets {
    pub const fn all(value: f32) -> Self {
        Self {
            top: value,
            right: value,
            bottom: value,
            left: value,
        }
    }

    pub const fn symmetric(horizontal: f32, vertical: f32) -> Self {
        Self {
            top: vertical,
            right: horizontal,
            bottom: vertical,
            left: horizontal,
        }
    }
}

#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct AppLayout {
    pub root: Rect,
    pub header: Rect,
    pub main: Rect,
    pub sidebar: Rect,
    pub prompt: Rect,
    pub status: Rect,
    pub sidebar_collapsed: bool,
}

pub fn compute_shell_layout(viewport: Rect) -> AppLayout {
    let outer_gap = 16.0;
    let inner_gap = 12.0;
    let status_height = 32.0;
    let prompt_height = if viewport.height < 680.0 { 92.0 } else { 126.0 };
    let header_height = if viewport.height < 620.0 { 0.0 } else { 58.0 };
    let sidebar_collapsed = viewport.width < 980.0;
    let sidebar_width = if sidebar_collapsed {
        0.0
    } else {
        (viewport.width * 0.27).clamp(300.0, 440.0)
    };

    let content = viewport.inset(Insets::all(outer_gap));
    let header = Rect::new(content.x, content.y, content.width, header_height);
    let status = Rect::new(
        content.x,
        content.bottom() - status_height,
        content.width,
        status_height,
    );
    let prompt = Rect::new(
        content.x,
        status.y - inner_gap - prompt_height,
        content.width,
        prompt_height,
    );
    let body_y = header.y + header.height + if header_height > 0.0 { inner_gap } else { 0.0 };
    let body_h = (prompt.y - inner_gap - body_y).max(0.0);
    let sidebar = if sidebar_collapsed {
        Rect::ZERO
    } else {
        Rect::new(
            content.right() - sidebar_width,
            body_y,
            sidebar_width,
            body_h,
        )
    };
    let main_width = if sidebar_collapsed {
        content.width
    } else {
        (content.width - sidebar_width - inner_gap).max(0.0)
    };
    let main = Rect::new(content.x, body_y, main_width, body_h);

    AppLayout {
        root: viewport,
        header,
        main,
        sidebar,
        prompt,
        status,
        sidebar_collapsed,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn layout_collapses_sidebar_on_small_width() {
        let layout = compute_shell_layout(Rect::new(0.0, 0.0, 800.0, 700.0));

        assert!(layout.sidebar_collapsed);
        assert!(layout.sidebar.is_empty());
        assert!(layout.main.width > 700.0);
    }

    #[test]
    fn layout_keeps_prompt_and_status_visible() {
        let layout = compute_shell_layout(Rect::new(0.0, 0.0, 1440.0, 900.0));

        assert!(layout.main.height > 500.0);
        assert!(layout.prompt.y > layout.main.y);
        assert!(layout.status.y > layout.prompt.y);
    }
}