1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 15:13:12 -04:00
This commit is contained in:
Emil Ernerfeldt
2023-04-22 11:02:19 +02:00
parent 8fcbbb542f
commit 52657301c9
7 changed files with 626 additions and 0 deletions

13
Cargo.lock generated
View File

@@ -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]]

View File

@@ -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

View 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);
}
}
}

View File

@@ -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
View 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
View File

@@ -0,0 +1,7 @@
Example how to showcase `egui_extras::dock`.
```sh
cargo run -p dock
```
![](screenshot.png)

125
examples/dock/src/main.rs Normal file
View 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()
}
}