aura-gui 0.3.5

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Overlays anclados (menús, popovers, autocompletado) que se pintan encima de todo.
//!
//! `anchored()` no ocupa espacio en el layout: en prepaint entrega sus hijos a la
//! ventana, que los maqueta como raíces propias después del árbol principal.

use crate::core::{Bounds, Pixels, Point, Size};
use crate::view::any_element::AnyElement;
use crate::view::context::ElementContext;
use crate::view::element::{Element, ParentElement};
use crate::view::style::{Direction, Length, Style};
use crate::view::text_layout::compute_layout;
use smallvec::SmallVec;
use taffy::{AvailableSpace, NodeId};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Placement {
    /// Debajo del ancla; si no cabe, encima.
    Below,
    /// A la derecha del ancla; si no cabe, a la izquierda (submenús).
    Right,
}

pub struct DeferredOverlay {
    children: SmallVec<[AnyElement; 2]>,
    anchor: Bounds,
    placement: Placement,
    origin: Point,
    /// Zona fuera de la cual no se pinta; sin ella, la ventana entera.
    within: Option<Bounds>,
}

pub struct Anchored {
    target: Option<Bounds>,
    placement: Placement,
    within: Option<Bounds>,
    children: SmallVec<[AnyElement; 2]>,
    node: Option<NodeId>,
}

pub fn anchored() -> Anchored {
    Anchored { target: None, placement: Placement::Below, within: None, children: SmallVec::new(), node: None }
}

impl Anchored {
    /// Rectángulo absoluto junto al que se coloca; sin él se usa la posición en el layout.
    pub fn anchor_to(mut self, bounds: Bounds) -> Self {
        self.target = Some(bounds);
        self
    }

    /// Cambia el ancla después de construido (por ejemplo, al conocer la posición del cursor).
    pub fn set_anchor(&mut self, bounds: Bounds) {
        self.target = Some(bounds);
    }

    /// Mantiene el overlay dentro de `bounds` (p. ej. el panel que lo abre) en vez de la ventana: así un popup del
    /// editor no se pinta sobre el terminal ni sobre otro panel contiguo.
    pub fn set_within(&mut self, bounds: Bounds) {
        self.within = Some(bounds);
    }

    pub fn within(mut self, bounds: Bounds) -> Self {
        self.within = Some(bounds);
        self
    }

    pub fn position(self, point: Point) -> Self {
        self.anchor_to(Bounds::new(point, Size::default()))
    }

    pub fn placement(mut self, placement: Placement) -> Self {
        self.placement = placement;
        self
    }
}

impl ParentElement for Anchored {
    fn extend_children(&mut self, children: impl IntoIterator<Item = AnyElement>) {
        self.children.extend(children);
    }
}

impl Element for Anchored {
    fn request_layout(&mut self, cx: &mut ElementContext) -> NodeId {
        let style = Style {
            absolute: Some(true),
            width: Some(Length::Px(Pixels::ZERO)),
            height: Some(Length::Px(Pixels::ZERO)),
            ..Default::default()
        };
        let node = cx.request_node(&style, &[]);
        self.node = Some(node);
        node
    }

    fn prepaint(&mut self, cx: &mut ElementContext) {
        let anchor = match (self.target, self.node) {
            (Some(target), _) => target,
            (None, Some(node)) => cx.node_bounds(node),
            (None, None) => return,
        };
        let children = std::mem::take(&mut self.children);
        cx.frame.deferred.push(DeferredOverlay {
            children,
            anchor,
            placement: self.placement,
            origin: Point::default(),
            within: self.within,
        });
    }

    fn paint(&mut self, _cx: &mut ElementContext) {}
}

/// Coloca un overlay de `size` junto a `anchor` sin salirse de la ventana.
pub fn place(anchor: Bounds, placement: Placement, size: Size, viewport: Size) -> Point {
    place_in(anchor, placement, size, Bounds::new(Point::default(), viewport))
}

/// Como `place`, pero sin salirse de `area`: si no cabe debajo (o a la derecha) se voltea y, si tampoco cabe, se
/// pega al borde de la zona.
pub fn place_in(anchor: Bounds, placement: Placement, size: Size, area: Bounds) -> Point {
    let (width, height) = (size.width.0, size.height.0);
    let (left, top, right, bottom) = (area.origin.x.0, area.origin.y.0, area.right().0, area.bottom().0);
    let (mut x, mut y) = match placement {
        Placement::Below => (anchor.origin.x.0, anchor.bottom().0),
        Placement::Right => (anchor.right().0, anchor.origin.y.0),
    };
    if placement == Placement::Below && y + height > bottom && anchor.origin.y.0 - height >= top {
        y = anchor.origin.y.0 - height;
    }
    if placement == Placement::Right && x + width > right {
        x = anchor.origin.x.0 - width;
    }
    x = x.min(right - width).max(left);
    y = y.min(bottom - height).max(top);
    Point::new(Pixels(x), Pixels(y))
}

