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.
//! `Div`: contenedor flexbox, la base de toda interfaz en Aura.

use crate::core::{Bounds, HitboxId, Pixels, Point};
use crate::view::accessibility::AccessNode;
use crate::view::any_element::AnyElement;
use crate::view::context::ElementContext;
use crate::view::dispatch::DispatchNode;
use crate::view::element::{Element, ParentElement};
use crate::view::interactivity::{HandlerEntry, Interactivity, SharedScroll};
use crate::view::style::{Overflow, Style};
use crate::view::styled::Styled;
use smallvec::SmallVec;
use std::borrow::Cow;
use taffy::NodeId;

pub struct Div {
    pub(crate) style: Style,
    pub(crate) interactivity: Interactivity,
    children: SmallVec<[AnyElement; 4]>,
    node: Option<NodeId>,
    hitbox: Option<HitboxId>,
    scroll: Option<SharedScroll>,
}

pub fn div() -> Div {
    Div {
        style: Style::default(),
        interactivity: Interactivity::default(),
        children: SmallVec::new(),
        node: None,
        hitbox: None,
        scroll: None,
    }
}

impl Div {
    fn clips_children(&self) -> bool {
        matches!(self.style.overflow_x, Some(Overflow::Hidden | Overflow::Scroll))
            || matches!(self.style.overflow_y, Some(Overflow::Hidden | Overflow::Scroll))
    }

    /// Estilo base más los refinamientos de foco, hover y active que correspondan.
    /// Si ninguno aplica (el caso normal), devuelve `Cow::Borrowed(&self.style)` sin clonar nada.
    fn resolved_style(&self, cx: &ElementContext) -> Cow<'_, Style> {
        let mut resolved = Cow::Borrowed(&self.style);
        let focused = self.interactivity.focus.is_some_and(|handle| cx.is_focused(handle.id));
        if let Some(focus) = self.interactivity.focus_style.as_ref().filter(|_| focused) {
            resolved.to_mut().refine(focus);
        }
        if let Some((_, hover)) =
            self.interactivity.group_hover.as_ref().filter(|(group, _)| cx.is_group_hovered(group))
        {
            resolved.to_mut().refine(hover);
        }
        let Some(hitbox) = self.hitbox else { return resolved };
        if let Some(hover) = self.interactivity.hover_style.as_ref().filter(|_| cx.is_hovered(hitbox)) {
            resolved.to_mut().refine(hover);
        }
        if let Some(active) = self.interactivity.active_style.as_ref().filter(|_| cx.is_pressed(hitbox)) {
            resolved.to_mut().refine(active);
        }
        if let Some(dragged) = cx.active_drag_type().filter(|_| cx.is_hovered(hitbox)) {
            for (_, style) in self.interactivity.drag_over.iter().filter(|(type_id, _)| *type_id == dragged) {
                resolved.to_mut().refine(style);
            }
        }
        resolved
    }

    /// Matriz de `transform-*` sobre `bounds`, si el estilo trae alguna.
    fn transform_of(style: &Style, bounds: Bounds) -> Option<crate::gpu::Affine> {
        let spec = &style.extra.as_deref()?.transform;
        (!spec.is_none()).then(|| spec.affine(bounds))
    }

    fn content_origin(&self, bounds: Bounds) -> Point {
        let offset = self.scroll.as_ref().map_or(0.0, |scroll| scroll.get().offset_y);
        bounds.origin - Point::new(Pixels::ZERO, Pixels(offset))
    }

    fn dispatch_node(&self) -> DispatchNode {
        DispatchNode {
            parent: None,
            focus: self.interactivity.focus.map(|handle| handle.id),
            tab_index: self.interactivity.tab_index,
            context: self.interactivity.key_context.clone(),
            actions: self.interactivity.actions.clone(),
            key_down: self.interactivity.on_key_down.clone(),
            text_input: self.interactivity.on_text_input.clone(),
        }
    }

    /// Recorre los hijos con el origen desplazado por el scroll y, si corresponde, recortados.
    fn visit_children(
        &mut self,
        cx: &mut ElementContext,
        style_override: Option<&Style>,
        bounds: Bounds,
        visit: fn(&mut AnyElement, &mut ElementContext),
    ) {
        let origin = self.content_origin(bounds);
        let clips = self.clips_children();
        let Self { style, children, .. } = self;
        let active_style = style_override.unwrap_or(style);
        cx.with_text_style(active_style, |cx| {
            let mut body = |cx: &mut ElementContext| cx.with_origin(origin, |cx| visit_in_z_order(children, cx, visit));
            if clips { cx.with_mask(bounds, body) } else { body(cx) }
        });
    }
}

/// Hijos en su orden, o por `z-index` (estable) si alguno lo fija: lo mayor se pinta, y recibe el ratón, después.
fn visit_in_z_order(
    children: &mut [AnyElement],
    cx: &mut ElementContext,
    visit: fn(&mut AnyElement, &mut ElementContext),
) {
    if children.iter_mut().all(|child| child.z_index() == 0) {
        return children.iter_mut().for_each(|child| visit(child, cx));
    }
    let mut order: SmallVec<[(i16, usize); 16]> =
        children.iter_mut().enumerate().map(|(index, child)| (child.z_index(), index)).collect();
    order.sort_by_key(|&(z, _)| z);
    for (_, index) in order {
        visit(&mut children[index], cx);
    }
}

