grimdock 0.2.0

Dockable panel layout system for egui
Documentation
//! Theme and colour options example:
//! `cargo run --example 7_theme_options`

use eframe::egui;
use grimdock::{
    ChildSide, PaneId, PaneStyleOverride, PanelContext, PanelStyle, PanelTree, SplitDir, Tab,
    TabStyleOverride,
};

fn main() -> eframe::Result<()> {
    let options = eframe::NativeOptions {
        viewport: egui::ViewportBuilder::default()
            .with_title("grimdock 7_theme_options")
            .with_inner_size([1260.0, 760.0]),
        ..Default::default()
    };

    eframe::run_native(
        "grimdock 7_theme_options",
        options,
        Box::new(|cc| Ok(Box::new(App::new(&cc.egui_ctx)))),
    )
}

#[derive(Clone, PartialEq, Eq, Debug)]
enum TabId {
    Files,
    Editor,
    Search,
    Preview,
    Terminal,
}

struct App {
    tree: PanelTree<TabId>,
    style: PanelStyle,
    terminal_pane: PaneId,
    terminal_override_enabled: bool,
    terminal_override: PaneStyleOverride,
    search_override_enabled: bool,
    search_override: TabStyleOverride,
}

impl App {
    fn new(ctx: &egui::Context) -> Self {
        let (tree, terminal_pane) = make_tree();
        Self {
            tree,
            style: PanelStyle::from_visuals(&ctx.style().visuals),
            terminal_pane,
            terminal_override_enabled: true,
            terminal_override: PaneStyleOverride {
                header_bg: Some(egui::Color32::from_rgb(45, 28, 20)),
                content_bg: Some(egui::Color32::from_rgb(34, 22, 16)),
                border_color: Some(egui::Color32::from_rgb(168, 99, 56)),
                accent_color: Some(egui::Color32::from_rgb(230, 142, 78)),
            },
            search_override_enabled: true,
            search_override: TabStyleOverride {
                active_bg: Some(egui::Color32::from_rgb(30, 74, 66)),
                inactive_bg: Some(egui::Color32::from_rgb(24, 52, 48)),
                hovered_bg: Some(egui::Color32::from_rgb(36, 90, 80)),
                text_color: Some(egui::Color32::from_rgb(214, 245, 238)),
                accent_color: Some(egui::Color32::from_rgb(52, 204, 168)),
                icon_color: None,
            },
        }
    }

    fn sync_overrides(&mut self) {
        if let Some(mut pane) = self.tree.pane_mut(self.terminal_pane) {
            let mut options = pane.options();
            options.style_override = self.terminal_override_enabled.then_some(self.terminal_override);
            pane.set_options(options);
        }

        if let Some(search_pane_id) = self.tree.find_pane_containing(&TabId::Search) {
            if let Some(mut pane) = self.tree.pane_mut(search_pane_id) {
                if let Some(tab) = pane.tabs_mut().iter_mut().find(|tab| tab.id == TabId::Search) {
                    tab.style_override = self.search_override_enabled.then_some(self.search_override);
                }
            }
        }
    }
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        self.sync_overrides();

