Files
hcie-rust-v3.05/hcie-egui-app/crates/hcie-gui-egui/examples/dock_demo.rs
T
2026-07-09 02:59:53 +03:00

503 lines
15 KiB
Rust

//! 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<TabId>,
palette: Palette,
frame_count: u64,
documents: Vec<String>,
}
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()))),
)
}