grimdock 0.2.0

Dockable panel layout system for egui
Documentation
//! # grimdock
//!
//! A dockable panel layout system for [egui](https://github.com/emilk/egui).
//!
//! Provides an IDE-style workspace where panels can be split, resized, and
//! rearranged by dragging tabs — all within egui's immediate-mode layer.
//!
//! ## Quick start
//!
//! ```rust,no_run
//! # use grimdock::{PanelTree, PanelStyle, PanelContext, Tab};
//! # fn show(ui: &mut egui::Ui, tree: &mut PanelTree<&'static str>) {
//! let style = PanelStyle::default();
//! PanelContext::new(ui, tree, &style).show(|ui, tab_id| {
//!     ui.label(*tab_id);
//! });
//! # }
//! ```

mod content;
mod dnd;
mod header;
mod ids;
mod layout;
mod persistence;
mod style;
mod tab;
mod tree;

pub use persistence::{
    LegacyPersistedNode, LegacyPersistedPanelTree, PersistError, PersistedNode, PersistedPanelTree,
    PersistedPanelTreeFile, PANEL_TREE_FORMAT_VERSION,
};
pub use style::{
    ContentStyle, HandleStyle, HeaderButtonStyle, HeaderStyle, OverlayStyle, PaneStyleOverride,
    PanelStyle, TabStateStyle, TabStyle, TabStyleOverride, TypographyStyle,
};
pub use tab::{Tab, TabDropPolicy, TabIcon};
pub use tree::{
    ChildSide, DropPolicy, HeaderVisibility, Node, Pane, PaneAnchor, PaneBuilder, PaneId, PaneMut,
    PaneOptions, PaneRole, PanelTree, SplitDir,
};

use egui::Ui;

/// Built-in behavior for resolving add/open actions when a tab with the same
/// identifier may already exist.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OpenBehavior {
    /// Always insert a fresh tab, even if the same identifier already exists.
    AllowDuplicate,
    /// Focus the existing tab anywhere in the dock instead of creating another.
    FocusExisting,
    /// Focus the existing tab only if it is already in the target pane.
    /// Otherwise insert a duplicate into the target pane.
    FocusExistingInPane,
}

/// A caller-provided entry shown in built-in add-tab and split-here menus.
#[derive(Clone, Debug)]
pub struct AddTabEntry<T: Clone + 'static> {
    pub title: String,
    pub tab: Tab<T>,
    pub open_behavior: OpenBehavior,
}

impl<T: Clone + 'static> AddTabEntry<T> {
    pub fn new(title: impl Into<String>, tab: Tab<T>) -> Self {
        Self {
            title: title.into(),
            tab,
            open_behavior: OpenBehavior::FocusExisting,
        }
    }

    pub fn with_open_behavior(mut self, open_behavior: OpenBehavior) -> Self {
        self.open_behavior = open_behavior;
        self
    }
}

/// A caller-provided pane action shown in the built-in pane menu.
#[derive(Clone, Debug)]
pub struct PaneMenuAction {
    pub id: String,
    pub title: String,
}

impl PaneMenuAction {
    pub fn new(id: impl Into<String>, title: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            title: title.into(),
        }
    }
}

/// A custom pane action invoked from the built-in pane menu during this frame.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PaneActionInvocation {
    pub pane_id: PaneId,
    pub action_id: String,
}

/// Mutations emitted by a single [`PanelContext::show`] pass.
#[derive(Debug)]
pub struct PanelOutput<T> {
    /// Tabs closed from pane headers during this frame.
    pub closed_tabs: Vec<T>,
    /// Custom pane actions invoked from built-in pane menus during this frame.
    pub pane_actions: Vec<PaneActionInvocation>,
}

impl<T> Default for PanelOutput<T> {
    fn default() -> Self {
        Self {
            closed_tabs: Vec::new(),
            pane_actions: Vec::new(),
        }
    }
}

