nightshade-editor 0.14.2

Interactive map editor for the Nightshade game engine
//! Even-grid layout for camera tiles.
//!
//! The previous implementation kept calling `ui_tile_split` against the
//! root over and over, which builds a lopsided binary tree (each new tile
//! halves whatever was on the left, while the rightmost tile keeps
//! shrinking). The result for 9 cameras was the asymmetric layout shown
//! in the editor screenshots.
//!
//! `rebuild_grid` instead builds a near-square grid: `cols = ceil(sqrt(N))`
//! columns, `rows = ceil(N / cols)` rows per column, with the last column
//! truncated when needed. Each split goes against the *currently
//! remaining* pane (left for column splits, top for row splits), with the
//! ratio chosen so every column ends up exactly `1/cols` of the total
//! width and every cell within a column ends up `1/cells_in_col` tall.
//! The main viewport pane is always the cell at column 0 row 0; its
//! `TileId` changes as splits cascade through it, so we return the new
//! id for the caller to update `ViewportHandles::viewport_pane`.

use nightshade::ecs::ui::components::TileNode;
use nightshade::prelude::*;

pub struct GridLayout {
    pub main_pane: TileId,
    pub extras: Vec<(TileId, Entity)>,
}

pub fn rebuild_grid(
    world: &mut World,
    container: Entity,
    main_pane: TileId,
    main_content: Entity,
    extra_titles: &[String],
) -> Option<GridLayout> {
    let total = 1 + extra_titles.len();
    if total <= 1 {
        let _ = main_content;
        return Some(GridLayout {
            main_pane,
            extras: Vec::new(),
        });
    }

    let cols = (total as f32).sqrt().ceil() as usize;
    let rows = total.div_ceil(cols);

    let mut col_heads: Vec<(TileId, Entity)> = Vec::with_capacity(cols);
    let mut remaining_pane = main_pane;
    let mut remaining_content = main_content;
    for i in 0..cols.saturating_sub(1) {
        let ratio = 1.0 / (cols - i) as f32;
        let (old_id, new_id, new_content) = split_pane(
            world,
            container,
            remaining_pane,
            SplitDirection::Horizontal,
            ratio,
            "",
        )?;
        col_heads.push((old_id, remaining_content));
        remaining_pane = new_id;
        remaining_content = new_content;
    }
    col_heads.push((remaining_pane, remaining_content));

    let mut all_cells: Vec<(TileId, Entity)> = Vec::with_capacity(total);
    let mut placed = 0;
    for (col_i, (head_pane, head_content)) in col_heads.into_iter().enumerate() {
        let cells_in_col = std::cmp::min(rows, total - col_i * rows);
        if cells_in_col == 0 {
            continue;
        }
        let mut col_cells: Vec<(TileId, Entity)> = Vec::with_capacity(cells_in_col);
        col_cells.push((head_pane, head_content));

        let mut top_remaining_pane = head_pane;
        let mut top_remaining_content = head_content;
        for row_i in 0..cells_in_col.saturating_sub(1) {
            let ratio = 1.0 / (cells_in_col - row_i) as f32;
            let (old_id, new_id, new_content) = split_pane(
                world,
                container,
                top_remaining_pane,
                SplitDirection::Vertical,
                ratio,
                "",
            )?;
            col_cells.last_mut().unwrap().0 = old_id;
            col_cells.push((new_id, new_content));
            top_remaining_pane = new_id;
            top_remaining_content = new_content;
        }
        let _ = top_remaining_content;

        all_cells.extend(col_cells);
        placed += cells_in_col;
    }
    debug_assert_eq!(placed, total);

    let new_main_pane = all_cells[0].0;

    for (pane_id, _) in &all_cells {
        ui_tile_wrap_pane_in_tabs(world, container, *pane_id);
    }

    let extras: Vec<(TileId, Entity)> = all_cells.into_iter().skip(1).collect();

    for (index, (pane_id, _)) in extras.iter().enumerate() {
        if let Some(title) = extra_titles.get(index)
            && !title.is_empty()
        {
            set_pane_title(world, container, *pane_id, title);
        }
    }

    Some(GridLayout {
        main_pane: new_main_pane,
        extras,
    })
}

fn split_pane(
    world: &mut World,
    container: Entity,
    target: TileId,
    direction: SplitDirection,
    ratio: f32,
    title: &str,
) -> Option<(TileId, TileId, Entity)> {
    let (new_pane_id, new_content) =
        ui_tile_split(world, container, target, direction, ratio, title)?;
    let data = world.ui.get_ui_tile_container(container)?;
    let TileNode::Split { children, .. } = data.get(target)? else {
        return None;
    };
    Some((children[0], new_pane_id, new_content))
}

fn set_pane_title(world: &mut World, container: Entity, pane_id: TileId, title: &str) {
    if let Some(data) = world.ui.get_ui_tile_container_mut(container)
        && let Some(TileNode::Pane { title: t, .. }) = data.get_mut(pane_id)
    {
        *t = title.to_string();
    }
}