        egui::SidePanel::left("theme_controls")
            .resizable(true)
            .default_width(360.0)
            .show(ctx, |ui| {
                ui.heading("Theme workbench");
                ui.label("This example exposes the global PanelStyle plus pane and tab color overrides.");

                ui.horizontal(|ui| {
                    if ui.button("Use egui visuals").clicked() {
                        self.style = PanelStyle::from_visuals(&ctx.style().visuals);
                    }
                    if ui.button("Use crate defaults").clicked() {
                        self.style = PanelStyle::default();
                    }
                });

                ui.separator();

                ui.collapsing("Layout metrics", |ui| {
                    ui.add(egui::Slider::new(&mut self.style.header_height, 20.0..=40.0).text("Header height"));
                    ui.add(
                        egui::Slider::new(&mut self.style.collapsed_pane_thickness, 20.0..=60.0)
                            .text("Collapsed thickness"),
                    );
                    ui.add(egui::Slider::new(&mut self.style.handle_width, 2.0..=12.0).text("Handle width"));
                    ui.add(egui::Slider::new(&mut self.style.min_pane_size, 40.0..=160.0).text("Min pane size"));
                    ui.add(egui::Slider::new(&mut self.style.content_inset, 0.0..=20.0).text("Content inset"));
                    corner_radius_slider(ui, "Pane rounding", &mut self.style.pane_rounding);
                    corner_radius_slider(ui, "Tab rounding", &mut self.style.tabs.rounding);
                    corner_radius_slider(ui, "Header button rounding", &mut self.style.header.button.rounding);
                });

                ui.collapsing("Typography", |ui| {
                    ui.label("Dock chrome text is painted directly by grimdock.");
                    ui.label("Font weight comes from whichever font family your app has loaded.");
                    font_controls(ui, "Tab title", &mut self.style.typography.tab_title_font);
                    font_controls(
                        ui,
                        "Leading text icon",
                        &mut self.style.typography.tab_icon_text_font,
                    );
                });

                ui.collapsing("Header", |ui| {
                    color_row(ui, "Header bg", &mut self.style.header.bg);
                    color_row(ui, "Header border", &mut self.style.header.border_color);
                    color_row(ui, "Button bg", &mut self.style.header.button.bg);
                    color_row(ui, "Button hover bg", &mut self.style.header.button.hover_bg);
                    color_row(ui, "Button stroke", &mut self.style.header.button.stroke_color);
                    color_row(ui, "Button icon", &mut self.style.header.button.icon_color);
                    color_row(ui, "Button hover icon", &mut self.style.header.button.hover_icon_color);
                });

                ui.collapsing("Content", |ui| {
                    color_row(ui, "Content bg", &mut self.style.content.bg);
                    color_row(ui, "Content border", &mut self.style.content.border_color);
                });

                ui.collapsing("Tabs", |ui| {
                    ui.label("Active");
                    color_row(ui, "Active bg", &mut self.style.tabs.active.bg);
                    color_row(ui, "Active text", &mut self.style.tabs.active.text_color);
                    color_row(ui, "Active accent", &mut self.style.tabs.active.accent_color);

                    ui.separator();
                    ui.label("Inactive");
                    color_row(ui, "Inactive bg", &mut self.style.tabs.inactive.bg);
                    color_row(ui, "Inactive text", &mut self.style.tabs.inactive.text_color);
                    color_row(ui, "Inactive accent", &mut self.style.tabs.inactive.accent_color);

                    ui.separator();
                    ui.label("Hovered");
                    color_row(ui, "Hovered bg", &mut self.style.tabs.hovered.bg);
                    color_row(ui, "Hovered text", &mut self.style.tabs.hovered.text_color);
                    color_row(ui, "Hovered accent", &mut self.style.tabs.hovered.accent_color);
                });

                ui.collapsing("Handles and overlay", |ui| {
                    color_row(ui, "Handle", &mut self.style.handle.color);
                    color_row(ui, "Handle hover", &mut self.style.handle.hover_color);
                    color_row(ui, "Handle locked", &mut self.style.handle.locked_color);
                    color_row(ui, "Overlay fill", &mut self.style.overlay.fill);
                    color_row(ui, "Overlay stroke", &mut self.style.overlay.stroke.color);
                    ui.add(
                        egui::Slider::new(&mut self.style.overlay.stroke.width, 0.5..=4.0)
                            .text("Overlay stroke width"),
                    );
                });

                ui.collapsing("Pane override", |ui| {
                    ui.checkbox(&mut self.terminal_override_enabled, "Enable Terminal pane override");
                    color_option_row(ui, "Header bg", &mut self.terminal_override.header_bg);
                    color_option_row(ui, "Content bg", &mut self.terminal_override.content_bg);
                    color_option_row(ui, "Border", &mut self.terminal_override.border_color);
                    color_option_row(ui, "Accent", &mut self.terminal_override.accent_color);
                });

                ui.collapsing("Tab override", |ui| {
                    ui.checkbox(&mut self.search_override_enabled, "Enable Search tab override");
                    color_option_row(ui, "Active bg", &mut self.search_override.active_bg);
                    color_option_row(ui, "Inactive bg", &mut self.search_override.inactive_bg);
                    color_option_row(ui, "Hovered bg", &mut self.search_override.hovered_bg);
                    color_option_row(ui, "Text", &mut self.search_override.text_color);
                    color_option_row(ui, "Accent", &mut self.search_override.accent_color);
                });
            });

