Browse Source

Nicer styling of tabs

emilk/dock
Emil Ernerfeldt 2 years ago
parent
commit
f2d6885a6b
  1. 139
      crates/egui_extras/src/dock/behavior.rs
  2. 95
      crates/egui_extras/src/dock/branch/tabs.rs
  3. 88
      crates/egui_extras/src/dock/mod.rs
  4. 26
      crates/emath/src/range.rs
  5. 2
      crates/emath/src/rect.rs

139
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<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()
}
}
}

95
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<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
}
}

88
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<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,

26
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<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 {

2
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),

Loading…
Cancel
Save