1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-27 07:03:14 -04:00

Code cleanup

This commit is contained in:
Emil Ernerfeldt
2023-05-04 21:59:45 +02:00
parent aa30414caf
commit e84eef7815
5 changed files with 146 additions and 99 deletions

View File

@@ -1,4 +1,6 @@
use egui::{vec2, Color32, Id, Response, Rgba, Sense, Stroke, TextStyle, Ui, Visuals, WidgetText};
use egui::{
vec2, Color32, Id, Rect, Response, Rgba, Sense, Stroke, TextStyle, Ui, Visuals, WidgetText,
};
use super::{Node, NodeId, Nodes, ResizeState, SimplificationOptions, UiResponse};
@@ -7,14 +9,22 @@ 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.
///
/// You can make the leaf draggable by returning [`UiResponse::DragStarted`]
/// when the user drags some handle.
fn leaf_ui(&mut self, _ui: &mut Ui, _node_id: NodeId, _leaf: &mut Leaf) -> UiResponse;
fn tab_text_for_leaf(&mut self, leaf: &Leaf) -> WidgetText;
/// The title of a leaf tab.
fn tab_title_for_leaf(&mut self, leaf: &Leaf) -> WidgetText;
fn tab_text_for_node(&mut self, nodes: &Nodes<Leaf>, node_id: NodeId) -> WidgetText {
/// The title of a general tab.
///
/// The default implementation uses the name of the layout for branches, and
/// calls [`Self::tab_text_for_leaf`] for leaves.
fn tab_title_for_node(&mut self, nodes: &Nodes<Leaf>, node_id: NodeId) -> WidgetText {
if let Some(node) = nodes.nodes.get(&node_id) {
match node {
Node::Leaf(leaf) => self.tab_text_for_leaf(leaf),
Node::Leaf(leaf) => self.tab_title_for_leaf(leaf),
Node::Branch(branch) => format!("{:?}", branch.get_layout()).into(),
}
} else {
@@ -23,6 +33,8 @@ pub trait Behavior<Leaf> {
}
/// Show the title of a tab as a button.
///
/// You can override the default implementation to add e.g. a close button.
fn tab_ui(
&mut self,
nodes: &Nodes<Leaf>,
@@ -32,7 +44,7 @@ pub trait Behavior<Leaf> {
active: bool,
is_being_dragged: bool,
) -> Response {
let text = self.tab_text_for_node(nodes, node_id);
let text = self.tab_title_for_node(nodes, node_id);
let font_id = TextStyle::Button.resolve(ui.style());
let galley = text.into_galley(ui, Some(false), f32::INFINITY, font_id);
@@ -45,8 +57,8 @@ pub trait Behavior<Leaf> {
// Show a gap when dragged
if ui.is_rect_visible(rect) && !is_being_dragged {
let bg_color = self.tab_bg_color(ui.visuals(), active);
let stroke = self.tab_outline_stroke(ui.visuals(), active);
let bg_color = self.tab_bg_color(ui.visuals(), node_id, active);
let stroke = self.tab_outline_stroke(ui.visuals(), node_id, active);
ui.painter().rect(rect.shrink(0.5), 0.0, bg_color, stroke);
if active {
@@ -58,7 +70,7 @@ pub trait Behavior<Leaf> {
);
}
let text_color = self.tab_text_color(ui.visuals(), active);
let text_color = self.tab_text_color(ui.visuals(), node_id, active);
ui.painter().galley_with_color(
egui::Align2::CENTER_CENTER
.align_size_within_rect(galley.size(), rect)
@@ -76,7 +88,7 @@ pub trait Behavior<Leaf> {
true
}
/// Adds some UI to the top right of the tab bar.
/// Adds some UI to the top right of each tab bar.
///
/// You can use this to, for instance, add a button for adding new tabs.
///
@@ -89,7 +101,7 @@ pub trait Behavior<Leaf> {
// --------
// Settings:
/// The height of the bar holding tab names.
/// The height of the bar holding tab titles.
fn tab_bar_height(&self, _style: &egui::Style) -> f32 {
24.0
}
@@ -100,15 +112,17 @@ pub trait Behavior<Leaf> {
1.0
}
// No child should shrink below this size
/// No child should shrink below this width nor height.
fn min_size(&self) -> f32 {
32.0
}
/// What are the rules for simplifying the tree?
fn simplification_options(&self) -> SimplificationOptions {
SimplificationOptions::default()
}
/// The stroke used for the lines in horizontal, vertical, and grid layouts.
fn resize_stroke(&self, style: &egui::Style, resize_state: ResizeState) -> egui::Stroke {
match resize_state {
ResizeState::Idle => egui::Stroke::NONE, // Let the gap speak for itself
@@ -122,7 +136,7 @@ pub trait Behavior<Leaf> {
8.0
}
/// The background color of the tab bar
/// The background color of the tab bar.
fn tab_bar_color(&self, visuals: &Visuals) -> Color32 {
if visuals.dark_mode {
Color32::BLACK
@@ -131,7 +145,8 @@ pub trait Behavior<Leaf> {
}
}
fn tab_bg_color(&self, visuals: &Visuals, active: bool) -> Color32 {
/// The background color of a tab.
fn tab_bg_color(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Color32 {
if active {
visuals.window_fill() // same as the tab contents
} else {
@@ -140,7 +155,7 @@ pub trait Behavior<Leaf> {
}
/// Stroke of the outline around a tab title.
fn tab_outline_stroke(&self, visuals: &Visuals, active: bool) -> Stroke {
fn tab_outline_stroke(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Stroke {
if active {
Stroke::new(1.0, visuals.widgets.active.bg_fill)
} else {
@@ -153,11 +168,42 @@ pub trait Behavior<Leaf> {
Stroke::new(1.0, visuals.widgets.noninteractive.bg_stroke.color)
}
fn tab_text_color(&self, visuals: &Visuals, active: bool) -> Color32 {
/// The color of the title text of the tab.
fn tab_text_color(&self, visuals: &Visuals, _node_id: NodeId, active: bool) -> Color32 {
if active {
visuals.widgets.active.text_color()
} else {
visuals.widgets.noninteractive.text_color()
}
}
/// When drag-and-dropping a node, how do we preview what is about to happen?
fn paint_drag_preview(
&self,
visuals: &Visuals,
painter: &egui::Painter,
parent_rect: Option<Rect>,
preview_rect: Rect,
) {
let preview_stroke = visuals.selection.stroke;
let preview_color = preview_stroke.color;
if let Some(parent_rect) = parent_rect {
// Show which parent we will be dropped into
painter.rect_stroke(parent_rect, 1.0, preview_stroke);
}
painter.rect(
preview_rect,
1.0,
preview_color.gamma_multiply(0.5),
preview_stroke,
);
}
/// Show we preview leaves that are being dragged,
/// i.e. show their ui in the region where they will end up?
fn preview_dragged_leaves(&self) -> bool {
false
}
}

View File

@@ -68,8 +68,6 @@ impl<Leaf: std::fmt::Debug> std::fmt::Debug for Dock<Leaf> {
// ----------------------------------------------------------------------------
// Construction
impl<Leaf> Dock<Leaf> {
pub fn new(root: NodeId, nodes: Nodes<Leaf>) -> Self {
Self {
@@ -79,6 +77,10 @@ impl<Leaf> Dock<Leaf> {
}
}
pub fn root(&self) -> NodeId {
self.root
}
pub fn parent_of(&self, node_id: NodeId) -> Option<NodeId> {
self.nodes
.nodes
@@ -92,15 +94,10 @@ impl<Leaf> Dock<Leaf> {
})
.map(|(id, _)| *id)
}
}
// Usage
impl<Leaf> Dock<Leaf> {
pub fn root(&self) -> NodeId {
self.root
}
/// Show all the leaves in the dock.
/// Show the dock in the given [`Ui`].
///
/// The dock will use upp all the avilable space - nothing more, nothing less.
pub fn ui(&mut self, behavior: &mut dyn Behavior<Leaf>, ui: &mut Ui) {
let options = behavior.simplification_options();
self.simplify(&options);
@@ -133,6 +130,15 @@ impl<Leaf> Dock<Leaf> {
self.nodes
.node_ui(behavior, &mut drop_context, ui, self.root);
self.preview_dragged_node(behavior, &drop_context, ui);
}
fn preview_dragged_node(
&mut self,
behavior: &mut dyn Behavior<Leaf>,
drop_context: &DropContext,
ui: &mut Ui,
) {
if let (Some(mouse_pos), Some(dragged_node_id)) =
(drop_context.mouse_pos, drop_context.dragged_node_id)
{
@@ -147,8 +153,8 @@ impl<Leaf> Dock<Leaf> {
let mut frame = egui::Frame::popup(ui.style());
frame.fill = frame.fill.gamma_multiply(0.5); // Make see-through
frame.show(ui, |ui| {
// TODO: preview contents
let text = behavior.tab_text_for_node(&self.nodes, dragged_node_id);
// TODO(emilk): preview contents?
let text = behavior.tab_title_for_node(&self.nodes, dragged_node_id);
ui.label(text);
});
});
@@ -156,26 +162,14 @@ impl<Leaf> Dock<Leaf> {
if let Some(preview_rect) = drop_context.preview_rect {
let preview_rect = self.smooth_preview_rect(ui.ctx(), preview_rect);
let preview_stroke = ui.visuals().selection.stroke;
let preview_color = preview_stroke.color;
let parent_rect = drop_context
.best_insertion
.and_then(|insertion_point| self.nodes.try_rect(insertion_point.parent_id));
if let Some(insertion_point) = &drop_context.best_insertion {
if let Some(parent_rect) = self.nodes.try_rect(insertion_point.parent_id) {
// Show which parent we will be dropped into
ui.painter().rect_stroke(parent_rect, 1.0, preview_stroke);
}
}
behavior.paint_drag_preview(ui.visuals(), ui.painter(), parent_rect, preview_rect);
ui.painter().rect(
preview_rect,
1.0,
preview_color.gamma_multiply(0.5),
preview_stroke,
);
let preview_child = false;
if preview_child {
// Preview actual child?
if behavior.preview_dragged_leaves() {
// TODO(emilk): add support for previewing branches too.
if preview_rect.width() > 32.0 && preview_rect.height() > 32.0 {
if let Some(Node::Leaf(leaf)) = self.nodes.get_mut(dragged_node_id) {
let _ = behavior.leaf_ui(
@@ -200,6 +194,7 @@ impl<Leaf> Dock<Leaf> {
}
}
/// Take the preview rectangle and smooth it over time.
fn smooth_preview_rect(&mut self, ctx: &egui::Context, new_rect: Rect) -> Rect {
let dt = ctx.input(|input| input.stable_dt).at_most(0.1);
let t = egui::emath::exponential_smooth_factor(0.9, 0.05, dt);
@@ -228,7 +223,8 @@ impl<Leaf> Dock<Leaf> {
}
}
fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) {
/// Move the given node to the given insertion point.
pub fn move_node(&mut self, moved_node_id: NodeId, insertion_point: InsertionPoint) {
log::debug!(
"Moving {moved_node_id:?} into {:?}",
insertion_point.insertion
@@ -238,7 +234,7 @@ impl<Leaf> Dock<Leaf> {
}
/// Find the currently dragged node, if any.
fn dragged_id(&self, ctx: &egui::Context) -> Option<NodeId> {
pub fn dragged_id(&self, ctx: &egui::Context) -> Option<NodeId> {
if !is_possible_drag(ctx) {
// We're not sure we're dragging _at all_ yet.
return None;
@@ -246,7 +242,7 @@ impl<Leaf> Dock<Leaf> {
for &node_id in self.nodes.nodes.keys() {
if node_id == self.root {
continue; // now allowed to drag root
continue; // not allowed to drag root
}
let id = node_id.id();
@@ -264,9 +260,12 @@ impl<Leaf> Dock<Leaf> {
None
}
/// Performs no simplifcations!
fn remove_node_id_from_parent(&mut self, dragged_node_id: NodeId) {
self.nodes
.remove_node_id_from_parent(self.root, dragged_node_id);
/// Performs no simplifcations, nor does it remove the actual [`Node`].
pub fn remove_node_id_from_parent(&mut self, remove_me: NodeId) {
for parent in self.nodes.nodes.values_mut() {
if let Node::Branch(branch) = parent {
branch.retain(|child| child != remove_me);
}
}
}
}

View File

@@ -16,21 +16,36 @@
//! The user needs to implement this in order to specify the `ui` of each `Leaf` and
//! the tab name of leaves (if there are tab nodes).
//!
//! ## Implementation notes
//! In many places we want to recursively visit all noted, while also mutating them.
//! In order to not get into trouble with the borrow checker a trick is used:
//! each [`Node`] is removed, mutated, recursed, and then re-added.
//! You'll see this pattern many times reading the following code.
//!
//! ## Shortcomings
//! We use real recursion, so if your trees get too deep you will get a stack overflow.
//!
//!
//! ## Future improvements
//! * A new ui for each node, nested
//! * Per-tab close-buttons
//! * Easy per-tab close-buttons
//! * Scrolling of tab-bar
//! * Vertical tab bar
//! * Auto-grid layouts (re-arange as parent is resized)
//! * Auto-join nested horizontal/vertical layouts in the simplify step
// ## Implementation notes
// In many places we want to recursively visit all noted, while also mutating them.
// In order to not get into trouble with the borrow checker a trick is used:
// each [`Node`] is removed, mutated, recursed, and then re-added.
// You'll see this pattern many times reading the following code.
//
// Each frame consists of two passes: layout, and ui.
// The layout pass figures out where each node should be placed.
// The ui pass does all the painting.
// These two passes could be combined into one pass if we wanted to,
// but having them split up makes the code slightly simpler, and
// leaves the door open for more complex layout (e.g. min/max sizes per node).
//
// Everything is quite dynamic, so we have a bunch of defensive coding that call `warn!` on failure.
// These situations should not happen in normal use, but could happen if the user messes with
// the internals of the tree, putting it in an invalid state.
//
// ## TODO before release:
// * Auto-grid layouts (re-arange as parent is resized)
// * Clip tab titles to not cover "add new tab" button
use egui::{Id, Pos2, Rect};
@@ -96,6 +111,7 @@ pub enum UiResponse {
DragStarted,
}
/// What are the rules for simplifying the tree?
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SimplificationOptions {
pub prune_empty_tabs: bool,
@@ -143,7 +159,7 @@ pub struct InsertionPoint {
}
impl InsertionPoint {
fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self {
pub fn new(parent_id: NodeId, insertion: LayoutInsertion) -> Self {
Self {
parent_id,
insertion,
@@ -185,7 +201,14 @@ struct DropContext {
}
impl DropContext {
fn on_node<Leaf>(&mut self, parent_id: NodeId, rect: Rect, node: &Node<Leaf>) {
fn on_node<Leaf>(
&mut self,
behavior: &mut dyn Behavior<Leaf>,
style: &egui::Style,
parent_id: NodeId,
rect: Rect,
node: &Node<Leaf>,
) {
if !self.enabled {
return;
}
@@ -212,17 +235,18 @@ impl DropContext {
);
}
// self.suggest_rect(InsertionPoint::new(parent_id, LayoutType::Tabs, 1), rect);
self.suggest_rect(
InsertionPoint::new(parent_id, LayoutInsertion::Tabs(usize::MAX)),
rect.split_top_bottom_at_y(rect.top() + behavior.tab_bar_height(style))
.1,
);
}
fn suggest_rect(&mut self, insertion: InsertionPoint, preview_rect: Rect) {
self.suggest_point(insertion, preview_rect.center(), preview_rect);
}
fn suggest_point(&mut self, insertion: InsertionPoint, target_point: Pos2, preview_rect: Rect) {
if !self.enabled {
return;
}
let target_point = preview_rect.center();
if let Some(mouse_pos) = self.mouse_pos {
let dist_sq = mouse_pos.distance_sq(target_point);
if dist_sq < self.best_dist_sq {

View File

@@ -14,7 +14,7 @@ pub struct Nodes<Leaf> {
/// Filled in by the layout step at the start of each frame.
#[serde(default, skip)]
pub rects: HashMap<NodeId, Rect>,
pub(super) rects: HashMap<NodeId, Rect>,
}
impl<Leaf> Default for Nodes<Leaf> {
@@ -29,11 +29,11 @@ impl<Leaf> Default for Nodes<Leaf> {
// ----------------------------------------------------------------------------
impl<Leaf> Nodes<Leaf> {
pub fn try_rect(&self, node_id: NodeId) -> Option<Rect> {
pub(super) fn try_rect(&self, node_id: NodeId) -> Option<Rect> {
self.rects.get(&node_id).copied()
}
pub fn rect(&self, node_id: NodeId) -> Rect {
pub(super) fn rect(&self, node_id: NodeId) -> Rect {
let rect = self.try_rect(node_id);
debug_assert!(rect.is_some(), "Failed to find rect for {node_id:?}");
rect.unwrap_or(egui::Rect::from_min_max(Pos2::ZERO, Pos2::ZERO))
@@ -90,23 +90,6 @@ impl<Leaf> Nodes<Leaf> {
self.insert_node(Node::Branch(Branch::new_grid(children)))
}
/// Performs no simplifcations!
/// // TODO: remove ?
pub(super) fn remove_node_id_from_parent(&mut self, it: NodeId, remove: NodeId) -> GcAction {
if it == remove {
return GcAction::Remove;
}
let Some(mut node) = self.nodes.remove(&it) else {
log::warn!("Unexpected missing node during removal");
return GcAction::Remove;
};
if let Node::Branch(branch) = &mut node {
branch.retain(|child| self.remove_node_id_from_parent(child, remove) == GcAction::Keep);
}
self.nodes.insert(it, node);
GcAction::Keep
}
pub fn insert(&mut self, insertion_point: InsertionPoint, child_id: NodeId) {
let InsertionPoint {
parent_id,
@@ -257,7 +240,7 @@ impl<Leaf> Nodes<Leaf> {
ui: &mut Ui,
node_id: NodeId,
) {
// NOTE: important that we get thr rect and node in two steps,
// NOTE: important that we get the rect and node in two steps,
// otherwise we could loose the node when there is no rect.
let Some(rect) = self.try_rect(node_id) else {
log::warn!("Failed to find rect for node {node_id:?} during ui");
@@ -273,14 +256,9 @@ impl<Leaf> Nodes<Leaf> {
// Can't drag a node onto self or any children
drop_context.enabled = false;
}
drop_context.on_node(node_id, rect, &node);
drop_context.suggest_rect(
InsertionPoint::new(node_id, LayoutInsertion::Tabs(usize::MAX)),
rect.split_top_bottom_at_y(rect.top() + behavior.tab_bar_height(ui.style()))
.1,
);
drop_context.on_node(behavior, ui.style(), node_id, rect, &node);
// Each node gets its own `Ui`, nested inside each other, with proper clip rectangles.
let mut ui = egui::Ui::new(
ui.ctx().clone(),
ui.layer_id(),
@@ -314,7 +292,7 @@ impl<Leaf> Nodes<Leaf> {
};
if let Node::Branch(branch) = &mut node {
// TODO: join nested versions of the same horizontal/vertical layouts
// TODO(emilk): join nested versions of the same horizontal/vertical layouts
branch.simplify_children(|child| self.simplify(options, child));

View File

@@ -119,7 +119,7 @@ impl dock::Behavior<View> for DockBehavior {
view.ui(ui)
}
fn tab_text_for_leaf(&mut self, view: &View) -> egui::WidgetText {
fn tab_title_for_leaf(&mut self, view: &View) -> egui::WidgetText {
format!("View {}", view.nr).into()
}
@@ -249,7 +249,7 @@ fn tree_ui(
// Get the name BEFORE we remove the node below!
let text = format!(
"{} - {node_id:?}",
behavior.tab_text_for_node(nodes, node_id).text()
behavior.tab_title_for_node(nodes, node_id).text()
);
let Some(mut node) = nodes.nodes.remove(&node_id) else {