nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
mod management;
mod persistence;
mod rendering;

pub use persistence::{format_window_title, load_project, save_project};

use super::behavior::TileBehavior;
use super::config::MosaicConfig;
use super::modal::Modals;
use super::project::WindowLayout;
use super::widget::{Pane, Widget};
use crate::prelude::*;

pub enum LayoutEvent {
    Switched(String),
    Created(String),
    Saved(String),
    Deleted(String),
    Reset,
    Renamed(String),
}

pub struct Mosaic<W: Widget<C, M>, C = (), M = ()> {
    tile_tree: Option<egui_tiles::Tree<Pane<W>>>,
    pub config: MosaicConfig,
    pub(crate) behavior: TileBehavior<W, C, M>,
    pub(crate) modals: Modals,
    layout_modified: bool,
    pub layout_name: String,
    pub title: String,
    viewport_texture_overrides: Option<Vec<egui::TextureId>>,
    pub window_index: Option<usize>,
    pub is_active_window: bool,
    pending_messages: Vec<M>,
    incoming_messages: std::collections::HashMap<egui_tiles::TileId, Vec<M>>,
    layouts: Vec<WindowLayout<W>>,
    active_layout_index: usize,
    editing_layout_index: Option<usize>,
    layout_name_edit_buffer: String,
}

impl<W: Widget<C, M>, C, M> Default for Mosaic<W, C, M> {
    fn default() -> Self {
        Self {
            tile_tree: None,
            config: MosaicConfig::default(),
            behavior: TileBehavior::default(),
            modals: Modals::default(),
            layout_modified: false,
            layout_name: String::new(),
            title: String::new(),
            viewport_texture_overrides: None,
            window_index: None,
            is_active_window: true,
            pending_messages: Vec::new(),
            incoming_messages: std::collections::HashMap::new(),
            layouts: Vec::new(),
            active_layout_index: 0,
            editing_layout_index: None,
            layout_name_edit_buffer: String::new(),
        }
    }
}

impl<W: Widget<C, M>, C, M> Mosaic<W, C, M> {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_tree(tree: egui_tiles::Tree<Pane<W>>) -> Self {
        Self {
            tile_tree: Some(tree),
            ..Self::default()
        }
    }

    pub fn with_panes(panes: Vec<W>) -> Self {
        let mut tiles = egui_tiles::Tiles::default();
        let pane_ids: Vec<_> = panes
            .into_iter()
            .map(|widget| tiles.insert_pane(Pane::new(widget)))
            .collect();
        let root = tiles.insert_tab_tile(pane_ids);
        let tree = egui_tiles::Tree::new("mosaic_tree", root, tiles);
        Self::with_tree(tree)
    }

    pub fn with_config(mut self, config: MosaicConfig) -> Self {
        self.config = config;
        self
    }

    pub fn with_title(mut self, title: impl Into<String>) -> Self {
        self.title = title.into();
        self
    }

    pub fn set_tree(&mut self, tree: egui_tiles::Tree<Pane<W>>) {
        self.tile_tree = Some(tree);
        self.layout_modified = true;
    }

    pub fn current_tree(&self) -> Option<&egui_tiles::Tree<Pane<W>>> {
        self.tile_tree.as_ref()
    }

    pub fn from_window_layout(layout: WindowLayout<W>) -> Self {
        Self {
            tile_tree: Some(layout.tree),
            layout_name: layout.layout_name,
            ..Self::default()
        }
    }

    pub fn layout_modified(&self) -> bool {
        self.layout_modified
    }

    pub fn take_layout_modified(&mut self) -> bool {
        let modified = self.layout_modified;
        self.layout_modified = false;
        modified
    }

    pub fn viewport_rects(&self) -> &std::collections::HashMap<egui_tiles::TileId, egui::Rect> {
        &self.behavior.viewport_rects
    }

    pub fn selected_viewport_tile(&self) -> Option<egui_tiles::TileId> {
        self.behavior.selected_viewport_tile
    }

    pub fn modals(&mut self) -> &mut Modals {
        &mut self.modals
    }

    pub fn clear_required_cameras(world: &mut World) {
        world.resources.user_interface.required_cameras.clear();
        world.resources.user_interface.required_camera_sizes.clear();
    }

    pub fn set_viewport_textures(&mut self, textures: Vec<egui::TextureId>) {
        self.viewport_texture_overrides = Some(textures);
    }

    pub fn for_each_widget(&self, mut callback: impl FnMut(&W)) {
        if let Some(tree) = &self.tile_tree {
            for (_tile_id, tile) in tree.tiles.iter() {
                if let egui_tiles::Tile::Pane(pane) = tile {
                    callback(&pane.widget);
                }
            }
        }
    }

    pub fn for_each_widget_with_id(&self, mut callback: impl FnMut(egui_tiles::TileId, &W)) {
        if let Some(tree) = &self.tile_tree {
            for (tile_id, tile) in tree.tiles.iter() {
                if let egui_tiles::Tile::Pane(pane) = tile {
                    callback(*tile_id, &pane.widget);
                }
            }
        }
    }

    pub fn for_each_widget_mut(&mut self, mut callback: impl FnMut(&mut W)) {
        if let Some(tree) = &mut self.tile_tree {
            for (_tile_id, tile) in tree.tiles.iter_mut() {
                if let egui_tiles::Tile::Pane(pane) = tile {
                    callback(&mut pane.widget);
                }
            }
        }
    }

    pub fn for_each_widget_with_id_mut(
        &mut self,
        mut callback: impl FnMut(egui_tiles::TileId, &mut W),
    ) {
        if let Some(tree) = &mut self.tile_tree {
            for (tile_id, tile) in tree.tiles.iter_mut() {
                if let egui_tiles::Tile::Pane(pane) = tile {
                    callback(*tile_id, &mut pane.widget);
                }
            }
        }
    }

