From f2d6885a6beeafdd779a247486168498da0b1970 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 4 May 2023 15:06:24 +0200 Subject: [PATCH] Nicer styling of tabs --- crates/egui_extras/src/dock/behavior.rs | 139 +++++++++++++++++++++ crates/egui_extras/src/dock/branch/tabs.rs | 95 ++++++++------ crates/egui_extras/src/dock/mod.rs | 88 ++----------- crates/emath/src/range.rs | 26 ++++ crates/emath/src/rect.rs | 2 +- 5 files changed, 230 insertions(+), 120 deletions(-) create mode 100644 crates/egui_extras/src/dock/behavior.rs diff --git a/crates/egui_extras/src/dock/behavior.rs b/crates/egui_extras/src/dock/behavior.rs new file mode 100644 index 000000000..a0638e942 --- /dev/null +++ b/crates/egui_extras/src/dock/behavior.rs @@ -0,0 +1,139 @@ +use egui::{vec2, Color32, Id, Response, Rgba, Sense, Stroke, TextStyle, Ui, Visuals, WidgetText}; + +use super::{Node, NodeId, Nodes, ResizeState, SimplificationOptions, UiResponse}; + +/// Trait defining how the [`Dock`] and its leaf should be shown. +pub trait Behavior { + /// 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 Ui, _node_id: NodeId, _leaf: &mut Leaf) -> UiResponse; + + fn tab_text_for_leaf(&mut self, leaf: &Leaf) -> WidgetText; + + fn tab_text_for_node(&mut self, nodes: &Nodes, node_id: NodeId) -> WidgetText { + match &nodes.nodes[&node_id] { + Node::Leaf(leaf) => self.tab_text_for_leaf(leaf), + Node::Branch(branch) => format!("{:?}", branch.get_layout()).into(), + } + } + + /// Show the title of a tab as a button. + fn tab_ui( + &mut self, + nodes: &Nodes, + ui: &mut Ui, + id: Id, + node_id: NodeId, + active: bool, + is_being_dragged: bool, + ) -> Response { + let text = self.tab_text_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); + let (_, rect) = ui.allocate_space(galley.size()); + let response = ui.interact(rect, id, Sense::click_and_drag()); + + // Show a gap when dragged + if ui.is_rect_visible(rect) && !is_being_dragged { + { + let mut bg_rect = rect; + bg_rect.min.y = ui.max_rect().min.y; + bg_rect.max.y = ui.max_rect().max.y; + bg_rect = bg_rect.expand2(vec2(0.5 * ui.spacing().item_spacing.x, 0.0)); + + let bg_color = self.tab_bg_color(ui.visuals(), active); + let stroke = self.tab_outline_stroke(ui.visuals(), active); + ui.painter().rect(bg_rect, 0.0, bg_color, stroke); + + if active { + // Make the tab name area connect with the tab ui area: + ui.painter().hline( + bg_rect.x_range(), + bg_rect.bottom(), + Stroke::new(stroke.width + 1.0, bg_color), + ); + } + } + + let text_color = self.tab_text_color(ui.visuals(), active); + ui.painter() + .galley_with_color(rect.min, galley.galley, 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, + /// and between rows/columns in a grid layout. + fn gap_width(&self, _style: &egui::Style) -> f32 { + 1.0 + } + + // No child should shrink below this size + fn min_size(&self) -> f32 { + 32.0 + } + + fn simplification_options(&self) -> SimplificationOptions { + SimplificationOptions::default() + } + + 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 + ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke, + ResizeState::Dragging => style.visuals.widgets.active.fg_stroke, + } + } + + /// The background color of the tab bar + fn tab_bar_color(&self, visuals: &Visuals) -> Color32 { + (Rgba::from(visuals.window_fill()) * Rgba::from_gray(0.7)).into() + } + + fn tab_bg_color(&self, visuals: &Visuals, active: bool) -> Color32 { + if active { + // blend it with the tab contents: + visuals.window_fill() + } else { + // fade into background: + self.tab_bar_color(visuals) + } + } + + /// Stroke of the outline around a tab title. + fn tab_outline_stroke(&self, visuals: &Visuals, active: bool) -> Stroke { + if active { + Stroke::new(1.0, visuals.widgets.active.bg_fill) + } else { + Stroke::NONE + } + } + + /// Stroke of the line separating the tab title bar and the content of the active tab. + fn tab_bar_hline_stroke(&self, visuals: &Visuals) -> Stroke { + Stroke::new(1.0, visuals.widgets.noninteractive.bg_stroke.color) + } + + fn tab_text_color(&self, visuals: &Visuals, active: bool) -> Color32 { + if active { + visuals.widgets.active.text_color() + } else { + visuals.widgets.noninteractive.text_color() + } + } +} diff --git a/crates/egui_extras/src/dock/branch/tabs.rs b/crates/egui_extras/src/dock/branch/tabs.rs index fc11834a4..8c1725eae 100644 --- a/crates/egui_extras/src/dock/branch/tabs.rs +++ b/crates/egui_extras/src/dock/branch/tabs.rs @@ -47,16 +47,40 @@ impl Tabs { self.active = self.children.first().copied().unwrap_or_default(); } + let next_active = self.tab_bar_ui(behavior, ui, rect, nodes, drop_context, node_id); + + // When dragged, don't show it (it is "being held") + let is_active_being_dragged = + ui.memory(|mem| mem.is_being_dragged(self.active.id())) && is_possible_drag(ui.ctx()); + if !is_active_being_dragged { + nodes.node_ui(behavior, drop_context, ui, self.active); + } + + // We have only laid out the active tab, so we need to switch active tab after the ui pass: + self.active = next_active; + } + + fn tab_bar_ui( + &mut self, + behavior: &mut dyn Behavior, + ui: &mut egui::Ui, + rect: Rect, + nodes: &mut Nodes, + drop_context: &mut DropContext, + node_id: NodeId, + ) -> NodeId { + let mut next_active = self.active; + let tab_bar_height = behavior.tab_bar_height(ui.style()); let tab_bar_rect = rect.split_top_bottom_at_y(rect.top() + tab_bar_height).0; let mut tab_bar_ui = ui.child_ui(tab_bar_rect, *ui.layout()); - let mut next_active = self.active; + let mut button_rects = HashMap::new(); + let mut dragged_index = None; - // Show tab bar: tab_bar_ui.horizontal(|ui| { - let mut button_rects = HashMap::new(); - let mut dragged_index = None; + ui.painter() + .rect_filled(ui.max_rect(), 0.0, behavior.tab_bar_color(ui.visuals())); for (i, &child_id) in self.children.iter().enumerate() { let is_being_dragged = is_being_dragged(ui.ctx(), child_id); @@ -82,42 +106,37 @@ impl Tabs { dragged_index = Some(i); } } - - let preview_thickness = 6.0; - let after_rect = |rect: Rect| { - let dragged_size = if let Some(dragged_index) = dragged_index { - // We actually know the size of this thing - button_rects[&self.children[dragged_index]].size() - } else { - rect.size() // guess that the size is the same as the last button - }; - Rect::from_min_size( - rect.right_top() + vec2(ui.spacing().item_spacing.x, 0.0), - dragged_size, - ) - }; - super::linear::drop_zones( - preview_thickness, - &self.children, - dragged_index, - super::LinearDir::Horizontal, - |node_id| button_rects[&node_id], - |rect, i| { - drop_context - .suggest_rect(InsertionPoint::new(node_id, LayoutInsertion::Tabs(i)), rect); - }, - after_rect, - ); }); - // When dragged, don't show it (it is "being held") - let is_active_being_dragged = - ui.memory(|mem| mem.is_being_dragged(self.active.id())) && is_possible_drag(ui.ctx()); - if !is_active_being_dragged { - nodes.node_ui(behavior, drop_context, ui, self.active); - } + // ----------- + // Drop zones: - // We have only laid out the active tab, so we need to switch active tab after the ui pass: - self.active = next_active; + let preview_thickness = 6.0; + let after_rect = |rect: Rect| { + let dragged_size = if let Some(dragged_index) = dragged_index { + // We actually know the size of this thing + button_rects[&self.children[dragged_index]].size() + } else { + rect.size() // guess that the size is the same as the last button + }; + Rect::from_min_size( + rect.right_top() + vec2(ui.spacing().item_spacing.x, 0.0), + dragged_size, + ) + }; + super::linear::drop_zones( + preview_thickness, + &self.children, + dragged_index, + super::LinearDir::Horizontal, + |node_id| button_rects[&node_id], + |rect, i| { + drop_context + .suggest_rect(InsertionPoint::new(node_id, LayoutInsertion::Tabs(i)), rect); + }, + after_rect, + ); + + next_active } } diff --git a/crates/egui_extras/src/dock/mod.rs b/crates/egui_extras/src/dock/mod.rs index 1897c67e4..987060e27 100644 --- a/crates/egui_extras/src/dock/mod.rs +++ b/crates/egui_extras/src/dock/mod.rs @@ -1,13 +1,19 @@ // # TODO // * A new ui for each node, nested // * Styling +// * Per-tab close-buttons +// * Scrolling of tab-bar +// * Adding extra stuff at the end of the tab-bar (e.g. an "Add new tab" button) +// * Vertical tab bar use std::collections::{HashMap, HashSet}; -use egui::{Id, Key, NumExt, Pos2, Rect, Response, Sense, TextStyle, Ui, WidgetText}; +use egui::{Id, Key, NumExt, Pos2, Rect, Ui}; +mod behavior; mod branch; +pub use behavior::Behavior; pub use branch::{Branch, Grid, GridLoc, Layout, Linear, LinearDir, Tabs}; // ---------------------------------------------------------------------------- @@ -149,86 +155,6 @@ impl Default for SimplificationOptions { } } -/// Trait defining how the [`Dock`] and its leaf should be shown. -pub trait Behavior { - /// 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 Ui, _node_id: NodeId, _leaf: &mut Leaf) -> UiResponse; - - fn tab_text_for_leaf(&mut self, leaf: &Leaf) -> WidgetText; - - fn tab_text_for_node(&mut self, nodes: &Nodes, node_id: NodeId) -> WidgetText { - match &nodes.nodes[&node_id] { - Node::Leaf(leaf) => self.tab_text_for_leaf(leaf), - Node::Branch(branch) => format!("{:?}", branch.get_layout()).into(), - } - } - - fn tab_ui( - &mut self, - nodes: &Nodes, - ui: &mut Ui, - id: Id, - node_id: NodeId, - selected: bool, - is_being_dragged: bool, - ) -> Response { - let text = self.tab_text_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); - let (_, rect) = ui.allocate_space(galley.size()); - let response = ui.interact(rect, id, Sense::click_and_drag()); - let widget_style = ui.style().interact_selectable(&response, selected); - - // Show a gap when dragged - if ui.is_rect_visible(rect) && !is_being_dragged { - if selected { - ui.painter().rect_filled(rect, 0.0, widget_style.bg_fill); - } - 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 - } - - // No child should shrink below this size - fn min_size(&self) -> f32 { - 32.0 - } - - fn simplification_options(&self) -> SimplificationOptions { - SimplificationOptions::default() - } - - 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 - ResizeState::Hovering => style.visuals.widgets.hovered.fg_stroke, - ResizeState::Dragging => style.visuals.widgets.active.fg_stroke, - } - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum ResizeState { Idle, diff --git a/crates/emath/src/range.rs b/crates/emath/src/range.rs index 2a455caa4..ae3e3f0e5 100644 --- a/crates/emath/src/range.rs +++ b/crates/emath/src/range.rs @@ -11,6 +11,25 @@ pub struct Rangef { } impl Rangef { + /// Infinite range that contains everything, from -∞ to +∞, inclusive. + pub const EVERYTHING: Self = Self { + min: f32::NEG_INFINITY, + max: f32::INFINITY, + }; + + /// The inverse of [`Self::EVERYTHING`]: stretches from positive infinity to negative infinity. + /// Contains nothing. + pub const NOTHING: Self = Self { + min: f32::INFINITY, + max: f32::NEG_INFINITY, + }; + + /// An invalid [`Rangef`] filled with [`f32::NAN`]. + pub const NAN: Self = Self { + min: f32::NAN, + max: f32::NAN, + }; + #[inline] pub fn new(min: f32, max: f32) -> Self { Self { min, max } @@ -34,6 +53,13 @@ impl From for RangeInclusive { } } +impl From<&Rangef> for RangeInclusive { + #[inline] + fn from(&Rangef { min, max }: &Rangef) -> Self { + min..=max + } +} + impl From> for Rangef { #[inline] fn from(range: RangeInclusive) -> Self { diff --git a/crates/emath/src/rect.rs b/crates/emath/src/rect.rs index c5c737fb2..b8c6085a2 100644 --- a/crates/emath/src/rect.rs +++ b/crates/emath/src/rect.rs @@ -53,7 +53,7 @@ impl Rect { max: pos2(-INFINITY, -INFINITY), }; - /// An invalid [`Rect`] filled with [`f32::NAN`]; + /// An invalid [`Rect`] filled with [`f32::NAN`]. pub const NAN: Self = Self { min: pos2(f32::NAN, f32::NAN), max: pos2(f32::NAN, f32::NAN),