impl Styled for Div {
    fn style(&mut self) -> &mut Style {
        &mut self.style
    }
}

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

impl Element for Div {
    fn z_index(&self) -> i16 {
        self.style.extra.as_deref().and_then(|extra| extra.z).unwrap_or(0)
    }

    fn request_layout(&mut self, cx: &mut ElementContext) -> NodeId {
        // Los elementos se reconstruyen en cada frame: las variantes (`md:`, `dark:`) se funden una vez en el estilo base.
        for (variant, style) in std::mem::take(&mut self.interactivity.variants) {
            if variant.applies(cx) {
                self.style.refine(&style);
            }
        }
        let Self { style, children, .. } = self;
        let child_nodes: SmallVec<[NodeId; 4]> =
            cx.with_text_style(style, |cx| children.iter_mut().map(|child| child.request_layout(cx)).collect());
        let node = cx.request_node(&self.style, &child_nodes);
        self.node = Some(node);
        node
    }

    fn prepaint(&mut self, cx: &mut ElementContext) {
        let Some(node) = self.node else { return };
        let bounds = cx.node_bounds(node);
        let transform = Self::transform_of(&self.style, bounds);
        if let Some(transform) = transform {
            cx.push_hit_transform(transform);
        }
        self.prepaint_inside(cx, node, bounds);
        if transform.is_some() {
            cx.pop_hit_transform();
        }
    }

    fn paint(&mut self, cx: &mut ElementContext) {
        let Some(node) = self.node else { return };
        let bounds = cx.node_bounds(node);
        let resolved_owned = match self.resolved_style(cx) {
            Cow::Borrowed(_) => None,
            Cow::Owned(owned) => Some(owned),
        };
        let active_style = resolved_owned.as_ref().unwrap_or(&self.style);
        let transform = Self::transform_of(active_style, bounds);
        if let Some(transform) = transform {
            cx.frame.scene.push_transform(transform);
        }
        let filter = active_style.extra.as_deref().map(|extra| extra.filter).unwrap_or_default();
        if let Some(sigma) = filter.backdrop_blur {
            cx.paint_blur(bounds, active_style, sigma);
        }
        cx.paint_box(bounds, active_style);
        self.visit_children(cx, resolved_owned.as_ref(), bounds, |child, cx| child.paint(cx));
        let active_style = resolved_owned.as_ref().unwrap_or(&self.style);
        if let Some(scroll) = &self.scroll {
            cx.scrollbar(bounds, scroll, &[]);
        }
        if let Some(sigma) = filter.blur {
            cx.paint_blur(bounds, active_style, sigma);
        }
        if transform.is_some() {
            cx.frame.scene.pop_transform();
        }
    }
}

impl Div {
    fn prepaint_inside(&mut self, cx: &mut ElementContext, node: NodeId, bounds: Bounds) {
        if let Some(cell) = &self.interactivity.record_bounds {
            cell.set(bounds);
        }
        if let (true, Some(role)) = (cx.accessibility_enabled(), self.interactivity.role) {
            let interactivity = &self.interactivity;
            cx.register_access(AccessNode {
                role,
                label: interactivity.label.clone(),
                value: None,
                bounds,
                focus: interactivity.focus.map(|handle| handle.id),
                clickable: interactivity.on_click.is_some() || interactivity.on_mouse_down.is_some(),
            });
        }

        if let Some(id) = self.interactivity.id.clone().filter(|_| self.style.is_scrollable_y()) {
            let scroll = cx.scroll_state(&id);
            let mut state = scroll.get();
            state.viewport_y = bounds.size.height.0;
            state.max_y = (cx.node_content_size(node).height.0 - state.viewport_y).max(0.0);
            state.offset_y = state.offset_y.clamp(0.0, state.max_y);
            scroll.set(state);
            self.scroll = Some(scroll);
        }

        if self.interactivity.needs_hitbox(self.scroll.is_some()) {
            let hitbox = cx.insert_hitbox(bounds, self.interactivity.blocks_mouse);
            self.hitbox = Some(hitbox);
            let interactivity = &self.interactivity;
            cx.register_handlers(HandlerEntry {
                hitbox,
                on_click: interactivity.on_click.clone(),
                on_mouse_down: interactivity.on_mouse_down.clone(),
                on_right_click: interactivity.on_right_click.clone(),
                on_scroll: interactivity.on_scroll.clone(),
                on_drag: interactivity.on_drag.clone(),
                on_mouse_move: interactivity.on_mouse_move.clone(),
                scroll: self.scroll.clone(),
                focus: interactivity.focus,
                tooltip: interactivity.tooltip.clone(),
                cursor: interactivity.cursor,
                drag: interactivity.drag.clone(),
                drops: interactivity.drops.clone(),
            });
            if let Some(group) = &self.interactivity.group {
                cx.register_group(group.clone(), hitbox);
            }
        }

        let has_dispatch = self.interactivity.needs_dispatch_node();
        if has_dispatch {
            cx.push_dispatch_node(self.dispatch_node());
        }
        self.visit_children(cx, None, bounds, |child, cx| child.prepaint(cx));
        if has_dispatch {
            cx.pop_dispatch_node();
        }
    }
}