/// Maqueta y prepara los overlays pendientes (y los que estos abran a su vez).
pub(crate) fn prepaint_deferred(cx: &mut ElementContext, viewport: Size) -> Vec<DeferredOverlay> {
    let mut prepared = Vec::new();
    loop {
        let pending = std::mem::take(&mut cx.frame.deferred);
        if pending.is_empty() {
            return prepared;
        }
        for mut overlay in pending {
            let child_nodes: Vec<NodeId> = overlay.children.iter_mut().map(|child| child.request_layout(cx)).collect();
            let root_style = Style { direction: Some(Direction::Column), ..Default::default() };
            let root = cx.request_node(&root_style, &child_nodes);
            let available = taffy::Size { width: AvailableSpace::MaxContent, height: AvailableSpace::MaxContent };
            if let Err(error) = compute_layout(cx.frame, cx.text, cx.shape_cache, root, available) {
                eprintln!("falló el layout de un overlay: {error}");
                continue;
            }
            let size = cx
                .frame
                .taffy
                .layout(root)
                .map_or(Size::default(), |layout| Size::new(Pixels(layout.size.width), Pixels(layout.size.height)));
            let area = overlay.within.unwrap_or_else(|| Bounds::new(Point::default(), viewport));
            overlay.origin = place_in(overlay.anchor, overlay.placement, size, area);
            let origin = overlay.origin;
            cx.without_mask(|cx| {
                cx.with_origin(origin, |cx| overlay.children.iter_mut().for_each(|child| child.prepaint(cx)))
            });
            prepared.push(overlay);
        }
    }
}

pub(crate) fn paint_deferred(cx: &mut ElementContext, overlays: &mut [DeferredOverlay]) {
    for overlay in overlays {
        let origin = overlay.origin;
        cx.without_mask(|cx| {
            cx.with_origin(origin, |cx| overlay.children.iter_mut().for_each(|child| child.paint(cx)))
        });
    }
}

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

    fn bounds(x: f32, y: f32, width: f32, height: f32) -> Bounds {
        Bounds::new(Point::new(Pixels(x), Pixels(y)), Size::new(Pixels(width), Pixels(height)))
    }

    #[test]
    fn flips_above_when_there_is_no_room_below() {
        let viewport = Size::new(Pixels(800.0), Pixels(600.0));
        let origin = place(
            bounds(100.0, 580.0, 10.0, 20.0),
            Placement::Below,
            Size::new(Pixels(200.0), Pixels(100.0)),
            viewport,
        );
        assert_eq!(origin, Point::new(Pixels(100.0), Pixels(480.0)));
    }

    #[test]
    fn stays_inside_the_panel_that_opened_it_and_not_the_whole_window() {
        // Editor en y 0..400 y terminal debajo: un popup de 100 al pie del editor no puede entrar al terminal.
        let panel = bounds(0.0, 0.0, 800.0, 400.0);
        let size = Size::new(Pixels(200.0), Pixels(100.0));
        let flipped = place_in(bounds(100.0, 390.0, 10.0, 20.0), Placement::Below, size, panel);
        assert_eq!(flipped, Point::new(Pixels(100.0), Pixels(290.0)));
        let too_tall =
            place_in(bounds(100.0, 50.0, 10.0, 20.0), Placement::Below, Size::new(Pixels(200.0), Pixels(500.0)), panel);
        assert_eq!(too_tall.y, Pixels(0.0), "si no cabe en ningún lado, se pega al borde superior del panel");
        let beside =
            place_in(bounds(790.0, 390.0, 10.0, 20.0), Placement::Below, size, bounds(0.0, 100.0, 800.0, 300.0));
        assert!(beside.x.0 + 200.0 <= 800.0 && beside.y.0 + 100.0 <= 400.0 && beside.y.0 >= 100.0);
    }

    #[test]
    fn stays_inside_the_right_edge() {
        let viewport = Size::new(Pixels(800.0), Pixels(600.0));
        let origin =
            place(bounds(750.0, 10.0, 10.0, 20.0), Placement::Below, Size::new(Pixels(200.0), Pixels(100.0)), viewport);
        assert_eq!(origin.x, Pixels(600.0));
    }
}