mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 15:13:12 -04:00
dock wip
This commit is contained in:
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -1128,6 +1128,15 @@ dependencies = [
|
||||
"libloading 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dock"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui_extras",
|
||||
"env_logger",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.7"
|
||||
@@ -1319,8 +1328,10 @@ dependencies = [
|
||||
"chrono",
|
||||
"document-features",
|
||||
"egui",
|
||||
"getrandom",
|
||||
"image",
|
||||
"log",
|
||||
"rand",
|
||||
"resvg",
|
||||
"serde",
|
||||
"tiny-skia",
|
||||
@@ -1692,8 +1703,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -39,6 +39,11 @@ log = ["dep:log", "egui/log"]
|
||||
[dependencies]
|
||||
egui = { version = "0.21.0", path = "../egui", default-features = false }
|
||||
|
||||
# For dock:
|
||||
# required to make rand work on wasm, see https://github.com/rust-random/rand#wasm-support
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
rand = { version = "0.8.5", features = ["getrandom", "small_rng"] }
|
||||
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
#! ### Optional dependencies
|
||||
|
||||
459
crates/egui_extras/src/dock/mod.rs
Normal file
459
crates/egui_extras/src/dock/mod.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use egui::{pos2, vec2, CursorIcon, NumExt, Rect, WidgetText};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Types required for state
|
||||
|
||||
/// An identifier for a node in the dock tree, be it a branch or a leaf.
|
||||
#[derive(
|
||||
Clone, Copy, Debug, Default, Hash, PartialEq, Eq, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub struct NodeId(u128);
|
||||
|
||||
impl NodeId {
|
||||
pub const ZERO: Self = Self(0);
|
||||
|
||||
pub fn random() -> Self {
|
||||
use rand::Rng as _;
|
||||
Self(rand::thread_rng().gen())
|
||||
}
|
||||
}
|
||||
|
||||
/// The top level type. Contains all peristent state, including layouts and sizes.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Dock<Leaf> {
|
||||
pub root: NodeId,
|
||||
pub nodes: Nodes<Leaf>,
|
||||
}
|
||||
|
||||
impl<Leaf> Default for Dock<Leaf> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
root: Default::default(),
|
||||
nodes: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains all node state, but no root.
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Nodes<Leaf> {
|
||||
pub nodes: HashMap<NodeId, NodeState<Leaf>>,
|
||||
}
|
||||
|
||||
impl<Leaf> Default for Nodes<Leaf> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nodes: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct NodeState<Leaf> {
|
||||
pub layout: NodeLayout<Leaf>,
|
||||
|
||||
/// Filled in by the layout step at the start of each frame.
|
||||
#[serde(skip)]
|
||||
#[serde(default = "nan_rect")]
|
||||
pub rect: Rect,
|
||||
}
|
||||
|
||||
fn nan_rect() -> Rect {
|
||||
Rect::NAN
|
||||
}
|
||||
|
||||
impl<Leaf> From<NodeLayout<Leaf>> for NodeState<Leaf> {
|
||||
fn from(layout: NodeLayout<Leaf>) -> Self {
|
||||
Self {
|
||||
layout,
|
||||
rect: Rect::NAN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum NodeLayout<Leaf> {
|
||||
Leaf(Leaf),
|
||||
Tabs(Tabs),
|
||||
Horizontal(Horizontal),
|
||||
// Vertical(Vertical)
|
||||
// Grid(Grid)
|
||||
}
|
||||
|
||||
impl<Leaf> NodeLayout<Leaf> {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
NodeLayout::Leaf(_) => "Leaf",
|
||||
NodeLayout::Tabs(_) => "Tabs",
|
||||
NodeLayout::Horizontal(_) => "Horizontal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Tabs {
|
||||
pub children: Vec<NodeId>,
|
||||
pub active: NodeId,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Horizontal {
|
||||
pub children: Vec<NodeId>,
|
||||
pub shares: Shares,
|
||||
}
|
||||
|
||||
/// How large of a share of space each child has, on a 1D axis.
|
||||
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct Shares {
|
||||
/// How large of a share each child has.
|
||||
///
|
||||
/// For instance, the shares `[1, 2, 3]` means that the first child gets 1/6 of the space,
|
||||
/// the second gets 2/6 and the third gets 3/6.
|
||||
pub shares: HashMap<NodeId, f32>,
|
||||
}
|
||||
|
||||
impl Shares {
|
||||
fn split(&self, children: &[NodeId], available_width: f32) -> Vec<f32> {
|
||||
let mut num_shares = 0.0;
|
||||
for child in children {
|
||||
num_shares += self.shares.get(child).copied().unwrap_or(1.0);
|
||||
}
|
||||
if num_shares == 0.0 {
|
||||
num_shares = 1.0;
|
||||
}
|
||||
children
|
||||
.iter()
|
||||
.map(|child| {
|
||||
available_width * self.shares.get(child).copied().unwrap_or(1.0) / num_shares
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Leaf> From<Tabs> for NodeLayout<Leaf> {
|
||||
fn from(tabs: Tabs) -> Self {
|
||||
NodeLayout::Tabs(tabs)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Leaf> From<Horizontal> for NodeLayout<Leaf> {
|
||||
fn from(horizontal: Horizontal) -> Self {
|
||||
NodeLayout::Horizontal(horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum NodeAction {
|
||||
Keep,
|
||||
Remove,
|
||||
}
|
||||
|
||||
/// Trait defining how the [`Dock`] and its leaf should be shown.
|
||||
pub trait Behavior<Leaf> {
|
||||
/// Show this leaf node in the given [`egui::Ui`].
|
||||
///
|
||||
/// If this is an unknown node, return [`NodeAction::Remove`] and the node will be removed.
|
||||
fn leaf_ui(&mut self, _ui: &mut egui::Ui, _node_id: NodeId, _leaf: &mut Leaf);
|
||||
|
||||
fn tab_text_for_leaf(&mut self, leaf: &Leaf) -> egui::WidgetText;
|
||||
|
||||
fn tab_text_for_node(&mut self, nodes: &Nodes<Leaf>, node_id: NodeId) -> WidgetText {
|
||||
match &nodes.nodes[&node_id].layout {
|
||||
NodeLayout::Leaf(leaf) => self.tab_text_for_leaf(leaf),
|
||||
layout => layout.name().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn tab_ui(
|
||||
&mut self,
|
||||
nodes: &Nodes<Leaf>,
|
||||
ui: &mut egui::Ui,
|
||||
node_id: NodeId,
|
||||
selected: bool,
|
||||
) -> egui::Response {
|
||||
let text = self.tab_text_for_node(nodes, node_id);
|
||||
let font_id = egui::TextStyle::Button.resolve(ui.style());
|
||||
let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
|
||||
let (rect, response) = ui.allocate_exact_size(galley.size(), egui::Sense::click_and_drag());
|
||||
let widget_style = ui.style().interact_selectable(&response, selected);
|
||||
ui.painter()
|
||||
.galley_with_color(rect.min, galley.galley, widget_style.text_color());
|
||||
response
|
||||
}
|
||||
|
||||
/// Returns `false` if this leaf should be removed from its parent.
|
||||
fn retain_leaf(&mut self, _leaf: &Leaf) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
// ---
|
||||
// Settings:
|
||||
|
||||
/// The height of the bar holding tab names.
|
||||
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
|
||||
20.0
|
||||
}
|
||||
|
||||
/// Width of the gap between nodes in a horizontal or vertical layout
|
||||
fn gap_width(&self, _style: &egui::Style) -> f32 {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Construction
|
||||
|
||||
impl<Leaf> Dock<Leaf> {
|
||||
pub fn new(root: NodeId, nodes: Nodes<Leaf>) -> Self {
|
||||
Self { root, nodes }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Leaf> Nodes<Leaf> {
|
||||
#[must_use]
|
||||
pub fn insert_node(&mut self, node: NodeState<Leaf>) -> NodeId {
|
||||
let id = NodeId::random();
|
||||
self.nodes.insert(id, node);
|
||||
id
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn insert_leaf(&mut self, leaf: Leaf) -> NodeId {
|
||||
self.insert_node(NodeLayout::Leaf(leaf).into())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn insert_tab_node(&mut self, children: Vec<NodeId>) -> NodeId {
|
||||
let tabs = Tabs {
|
||||
active: children.first().copied().unwrap_or_default(),
|
||||
children,
|
||||
};
|
||||
self.insert_node(NodeLayout::Tabs(tabs).into())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn insert_horizontal_node(&mut self, children: Vec<NodeId>) -> NodeId {
|
||||
let horizontal = Horizontal {
|
||||
children,
|
||||
shares: Default::default(),
|
||||
};
|
||||
self.insert_node(NodeLayout::Horizontal(horizontal).into())
|
||||
}
|
||||
|
||||
pub fn get(&self, node_id: NodeId) -> Option<&NodeLayout<Leaf>> {
|
||||
self.nodes.get(&node_id).map(|node| &node.layout)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
impl<Leaf> Dock<Leaf> {
|
||||
pub fn root(&self) -> NodeId {
|
||||
self.root
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, behavior: &mut dyn Behavior<Leaf>, ui: &mut egui::Ui) {
|
||||
self.nodes.gc_root(behavior, self.root);
|
||||
|
||||
self.nodes.layout_node(
|
||||
ui.style(),
|
||||
behavior,
|
||||
ui.available_rect_before_wrap(),
|
||||
self.root,
|
||||
);
|
||||
|
||||
self.nodes.node_ui(behavior, ui, self.root);
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// gc
|
||||
|
||||
#[derive(PartialEq, Eq)]
|
||||
enum GcAction {
|
||||
Keep,
|
||||
Remove,
|
||||
}
|
||||
|
||||
impl<Leaf> Nodes<Leaf> {
|
||||
fn gc_root(&mut self, behavior: &mut dyn Behavior<Leaf>, root_id: NodeId) {
|
||||
let mut visited = HashSet::default();
|
||||
self.gc_node_id(behavior, &mut visited, root_id);
|
||||
self.nodes.retain(|node_id, _| visited.contains(node_id));
|
||||
}
|
||||
|
||||
fn gc_node_id(
|
||||
&mut self,
|
||||
behavior: &mut dyn Behavior<Leaf>,
|
||||
visited: &mut HashSet<NodeId>,
|
||||
node_id: NodeId,
|
||||
) -> GcAction {
|
||||
let Some(mut node) = self.nodes.remove(&node_id) else { return GcAction::Remove; };
|
||||
if !visited.insert(node_id) {
|
||||
#[cfg(feature = "log")]
|
||||
log::warn!("Cycle detected in egui_extras::dock");
|
||||
return GcAction::Remove;
|
||||
}
|
||||
|
||||
match &mut node.layout {
|
||||
NodeLayout::Leaf(leaf) => {
|
||||
if !behavior.retain_leaf(leaf) {
|
||||
return GcAction::Remove;
|
||||
}
|
||||
}
|
||||
NodeLayout::Tabs(layout) => {
|
||||
layout
|
||||
.children
|
||||
.retain(|&child| self.gc_node_id(behavior, visited, child) == GcAction::Keep);
|
||||
}
|
||||
NodeLayout::Horizontal(layout) => {
|
||||
layout
|
||||
.children
|
||||
.retain(|&child| self.gc_node_id(behavior, visited, child) == GcAction::Keep);
|
||||
}
|
||||
}
|
||||
self.nodes.insert(node_id, node);
|
||||
GcAction::Keep
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// layout
|
||||
|
||||
impl<Leaf> Nodes<Leaf> {
|
||||
fn layout_node(
|
||||
&mut self,
|
||||
style: &egui::Style,
|
||||
behavior: &mut dyn Behavior<Leaf>,
|
||||
rect: Rect,
|
||||
node_id: NodeId,
|
||||
) {
|
||||
let Some(mut node) = self.nodes.remove(&node_id) else { return; };
|
||||
node.rect = rect;
|
||||
|
||||
match &node.layout {
|
||||
NodeLayout::Leaf(_) => {}
|
||||
NodeLayout::Tabs(tabs) => {
|
||||
self.layout_tabs(style, behavior, rect, tabs);
|
||||
}
|
||||
NodeLayout::Horizontal(horizontal) => {
|
||||
self.layout_horizontal(style, behavior, rect, horizontal);
|
||||
}
|
||||
}
|
||||
|
||||
self.nodes.insert(node_id, node);
|
||||
}
|
||||
|
||||
fn layout_tabs(
|
||||
&mut self,
|
||||
style: &egui::Style,
|
||||
behavior: &mut dyn Behavior<Leaf>,
|
||||
rect: Rect,
|
||||
tabs: &Tabs,
|
||||
) {
|
||||
let mut active_rect = rect;
|
||||
active_rect.min.y += behavior.tab_bar_height(style);
|
||||
|
||||
if false {
|
||||
self.layout_node(style, behavior, active_rect, tabs.active);
|
||||
} else {
|
||||
// Layout all nodes in case the user switches active tab
|
||||
for &child_id in &tabs.children {
|
||||
self.layout_node(style, behavior, active_rect, child_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_horizontal(
|
||||
&mut self,
|
||||
style: &egui::Style,
|
||||
behavior: &mut dyn Behavior<Leaf>,
|
||||
rect: Rect,
|
||||
horizontal: &Horizontal,
|
||||
) {
|
||||
if horizontal.children.is_empty() {
|
||||
return;
|
||||
}
|
||||
let num_gaps = horizontal.children.len() - 1;
|
||||
let gap_width = behavior.gap_width(style);
|
||||
let total_gap_width = gap_width * num_gaps as f32;
|
||||
let available_width = (rect.width() - total_gap_width).at_least(0.0);
|
||||
|
||||
let widths = horizontal
|
||||
.shares
|
||||
.split(&horizontal.children, available_width);
|
||||
|
||||
let mut x = rect.min.x;
|
||||
for (child, width) in horizontal.children.iter().zip(widths) {
|
||||
let child_rect = Rect::from_min_size(pos2(x, rect.min.y), vec2(width, rect.height()));
|
||||
self.layout_node(style, behavior, child_rect, *child);
|
||||
x += width + gap_width;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// ui
|
||||
|
||||
impl<Leaf> Nodes<Leaf> {
|
||||
fn node_ui(&mut self, behavior: &mut dyn Behavior<Leaf>, ui: &mut egui::Ui, node_id: NodeId) {
|
||||
let Some(mut node) = self.nodes.remove(&node_id) else { return };
|
||||
|
||||
match &mut node.layout {
|
||||
NodeLayout::Leaf(leaf) => {
|
||||
let mut leaf_ui = ui.child_ui(node.rect, *ui.layout());
|
||||
behavior.leaf_ui(&mut leaf_ui, node_id, leaf);
|
||||
}
|
||||
NodeLayout::Tabs(tabs) => self.tabs_ui(behavior, ui, node.rect, tabs),
|
||||
NodeLayout::Horizontal(horizontal) => {
|
||||
self.horizontal_ui(behavior, ui, node.rect, horizontal);
|
||||
}
|
||||
};
|
||||
self.nodes.insert(node_id, node);
|
||||
}
|
||||
|
||||
fn tabs_ui(
|
||||
&mut self,
|
||||
behavior: &mut dyn Behavior<Leaf>,
|
||||
ui: &mut egui::Ui,
|
||||
rect: Rect,
|
||||
tabs: &mut Tabs,
|
||||
) {
|
||||
let tab_bar_height = behavior.tab_bar_height(ui.style());
|
||||
let tab_bar_rect = {
|
||||
let mut r = rect;
|
||||
r.max.y = r.min.y + tab_bar_height;
|
||||
r
|
||||
};
|
||||
let mut tab_bar_ui = ui.child_ui(tab_bar_rect, *ui.layout());
|
||||
|
||||
// Show tab bar:
|
||||
tab_bar_ui.horizontal(|ui| {
|
||||
for &child_id in &tabs.children {
|
||||
let selected = child_id == tabs.active;
|
||||
let response = behavior.tab_ui(self, ui, child_id, selected);
|
||||
let response = response.on_hover_cursor(CursorIcon::Grab);
|
||||
if response.clicked() {
|
||||
tabs.active = child_id;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
self.node_ui(behavior, ui, tabs.active);
|
||||
}
|
||||
|
||||
fn horizontal_ui(
|
||||
&mut self,
|
||||
behavior: &mut dyn Behavior<Leaf>,
|
||||
ui: &mut egui::Ui,
|
||||
_rect: Rect,
|
||||
horizontal: &mut Horizontal,
|
||||
) {
|
||||
for child in &horizontal.children {
|
||||
self.node_ui(behavior, ui, *child);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@
|
||||
#[cfg(feature = "chrono")]
|
||||
mod datepicker;
|
||||
|
||||
pub mod dock;
|
||||
pub mod image;
|
||||
mod layout;
|
||||
mod sizing;
|
||||
|
||||
16
examples/dock/Cargo.toml
Normal file
16
examples/dock/Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "dock"
|
||||
version = "0.1.0"
|
||||
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
publish = false
|
||||
|
||||
|
||||
[dependencies]
|
||||
eframe = { path = "../../crates/eframe", features = [
|
||||
"__screenshot", # __screenshot is so we can dump a screenshot using EFRAME_SCREENSHOT_TO
|
||||
] }
|
||||
egui_extras = { path = "../../crates/egui_extras" }
|
||||
env_logger = "0.10"
|
||||
7
examples/dock/README.md
Normal file
7
examples/dock/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Example how to showcase `egui_extras::dock`.
|
||||
|
||||
```sh
|
||||
cargo run -p dock
|
||||
```
|
||||
|
||||

|
||||
125
examples/dock/src/main.rs
Normal file
125
examples/dock/src/main.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
|
||||
|
||||
use eframe::egui;
|
||||
use egui::Color32;
|
||||
|
||||
use egui_extras::dock;
|
||||
|
||||
fn main() -> Result<(), eframe::Error> {
|
||||
env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`).
|
||||
let options = eframe::NativeOptions {
|
||||
initial_window_size: Some(egui::vec2(320.0, 240.0)),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native("Dock", options, Box::new(|_cc| Box::<MyApp>::default()))
|
||||
}
|
||||
|
||||
pub struct View {
|
||||
title: String,
|
||||
color: Color32,
|
||||
}
|
||||
|
||||
impl View {
|
||||
pub fn with_nr(i: usize) -> Self {
|
||||
Self {
|
||||
title: format!("Node {}", i),
|
||||
color: egui::epaint::Hsva::new(0.1 * i as f32, 0.5, 0.5, 1.0).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ui: &mut egui::Ui) {
|
||||
ui.painter().rect_filled(ui.max_rect(), 0.0, self.color);
|
||||
ui.label(&self.title);
|
||||
}
|
||||
}
|
||||
|
||||
struct MyApp {
|
||||
dock: dock::Dock<View>,
|
||||
}
|
||||
|
||||
impl Default for MyApp {
|
||||
fn default() -> Self {
|
||||
let mut next_view_nr = 0;
|
||||
let mut gen_view = || {
|
||||
let view = View::with_nr(next_view_nr);
|
||||
next_view_nr += 1;
|
||||
view
|
||||
};
|
||||
|
||||
let mut nodes = dock::Nodes::default();
|
||||
|
||||
let tab0 = { nodes.insert_leaf(gen_view()) };
|
||||
let tab1 = {
|
||||
let a = nodes.insert_leaf(gen_view());
|
||||
let b = nodes.insert_leaf(gen_view());
|
||||
nodes.insert_tab_node(vec![a, b])
|
||||
};
|
||||
let tab2 = {
|
||||
let a = nodes.insert_leaf(gen_view());
|
||||
let b = nodes.insert_leaf(gen_view());
|
||||
nodes.insert_horizontal_node(vec![a, b])
|
||||
};
|
||||
|
||||
let root = nodes.insert_tab_node(vec![tab0, tab1, tab2]);
|
||||
|
||||
let dock = dock::Dock::new(root, nodes);
|
||||
|
||||
Self { dock }
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for MyApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
let mut behavior = DockBehavior {};
|
||||
|
||||
egui::SidePanel::left("tree").show(ctx, |ui| {
|
||||
tree_ui(ui, &mut behavior, &self.dock.nodes, self.dock.root);
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
self.dock.ui(&mut behavior, ui);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn tree_ui(
|
||||
ui: &mut egui::Ui,
|
||||
behavior: &mut dyn dock::Behavior<View>,
|
||||
nodes: &dock::Nodes<View>,
|
||||
node_id: dock::NodeId,
|
||||
) {
|
||||
let Some(node) = nodes.get(node_id) else { return; };
|
||||
|
||||
// if let dock::NodeLayout::Leaf(view) = node {
|
||||
// ui.label(&view.title);
|
||||
// return;
|
||||
// }
|
||||
|
||||
egui::CollapsingHeader::new(behavior.tab_text_for_node(nodes, node_id))
|
||||
.default_open(true)
|
||||
.show(ui, |ui| match node {
|
||||
dock::NodeLayout::Leaf(_) => {}
|
||||
dock::NodeLayout::Tabs(tabs) => {
|
||||
for &child in &tabs.children {
|
||||
tree_ui(ui, behavior, nodes, child);
|
||||
}
|
||||
}
|
||||
dock::NodeLayout::Horizontal(horizontal) => {
|
||||
for &child in &horizontal.children {
|
||||
tree_ui(ui, behavior, nodes, child);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
struct DockBehavior {}
|
||||
|
||||
impl dock::Behavior<View> for DockBehavior {
|
||||
fn leaf_ui(&mut self, ui: &mut egui::Ui, _node_id: dock::NodeId, view: &mut View) {
|
||||
view.ui(ui);
|
||||
}
|
||||
|
||||
fn tab_text_for_leaf(&mut self, view: &View) -> egui::WidgetText {
|
||||
view.title.clone().into()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user