503 lines
15 KiB
Rust
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()))),
|
||
|
|
)
|
||
|
|
}
|