        egui::CentralPanel::default()
            .frame(egui::Frame::NONE)
            .show(ctx, |ui| {
                PanelContext::new(ui, &mut self.tree, &self.style).show(|ui, tab_id| match tab_id {
                    TabId::Files => {
                        ui.heading("Files");
                        ui.label("Use this workbench to tune every main color and size in PanelStyle.");
                    }
                    TabId::Editor => {
                        ui.heading("Editor");
                        ui.label("Drag tabs and resize panes while adjusting the theme controls.");
                    }
                    TabId::Search => {
                        ui.heading("Search");
                        ui.label("This tab can demonstrate per-tab color overrides.");
                    }
                    TabId::Preview => {
                        ui.heading("Preview");
                        ui.label("The dock updates live as you change colors, rounding, and handle styles.");
                    }
                    TabId::Terminal => {
                        ui.heading("Terminal");
                        ui.label("This pane can demonstrate per-pane header/content/accent overrides.");
                    }
                });
            });
    }
}

fn color_row(ui: &mut egui::Ui, label: &str, color: &mut egui::Color32) {
    ui.horizontal(|ui| {
        ui.label(label);
        ui.color_edit_button_srgba(color);
    });
}

fn color_option_row(ui: &mut egui::Ui, label: &str, color: &mut Option<egui::Color32>) {
    let mut enabled = color.is_some();
    ui.horizontal(|ui| {
        ui.checkbox(&mut enabled, label);
        if !enabled {
            *color = None;
            return;
        }

        let value = color.get_or_insert(egui::Color32::WHITE);
        ui.color_edit_button_srgba(value);
    });
}

fn corner_radius_slider(ui: &mut egui::Ui, label: &str, radius: &mut egui::CornerRadius) {
    let mut value = radius.nw;
    ui.add(egui::Slider::new(&mut value, 0..=16).text(label));
    *radius = egui::CornerRadius::same(value);
}

fn font_controls(ui: &mut egui::Ui, label: &str, font: &mut egui::FontId) {
    ui.group(|ui| {
        ui.label(label);
        ui.add(egui::Slider::new(&mut font.size, 8.0..=24.0).text("Size"));

        let mut family = match &font.family {
            egui::FontFamily::Proportional => 0,
            egui::FontFamily::Monospace => 1,
            egui::FontFamily::Name(_) => 2,
        };
        egui::ComboBox::from_label("Family")
            .selected_text(match family {
                0 => "Proportional",
                1 => "Monospace",
                _ => "Custom",
            })
            .show_ui(ui, |ui| {
                ui.selectable_value(&mut family, 0, "Proportional");
                ui.selectable_value(&mut family, 1, "Monospace");
                ui.selectable_value(&mut family, 2, "Custom");
            });

        if family == 2 {
            let mut name = match &font.family {
                egui::FontFamily::Name(name) => name.to_string(),
                _ => String::from("bold"),
            };
            ui.text_edit_singleline(&mut name);
            font.family = egui::FontFamily::Name(name.into());
        } else {
            font.family = if family == 0 {
                egui::FontFamily::Proportional
            } else {
                egui::FontFamily::Monospace
            };
        }
    });
}

fn make_tree() -> (PanelTree<TabId>, PaneId) {
    let mut tree = PanelTree::new(vec![
        Tab::new("editor", TabId::Editor).with_leading_visual(">"),
        Tab::new("search", TabId::Search).with_leading_visual("?"),
    ]);

    tree.split_leaf(
        0,
        SplitDir::Horizontal,
        Tab::new("files", TabId::Files).with_leading_visual("#"),
        ChildSide::First,
    );

    let editor_pane = tree
        .find_pane_containing(&TabId::Editor)
        .expect("editor pane should exist");
    let terminal_pane = tree
        .pane_mut(editor_pane)
        .expect("editor pane should exist")
        .split(
            SplitDir::Vertical,
            Tab::new("terminal", TabId::Terminal).with_leading_visual("$"),
            ChildSide::Second,
        );

    if let Some(mut pane) = tree.pane_mut(editor_pane) {
        pane.push_tab(Tab::new("preview", TabId::Preview).with_leading_visual("*"));
    }

    (tree, terminal_pane)
}