mirror of
https://github.com/emilk/egui.git
synced 2026-06-27 07:03:14 -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();
|
||||
}
|
||||
|
||||
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_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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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)]
|
||||
pub enum ResizeState {
|
||||
Idle,
|
||||
|
||||
@@ -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<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 {
|
||||
#[inline]
|
||||
fn from(range: RangeInclusive<f32>) -> Self {
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user