mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -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",
|
"libloading 0.7.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dock"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"egui_extras",
|
||||||
|
"env_logger",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "document-features"
|
name = "document-features"
|
||||||
version = "0.2.7"
|
version = "0.2.7"
|
||||||
@@ -1319,8 +1328,10 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"document-features",
|
"document-features",
|
||||||
"egui",
|
"egui",
|
||||||
|
"getrandom",
|
||||||
"image",
|
"image",
|
||||||
"log",
|
"log",
|
||||||
|
"rand",
|
||||||
"resvg",
|
"resvg",
|
||||||
"serde",
|
"serde",
|
||||||
"tiny-skia",
|
"tiny-skia",
|
||||||
@@ -1692,8 +1703,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ log = ["dep:log", "egui/log"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
egui = { version = "0.21.0", path = "../egui", default-features = false }
|
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"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
|
||||||
#! ### Optional dependencies
|
#! ### 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")]
|
#[cfg(feature = "chrono")]
|
||||||
mod datepicker;
|
mod datepicker;
|
||||||
|
|
||||||
|
pub mod dock;
|
||||||
pub mod image;
|
pub mod image;
|
||||||
mod layout;
|
mod layout;
|
||||||
mod sizing;
|
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