codecraft 0.1.1

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, an immediate-mode UI, audio and gamepad haptics
Documentation
//! Opening, placing and pointing the submenus.
//!
//! Split out of [`super::systems`] because placing one is most of what a menu
//! does and all of what it gets wrong: where a submenu goes decides whether it
//! covers something, and anything it covers is still under the pointer.
use std::collections::HashMap;

use bevy_ecs::prelude::*;

use super::geometry::{ButtonIcons, Icon, Position, Size};
use super::icons::{ours, path};
use super::layout::{self, Expand};
use super::menu::Submenu;
use super::panel::PanelChild;
use super::rect::Rect;
use super::resources::{CursorPosition, ScreenSize};
use super::visibility::Visibility;

/// One row that opens a menu, and everything needed to decide whether it is
/// the one showing and where to put what it opens.
struct Row {
    /// The row itself, and the panel it is a row of.
    entity: Entity,
    panel: Entity,
    rect: Rect,
    /// Where its submenu goes, laid out, and which way it went to get there.
    opens: Rect,
    rows: Vec<Rect>,
    side: Expand,
    /// The entities it owns, copied out so the placing pass does not need the
    /// query the gathering pass is holding.
    panel_entity: Entity,
    buttons: Vec<Entity>,
    pointer: Entity,
    /// Whether its submenu is up as things stand.
    showing: bool,
}

/// Opens each submenu, places it beside the panel it came from, points a
/// triangle back at the row that opened it, and puts a caret on that row
/// saying which way it went.
///
/// **One at a time.** A panel's rows all open their menus into the same strip
/// of screen beside it, so those menus overlap each other; if each decided for
/// itself whether the pointer was inside it, every one of them would say yes
/// and they would all be drawn on top of one another. So the panel picks: the
/// row under the pointer if there is one, otherwise the row whose menu the
/// pointer has moved into, and nothing else.
///
/// That second case is what lets the pointer travel from a row into what it
/// opened without the menu closing under it, which is the one movement
/// anybody makes.
///
/// Everything is placed here rather than at spawn, every frame, from wherever
/// the parent ended up. That is a handful of rects a frame and it means a
/// resize, a relayout, or a parent that moved for any other reason all keep
/// their submenus attached with nothing extra to remember.
pub fn update_submenu_system(
    screen: Res<ScreenSize>,
    cursor: Res<CursorPosition>,
    parents: Query<(Entity, &Position, &Size, &Submenu, Option<&PanelChild>)>,
    mut rects: Query<(&mut Position, &mut Size), Without<Submenu>>,
    mut visibilities: Query<&mut Visibility>,
    mut icons: Query<&mut Icon>,
    mut carets: Query<&mut ButtonIcons>,
) {
    // Worked out first and all at once: deciding which row wins needs to see
    // the others, and placing anything needs the query these reads hold.
    let mut rows = Vec::new();
    for (entity, row_pos, row_size, submenu, child_of) in &parents {
        let rect = row_pos.rect(row_size);
        // Against the panel the row sits in, not against the row: see
        // `layout::submenu_rect`.
        let panel = child_of.map(|child| child.0).unwrap_or(entity);
        let parent_panel = rects
            .get(panel)
            .map(|(pos, size)| pos.rect(size))
            .unwrap_or(rect);

        let (opens, opened_rows) = layout::submenu_rect(
            parent_panel,
            rect,
            submenu.buttons.len(),
            submenu.metrics,
            submenu.expand,
            submenu.preferred,
            screen.width,
            screen.height,
        );
        // Which side it actually landed on, read back from where it went
        // rather than from what it asked for: `submenu_rect` flips when there
        // is no room, and both the caret and the triangle have to say what
        // happened rather than what was wanted.
        let side = match opens.center_x() < parent_panel.center_x() {
            true => Expand::Left,
            false => Expand::Right,
        };
        let showing = visibilities
            .get(submenu.panel)
            .map(|visibility| visibility.0)
            .unwrap_or(false);

        rows.push(Row {
            entity,
            panel,
            rect,
            opens,
            rows: opened_rows,
            side,
            panel_entity: submenu.panel,
            buttons: submenu.buttons.clone(),
            pointer: submenu.pointer,
            showing,
        });
    }

    // One row per panel gets to be open. Hovering a row wins outright, so
    // moving along the parent menu swaps which submenu is up; failing that,
    // the row whose menu the pointer is already inside keeps it.
    let mut open: HashMap<Entity, Entity> = HashMap::new();
    for row in &rows {
        if row.rect.contains(cursor.x, cursor.y) {
            open.insert(row.panel, row.entity);
        }
    }
    for row in &rows {
        // The whole panel, not each of its rows: the padding around them is
        // inside the menu, and a pointer crossing it has not left.
        if row.showing && row.opens.contains(cursor.x, cursor.y) {
            open.entry(row.panel).or_insert(row.entity);
        }
    }

    for row in &rows {
        let (pointer_rect, pointer_icon) = pointer_for(row.opens, row.rect, row.side);
        let mut place = |entity: Entity, rect: Rect| {
            if let Ok((mut pos, mut size)) = rects.get_mut(entity) {
                (pos.x, pos.y) = (rect.x, rect.y);
                (size.width, size.height) = (rect.width, rect.height);
            }
        };
        place(row.panel_entity, row.opens);
        for (&button, &rect) in row.buttons.iter().zip(&row.rows) {
            place(button, rect);
        }
        place(row.pointer, pointer_rect);

        if let Ok(mut icon) = icons.get_mut(row.pointer) {
            icon.path = pointer_icon;
        }
        if let Ok(mut wears) = carets.get_mut(row.entity) {
            wears.trailing = Some(caret_for(row.side));
        }

        let showing = open.get(&row.panel) == Some(&row.entity);
        for entity in [row.panel_entity, row.pointer]
            .into_iter()
            .chain(row.buttons.iter().copied())
        {
            if let Ok(mut visibility) = visibilities.get_mut(entity) {
                *visibility = Visibility(showing);
            }
        }
    }
}

/// Where the triangle goes and which way it faces: on the submenu's edge
/// nearest its parent, level with the row it came out of.
///
/// It sits *outside* the panel rather than on it, so it reads as a tail on the
/// panel rather than as a mark inside it, and so it cannot cover the first row.
fn pointer_for(panel: Rect, row: Rect, opened: Expand) -> (Rect, &'static str) {
    let size = layout::POINTER_SIZE;
    // Level with the row, but never off the end of the panel it hangs from.
    let y = (row.y + row.height * 0.5 - size * 0.5)
        .clamp(panel.y, (panel.y + panel.height - size).max(panel.y));
    match opened {
        // The panel is left of its parent, so the tail is on the panel's right
        // edge, pointing back the way it came.
        Expand::Left => (
            Rect::new(panel.x + panel.width, y, size, size),
            ours::POINTER_RIGHT,
        ),
        _ => (Rect::new(panel.x - size, y, size, size), ours::POINTER_LEFT),
    }
}

/// The caret a row wears to say which way it opens.
pub fn caret_for(opened: Expand) -> &'static str {
    match opened {
        Expand::Left => path::CARET_LEFT,
        _ => path::CARET_RIGHT,
    }
}