/// Entry point for rendering the panel layout each frame.
///
/// The caller owns and persists the [`PanelTree`] across frames. Each frame,
/// construct a `PanelContext` and call [`PanelContext::show`] with a render
/// callback.
///
/// # Example
///
/// ```rust,no_run
/// # use grimdock::{PanelTree, PanelStyle, PanelContext, Tab};
/// # fn show(ui: &mut egui::Ui, tree: &mut PanelTree<&'static str>) {
/// PanelContext::new(ui, tree, &PanelStyle::default()).show(|ui, id| {
///     ui.heading(*id);
/// });
/// # }
/// ```
pub struct PanelContext<'ui, T: Clone + 'static> {
    ui: &'ui mut Ui,
    tree: &'ui mut PanelTree<T>,
    style: &'ui PanelStyle,
    add_tab_entries: &'ui [AddTabEntry<T>],
    add_tab_provider: Option<&'ui dyn Fn(PaneId, &PanelTree<T>) -> Vec<AddTabEntry<T>>>,
    pane_menu_actions: &'ui [PaneMenuAction],
    pane_menu_provider: Option<&'ui dyn Fn(PaneId, &PanelTree<T>) -> Vec<PaneMenuAction>>,
}

impl<'ui, T: Clone + 'static> PanelContext<'ui, T> {
    pub fn new(
        ui: &'ui mut Ui,
        tree: &'ui mut PanelTree<T>,
        style: &'ui PanelStyle,
    ) -> Self {
        Self {
            ui,
            tree,
            style,
            add_tab_entries: &[],
            add_tab_provider: None,
            pane_menu_actions: &[],
            pane_menu_provider: None,
        }
    }

    /// Provide the same entries for built-in add-tab, overflow, and split-here
    /// menus in every pane.
    pub fn with_add_tab_entries(mut self, add_tab_entries: &'ui [AddTabEntry<T>]) -> Self {
        self.add_tab_entries = add_tab_entries;
        self
    }

    /// Provide pane-scoped entries for built-in add-tab and split-here menus.
    pub fn with_add_tab_provider(
        mut self,
        add_tab_provider: &'ui dyn Fn(PaneId, &PanelTree<T>) -> Vec<AddTabEntry<T>>,
    ) -> Self {
        self.add_tab_provider = Some(add_tab_provider);
        self
    }

    /// Provide the same custom pane menu actions for every pane.
    pub fn with_pane_menu_actions(mut self, pane_menu_actions: &'ui [PaneMenuAction]) -> Self {
        self.pane_menu_actions = pane_menu_actions;
        self
    }

    /// Provide pane-scoped custom actions for the built-in pane menu.
    pub fn with_pane_menu_provider(
        mut self,
        pane_menu_provider: &'ui dyn Fn(PaneId, &PanelTree<T>) -> Vec<PaneMenuAction>,
    ) -> Self {
        self.pane_menu_provider = Some(pane_menu_provider);
        self
    }

    /// Run all three rendering passes and resolve any drag-and-drop that
    /// completed this frame.
    ///
    /// `render` is called once per visible leaf with the egui [`Ui`] for that
    /// pane's content area and a reference to the focused tab's identifier.
    pub fn show(self, render: impl FnMut(&mut Ui, &T)) -> PanelOutput<T>
    where
        T: PartialEq,
    {
        let Self {
            ui,
            tree,
            style,
            add_tab_entries,
            add_tab_provider,
            pane_menu_actions,
            pane_menu_provider,
        } = self;
        let mut output = PanelOutput::default();

        // Pass 1: layout — assign rects, draw resize handles.
        let leaf_rects = layout::layout_pass(ui, tree, style);

        // Pass 2: headers — draw tab buttons, initiate drags.
        header::header_pass(
            ui,
            tree,
            &leaf_rects,
            style,
            add_tab_entries,
            add_tab_provider,
            pane_menu_actions,
            pane_menu_provider,
            &mut output.closed_tabs,
            &mut output.pane_actions,
        );

        // Pass 3: content — invoke caller callback for each focused tab.
        content::content_pass(ui, tree, &leaf_rects, style, render);

        // Pass 4: drag-and-drop — draw overlay, resolve drops.
        dnd::dnd_pass(ui, tree, style);

        output
    }
}