mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 14:20:04 -04:00
Nicer styling of tabs
This commit is contained in:
139
crates/egui_extras/src/dock/behavior.rs
Normal file
139
crates/egui_extras/src/dock/behavior.rs
Normal file
@@ -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<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 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<Leaf>, 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<Leaf>,
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,16 +47,40 @@ impl Tabs {
|
|||||||
self.active = self.children.first().copied().unwrap_or_default();
|
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<Leaf>(
|
||||||
|
&mut self,
|
||||||
|
behavior: &mut dyn Behavior<Leaf>,
|
||||||
|
ui: &mut egui::Ui,
|
||||||
|
rect: Rect,
|
||||||
|
nodes: &mut Nodes<Leaf>,
|
||||||
|
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_height = behavior.tab_bar_height(ui.style());
|
||||||
let tab_bar_rect = rect.split_top_bottom_at_y(rect.top() + tab_bar_height).0;
|
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 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| {
|
tab_bar_ui.horizontal(|ui| {
|
||||||
let mut button_rects = HashMap::new();
|
ui.painter()
|
||||||
let mut dragged_index = None;
|
.rect_filled(ui.max_rect(), 0.0, behavior.tab_bar_color(ui.visuals()));
|
||||||
|
|
||||||
for (i, &child_id) in self.children.iter().enumerate() {
|
for (i, &child_id) in self.children.iter().enumerate() {
|
||||||
let is_being_dragged = is_being_dragged(ui.ctx(), child_id);
|
let is_being_dragged = is_being_dragged(ui.ctx(), child_id);
|
||||||
@@ -82,42 +106,37 @@ impl Tabs {
|
|||||||
dragged_index = Some(i);
|
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 =
|
// Drop zones:
|
||||||
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:
|
let preview_thickness = 6.0;
|
||||||
self.active = next_active;
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
// # TODO
|
// # TODO
|
||||||
// * A new ui for each node, nested
|
// * A new ui for each node, nested
|
||||||
// * Styling
|
// * 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 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;
|
mod branch;
|
||||||
|
|
||||||
|
pub use behavior::Behavior;
|
||||||
pub use branch::{Branch, Grid, GridLoc, Layout, Linear, LinearDir, Tabs};
|
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<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 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<Leaf>, 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<Leaf>,
|
|
||||||
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)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum ResizeState {
|
pub enum ResizeState {
|
||||||
Idle,
|
Idle,
|
||||||
|
|||||||
@@ -11,6 +11,25 @@ pub struct Rangef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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]
|
#[inline]
|
||||||
pub fn new(min: f32, max: f32) -> Self {
|
pub fn new(min: f32, max: f32) -> Self {
|
||||||
Self { min, max }
|
Self { min, max }
|
||||||
@@ -34,6 +53,13 @@ impl From<Rangef> for RangeInclusive<f32> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<&Rangef> for RangeInclusive<f32> {
|
||||||
|
#[inline]
|
||||||
|
fn from(&Rangef { min, max }: &Rangef) -> Self {
|
||||||
|
min..=max
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<RangeInclusive<f32>> for Rangef {
|
impl From<RangeInclusive<f32>> for Rangef {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn from(range: RangeInclusive<f32>) -> Self {
|
fn from(range: RangeInclusive<f32>) -> Self {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ impl Rect {
|
|||||||
max: pos2(-INFINITY, -INFINITY),
|
max: pos2(-INFINITY, -INFINITY),
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An invalid [`Rect`] filled with [`f32::NAN`];
|
/// An invalid [`Rect`] filled with [`f32::NAN`].
|
||||||
pub const NAN: Self = Self {
|
pub const NAN: Self = Self {
|
||||||
min: pos2(f32::NAN, f32::NAN),
|
min: pos2(f32::NAN, f32::NAN),
|
||||||
max: pos2(f32::NAN, f32::NAN),
|
max: pos2(f32::NAN, f32::NAN),
|
||||||
|
|||||||
Reference in New Issue
Block a user