beuvy-runtime 0.1.0

A low-level Bevy UI kit with reusable controls and utility-class styling.
Documentation
use crate::select::model::{Select, SelectPanel, SelectTrigger};
use bevy::prelude::*;
use bevy::ui::Val::{Auto, Px};
use bevy::window::PrimaryWindow;

pub(super) const SELECT_PANEL_GAP: f32 = 6.0;
const SELECT_PANEL_DEFAULT_MAX_HEIGHT: f32 = 360.0;
const SELECT_PANEL_MIN_HEIGHT: f32 = 48.0;

pub(crate) fn sync_select_panel_placement(
    selects: Query<&Select>,
    trigger_nodes: Query<(&ComputedNode, &UiGlobalTransform), With<SelectTrigger>>,
    mut panel_nodes: Query<(&mut Node, &ComputedNode), With<SelectPanel>>,
    parents: Query<&ChildOf>,
    ancestors: Query<(&Node, &ComputedNode, &UiGlobalTransform), Without<SelectPanel>>,
    primary_window: Query<&Window, With<PrimaryWindow>>,
) {
    let window_rect = primary_window.iter().next().map(|window| Rect {
        min: Vec2::ZERO,
        max: window.size(),
    });

    for select in &selects {
        if !select.open {
            continue;
        }

        let Ok((trigger_computed, trigger_transform)) = trigger_nodes.get(select.trigger) else {
            continue;
        };
        let Ok((mut panel_node, panel_computed)) = panel_nodes.get_mut(select.panel) else {
            continue;
        };

        let trigger_rect = node_global_rect(trigger_computed, trigger_transform);
        let clip_rect = nearest_vertical_clip_rect(select.panel, &parents, &ancestors)
            .or(window_rect)
            .unwrap_or(trigger_rect);
        let below_space = (clip_rect.max.y - trigger_rect.max.y - SELECT_PANEL_GAP).max(0.0);
        let above_space = (trigger_rect.min.y - clip_rect.min.y - SELECT_PANEL_GAP).max(0.0);
        let open_up = below_space < SELECT_PANEL_MIN_HEIGHT && above_space > below_space;
        let available_space = if open_up { above_space } else { below_space };
        let max_height = available_space
            .max(SELECT_PANEL_MIN_HEIGHT)
            .min(SELECT_PANEL_DEFAULT_MAX_HEIGHT);
        let panel_height = panel_computed.size().y * panel_computed.inverse_scale_factor();
        let panel_width = panel_computed.size().x * panel_computed.inverse_scale_factor();
        let trigger_width = trigger_computed.size().x * trigger_computed.inverse_scale_factor();

        let left = trigger_rect.min.x - clip_rect.min.x;
        let top = if open_up {
            trigger_rect.min.y - clip_rect.min.y - panel_height - SELECT_PANEL_GAP
        } else {
            trigger_rect.max.y - clip_rect.min.y + SELECT_PANEL_GAP
        };
        let max_left = (clip_rect.width() - panel_width).max(0.0);
        let max_top = (clip_rect.height() - panel_height).max(0.0);
        let clamped_left = left.clamp(0.0, max_left);
        let clamped_top = top.clamp(0.0, max_top);

        panel_node.left = Px(clamped_left);
        panel_node.right = Auto;
        panel_node.top = Px(clamped_top);
        panel_node.bottom = Auto;
        panel_node.min_width = Px(trigger_width);
        panel_node.margin.top = Px(0.0);
        panel_node.margin.bottom = Px(0.0);
        panel_node.max_height = Px(max_height);
        panel_node.overflow = Overflow::scroll_y();
    }
}

fn nearest_vertical_clip_rect(
    mut entity: Entity,
    parents: &Query<&ChildOf>,
    ancestors: &Query<(&Node, &ComputedNode, &UiGlobalTransform), Without<SelectPanel>>,
) -> Option<Rect> {
    while let Ok(parent) = parents.get(entity) {
        entity = parent.parent();
        let Ok((node, computed, transform)) = ancestors.get(entity) else {
            continue;
        };
        if !node.overflow.y.is_visible() {
            return Some(node_global_rect(computed, transform));
        }
    }
    None
}

fn node_global_rect(computed: &ComputedNode, transform: &UiGlobalTransform) -> Rect {
    let (_, _, center) = transform.to_scale_angle_translation();
    let physical = Rect::from_center_size(center, computed.size());
    let inverse = computed.inverse_scale_factor();
    Rect {
        min: physical.min * inverse,
        max: physical.max * inverse,
    }
}