    pub fn widget_count(&self) -> usize {
        self.tile_tree.as_ref().map_or(0, |tree| {
            tree.tiles
                .iter()
                .filter(|(_, tile)| matches!(tile, egui_tiles::Tile::Pane(_)))
                .count()
        })
    }

    pub fn find_widget(&self, predicate: impl Fn(&W) -> bool) -> Option<egui_tiles::TileId> {
        if let Some(tree) = &self.tile_tree {
            for (tile_id, tile) in tree.tiles.iter() {
                if let egui_tiles::Tile::Pane(pane) = tile
                    && predicate(&pane.widget)
                {
                    return Some(*tile_id);
                }
            }
        }
        None
    }

    pub fn get_widget(&self, tile_id: egui_tiles::TileId) -> Option<&W> {
        if let Some(tree) = &self.tile_tree
            && let Some(egui_tiles::Tile::Pane(pane)) = tree.tiles.get(tile_id)
        {
            return Some(&pane.widget);
        }
        None
    }

    pub fn get_widget_mut(&mut self, tile_id: egui_tiles::TileId) -> Option<&mut W> {
        if let Some(tree) = &mut self.tile_tree
            && let Some(egui_tiles::Tile::Pane(pane)) = tree.tiles.get_mut(tile_id)
        {
            return Some(&mut pane.widget);
        }
        None
    }

    pub fn activate_tab(&mut self, tile_id: egui_tiles::TileId) -> bool {
        if let Some(tree) = &mut self.tile_tree {
            tree.make_active(|id, _| id == tile_id)
        } else {
            false
        }
    }

    pub fn insert_pane(&mut self, widget: W) -> Option<egui_tiles::TileId> {
        if let Some(tree) = &mut self.tile_tree {
            let new_tile_id = tree.tiles.insert_pane(Pane::new(widget));
            if let Some(root) = tree.root {
                Self::add_to_container(&mut tree.tiles, root, new_tile_id);
            }
            self.layout_modified = true;
            Some(new_tile_id)
        } else {
            let mut tiles = egui_tiles::Tiles::default();
            let pane_id = tiles.insert_pane(Pane::new(widget));
            let root = tiles.insert_tab_tile(vec![pane_id]);
            self.tile_tree = Some(egui_tiles::Tree::new("mosaic_tree", root, tiles));
            self.layout_modified = true;
            Some(pane_id)
        }
    }

    pub fn insert_pane_in(
        &mut self,
        container_id: egui_tiles::TileId,
        widget: W,
    ) -> Option<egui_tiles::TileId> {
        if let Some(tree) = &mut self.tile_tree {
            let new_tile_id = tree.tiles.insert_pane(Pane::new(widget));
            Self::add_to_container(&mut tree.tiles, container_id, new_tile_id);
            self.layout_modified = true;
            Some(new_tile_id)
        } else {
            None
        }
    }

    pub fn remove_pane(&mut self, tile_id: egui_tiles::TileId) -> Option<W> {
        if let Some(tree) = &mut self.tile_tree
            && let Some(egui_tiles::Tile::Pane(pane)) = tree.tiles.remove(tile_id)
        {
            self.layout_modified = true;
            return Some(pane.widget);
        }
        None
    }

    pub(crate) fn add_to_container(
        tiles: &mut egui_tiles::Tiles<Pane<W>>,
        container_id: egui_tiles::TileId,
        child_id: egui_tiles::TileId,
    ) {
        match tiles.get_mut(container_id) {
            Some(egui_tiles::Tile::Container(egui_tiles::Container::Tabs(tabs))) => {
                tabs.add_child(child_id);
                tabs.set_active(child_id);
            }
            Some(egui_tiles::Tile::Container(egui_tiles::Container::Linear(linear))) => {
                linear.add_child(child_id);
            }
            Some(egui_tiles::Tile::Container(egui_tiles::Container::Grid(grid))) => {
                grid.add_child(child_id);
            }
            _ => {}
        }
    }

    pub fn drain_messages(&mut self) -> Vec<M> {
        self.pending_messages.drain(..).collect()
    }

    pub fn send_to(&mut self, tile_id: egui_tiles::TileId, message: M) {
        self.incoming_messages
            .entry(tile_id)
            .or_default()
            .push(message);
    }

    pub fn broadcast(&mut self, message: M)
    where
        M: Clone,
    {
        if let Some(tree) = &self.tile_tree {
            let pane_ids: Vec<egui_tiles::TileId> = tree
                .tiles
                .iter()
                .filter_map(|(tile_id, tile)| {
                    matches!(tile, egui_tiles::Tile::Pane(_)).then_some(*tile_id)
                })
                .collect();
            for tile_id in pane_ids {
                self.incoming_messages
                    .entry(tile_id)
                    .or_default()
                    .push(message.clone());
            }
        }
    }

    pub fn send_matching(&mut self, predicate: impl Fn(&W) -> bool, message: M)
    where
        M: Clone,
    {
        if let Some(tree) = &self.tile_tree {
            let matching_ids: Vec<egui_tiles::TileId> = tree
                .tiles
                .iter()
                .filter_map(|(tile_id, tile)| {
                    if let egui_tiles::Tile::Pane(pane) = tile {
                        predicate(&pane.widget).then_some(*tile_id)
                    } else {
                        None
                    }
                })
                .collect();
            for tile_id in matching_ids {
                self.incoming_messages
                    .entry(tile_id)
                    .or_default()
                    .push(message.clone());
            }
        }
    }
}