//! Standalone demo: dockable side panels with a document center. //! //! ```text //! cargo run -p hcie-gui-egui --example dock_demo //! ``` //! //! Layout: //! ```text //! ┌──────────┬──────────────────────────┬────────────┐ //! │ Explorer │ Document 1 | Document 2 │ Properties │ //! │ Outline │ Document 3 │ Output │ //! ├──────────┤ ├────────────┤ //! │ (left) │ (center) │ (right) │ //! └──────────┴──────────────────────────┴────────────┘ //! ``` use eframe::egui; use grimdock::{ AddTabEntry, ChildSide, DropPolicy, Node, PaneAnchor, PaneBuilder, PaneOptions, PaneRole, PaneStyleOverride, PanelContext, PanelStyle, PanelTree, SplitDir, Tab, TabStyleOverride, }; #[derive(Clone, PartialEq, Eq, Debug)] enum TabId { Document(u32), Explorer, Outline, Properties, Output, } impl std::fmt::Display for TabId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { TabId::Document(n) => write!(f, "Document {n}"), TabId::Explorer => write!(f, "Explorer"), TabId::Outline => write!(f, "Outline"), TabId::Properties => write!(f, "Properties"), TabId::Output => write!(f, "Output"), } } } struct Palette { bg: egui::Color32, panel_bg: egui::Color32, accent: egui::Color32, text: egui::Color32, text_dim: egui::Color32, border: egui::Color32, hover: egui::Color32, } impl Palette { const DARK: Self = Self { bg: egui::Color32::from_rgb(30, 30, 30), panel_bg: egui::Color32::from_rgb(37, 37, 38), accent: egui::Color32::from_rgb(0, 122, 204), text: egui::Color32::from_rgb(212, 212, 212), text_dim: egui::Color32::from_rgb(128, 128, 128), border: egui::Color32::from_rgb(51, 51, 51), hover: egui::Color32::from_rgb(45, 45, 48), }; } struct App { tree: PanelTree, palette: Palette, frame_count: u64, documents: Vec, } impl App { fn new() -> Self { let pal = Palette::DARK; let doc1 = Tab::new("Document 1", TabId::Document(1)) .with_leading_visual(">") .with_closable(true) .with_style_override(TabStyleOverride { active_bg: Some(pal.panel_bg), inactive_bg: Some(pal.bg), hovered_bg: Some(pal.hover), text_color: Some(pal.text), accent_color: Some(pal.accent), icon_color: None, max_width: None, }); let doc2 = Tab::new("Document 2", TabId::Document(2)) .with_leading_visual("+") .with_closable(true); let mut tree = PanelTree::new(vec![doc1, doc2]); let side_opts = |anchor: PaneAnchor, role: PaneRole| PaneOptions { header_visibility: grimdock::HeaderVisibility::Always, allow_collapse: true, allow_tab_reorder: true, allow_tab_drag_out: true, allow_resize: true, drop_policy: DropPolicy::all(), lock_layout: false, anchor: Some(anchor), role: Some(role), persist_when_empty: true, paint_content_bg: true, style_override: Some(PaneStyleOverride { header_bg: Some(pal.bg), content_bg: Some(pal.panel_bg), border_color: Some(pal.border), accent_color: Some(pal.accent), }), }; // Split left: Explorer + Outline tree.split_leaf_with( 0, SplitDir::Horizontal, PaneBuilder::new( Tab::new("Explorer", TabId::Explorer).with_leading_visual("\u{1F4C2}"), ) .with_tabs(vec![ Tab::new("Explorer", TabId::Explorer).with_leading_visual("\u{1F4C2}"), Tab::new("Outline", TabId::Outline).with_leading_visual("\u{2630}"), ]) .with_options(side_opts(PaneAnchor::Left, PaneRole::Sidebar)), ChildSide::First, ); // Split right from the center (Document) leaf let center_leaf = tree .find_pane_containing(&TabId::Document(1)) .expect("document pane must exist"); { let mut center = tree.pane_mut(center_leaf).expect("center pane"); center.split_with( SplitDir::Horizontal, PaneBuilder::new( Tab::new("Properties", TabId::Properties).with_leading_visual("\u{2699}"), ) .with_options(side_opts(PaneAnchor::Right, PaneRole::Inspector)), ChildSide::Second, ); } // Add third document tab to center let center_leaf = tree .find_pane_containing(&TabId::Document(1)) .expect("center pane after right split"); { let mut center = tree.pane_mut(center_leaf).expect("center pane"); center.push_tab(Tab::new("Document 3", TabId::Document(3)).with_closable(true)); } // Add Output tab to right pane let right_leaf = tree .find_pane_containing(&TabId::Properties) .expect("right pane must exist"); { let mut right = tree.pane_mut(right_leaf).expect("right pane"); right.push_tab(Tab::new("Output", TabId::Output).with_leading_visual("!")); } // Set split ratios if let Node::Split { ratio, .. } = tree.node_mut(0) { *ratio = 0.22; } // Set the center-vs-right split ratio. After our splits the tree has // nodes 0..7, and the second horizontal split is at index 2. if let Node::Split { dir: SplitDir::Horizontal, ratio, .. } = tree.node_mut(2) { *ratio = 0.78; } let documents = vec![ "Welcome to Document 1\n\nThis is a demo showing\ngrimdock panel docking.\n\n\ Panels can be:\n\ - Dragged between panes\n\ - Resized by handles\n\ - Collapsed from headers" .into(), "Document 2 — A different file.\n\nDrag tabs between panes,\nresize by dragging handles." .into(), "Document 3 — Third tab.\n\nPanels can be collapsed\nand re-ordered." .into(), ]; Self { tree, palette: pal, frame_count: 0, documents, } } } impl eframe::App for App { fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) { self.frame_count += 1; let ctx = ui.ctx().clone(); egui::CentralPanel::default() .frame(egui::Frame::NONE) .show_inside(ui, |ui| { let tree = &mut self.tree; let pal = &self.palette; let mut style = PanelStyle::from_egui_style(ui.style().as_ref()); style.content_inset = 4.0; style.tabs.rounding = egui::CornerRadius::same(4); style.header.button.rounding = egui::CornerRadius::same(4); let frame_count = self.frame_count; let documents = &self.documents; let add_tab_entries = vec![AddTabEntry::new( "New Document", Tab::new("New Document", TabId::Document(99)).with_closable(true), )]; PanelContext::new(ui, tree, &style) .with_add_tab_entries(&add_tab_entries) .show(|ui, tab_id| { render_content(ui, tab_id, pal, frame_count, documents); }); }); ctx.request_repaint(); } } fn render_content( ui: &mut egui::Ui, tab_id: &TabId, pal: &Palette, frame_count: u64, documents: &[String], ) { match tab_id { TabId::Document(n) => render_document(ui, *n, pal, documents), TabId::Explorer => render_explorer(ui, pal), TabId::Outline => render_outline(ui, pal), TabId::Properties => render_properties(ui, pal), TabId::Output => render_output(ui, pal, frame_count), } } fn render_document(ui: &mut egui::Ui, n: u32, pal: &Palette, documents: &[String]) { ui.add_space(12.0); let text = documents .get((n - 1) as usize) .map(|s| s.as_str()) .unwrap_or("(empty document)"); let available = ui.available_rect_before_wrap(); ui.painter().rect_filled( available, egui::CornerRadius::same(4), egui::Color32::from_rgb(30, 30, 30), ); ui.vertical(|ui| { ui.add_space(8.0); ui.horizontal(|ui| { ui.add_space(8.0); ui.label( egui::RichText::new(format!("Document {n}")) .size(16.0) .strong() .color(pal.text), ); }); ui.add_space(8.0); ui.separator(); ui.add_space(8.0); for (i, line) in text.lines().enumerate() { ui.horizontal(|ui| { ui.add_space(8.0); ui.label( egui::RichText::new(format!("{:>3} ", i + 1)) .monospace() .size(13.0) .color(pal.text_dim), ); ui.label( egui::RichText::new(line) .monospace() .size(13.0) .color(pal.text), ); }); } ui.add_space(16.0); ui.horizontal(|ui| { ui.add_space(8.0); ui.label( egui::RichText::new( "Drag tabs between panes | Resize by dragging handles | Collapse panels", ) .size(11.0) .color(pal.text_dim), ); }); }); } fn render_explorer(ui: &mut egui::Ui, pal: &Palette) { ui.add_space(6.0); let items = [ ("src/", true, 0), ("main.rs", false, 1), ("lib.rs", false, 1), ("render/", true, 1), ("pipeline.rs", false, 2), ("shader.wgsl", false, 2), ("ui/", true, 1), ("panels.rs", false, 2), ("dock.rs", false, 2), ("assets/", true, 0), ("icon.png", false, 1), ("font.ttf", false, 1), ("Cargo.toml", false, 0), ("README.md", false, 0), ]; for (name, is_dir, depth) in items { ui.horizontal(|ui| { ui.add_space(depth as f32 * 14.0); let prefix = if is_dir { "\u{1F4C1} " } else { " " }; let color = if is_dir { pal.accent } else { pal.text }; ui.label( egui::RichText::new(format!("{prefix}{name}")) .monospace() .size(12.0) .color(color), ); }); } } fn render_outline(ui: &mut egui::Ui, pal: &Palette) { ui.add_space(6.0); let items = [ ("struct App", 0), (" fn new()", 1), (" fn update()", 1), ("impl eframe::App", 0), (" fn ui()", 1), ("fn render_content()", 0), ("fn render_document()", 0), ("fn render_explorer()", 0), ]; for (name, depth) in items { ui.horizontal(|ui| { ui.add_space(depth as f32 * 14.0); ui.label( egui::RichText::new(name) .monospace() .size(12.0) .color(if depth == 0 { pal.accent } else { pal.text }), ); }); } } fn render_properties(ui: &mut egui::Ui, pal: &Palette) { ui.add_space(6.0); ui.label( egui::RichText::new("Properties") .strong() .size(13.0) .color(pal.text), ); ui.separator(); ui.add_space(4.0); let props = [ ("Type", "Panel Demo"), ("Version", "1.0.0"), ("Author", "HCIE"), ("License", "MIT"), ("Panels", "Left, Center, Right"), ("Docking", "grimdock"), ("Framework", "egui + eframe"), ]; for (key, val) in props { ui.horizontal(|ui| { ui.add_space(4.0); ui.label( egui::RichText::new(format!("{key}:")) .size(12.0) .color(pal.text_dim), ); ui.label(egui::RichText::new(val).size(12.0).color(pal.text)); }); } ui.add_space(8.0); ui.separator(); ui.add_space(4.0); ui.label( egui::RichText::new("Appearance") .strong() .size(13.0) .color(pal.text), ); ui.add_space(4.0); ui.horizontal(|ui| { ui.add_space(4.0); ui.label( egui::RichText::new("BG Color:") .size(12.0) .color(pal.text_dim), ); ui.color_edit_button_srgba(&mut egui::Color32::from_rgb(37, 37, 38)); }); ui.horizontal(|ui| { ui.add_space(4.0); ui.label( egui::RichText::new("Accent:") .size(12.0) .color(pal.text_dim), ); ui.color_edit_button_srgba(&mut egui::Color32::from_rgb(0, 122, 204)); }); ui.horizontal(|ui| { ui.add_space(4.0); ui.label( egui::RichText::new("Font Size:") .size(12.0) .color(pal.text_dim), ); ui.add(egui::Slider::new(&mut 13.0f32, 8.0..=24.0).suffix("px")); }); } fn render_output(ui: &mut egui::Ui, pal: &Palette, frame: u64) { ui.add_space(4.0); let messages = [ ("[INFO] Application started", pal.text), ("[INFO] grimdock layout initialized", pal.text), ("[INFO] Left panel: Explorer + Outline", pal.accent), ("[INFO] Right panel: Properties + Output", pal.accent), ("[INFO] Center: 3 document tabs", pal.text), ("[INFO] Waiting for user interaction...", pal.text_dim), ( "[OK] All panels responsive", egui::Color32::from_rgb(100, 200, 120), ), ]; for (msg, color) in messages { ui.horizontal(|ui| { ui.add_space(4.0); ui.label(egui::RichText::new(msg).monospace().size(11.0).color(color)); }); } ui.add_space(8.0); ui.separator(); ui.add_space(4.0); ui.horizontal(|ui| { ui.add_space(4.0); ui.label( egui::RichText::new(format!("tick: {frame}")) .monospace() .size(11.0) .color(pal.text_dim), ); }); } fn main() -> eframe::Result<()> { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_title("grimdock — dock panel demo") .with_inner_size([1200.0, 720.0]) .with_min_inner_size([800.0, 480.0]), ..Default::default() }; eframe::run_native( "grimdock dock demo", options, Box::new(|_cc| Ok(Box::new(App::new()))), ) }