diff --git a/CHANGELOG.md b/CHANGELOG.md index 617a6f9..6d0e921 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,45 @@ # egui_dock changelog +## 0.7.0 - (to be determined) + +### Changed + +- Adjusted the styling of tabs to closer follow the egui default styling. ([#139](https://github.com/Adanos020/egui_dock/pull/139)) +- Double-clicking on a separator resets the size of both adjacent nodes. ([#146](https://github.com/Adanos020/egui_dock/pull/146)) + +### Fixed + +- Correctly draw a border around a dock area using the `Style::border` property. ([#139](https://github.com/Adanos020/egui_dock/pull/139)) + +### Added + +- From [#139](https://github.com/Adanos020/egui_dock/pull/139): + - `Style::rounding` for the rounding of the dock area border. + - `TabStyle::active` for the active style of a tab. + - `TabStyle::inactive` for the inactive style of a tab. + - `TabStyle::focused` for the focused style of a tab. + - `TabStyle::hovered` for the hovered style of a tab. + - `TabStyle::tab_body` for styling the body of the tab including background color, stroke color, rounding and inner margin. + - `TabStyle::minimum_width` to set the minimum width of the tab. + - `TabInteractionStyle` to style the active/inactive/focused/hovered states of a tab. +- `AllowedSplits` enum which lets you choose in which directions a `DockArea` can be split. ([#145](https://github.com/Adanos020/egui_dock/pull/145)) +- `TabViewer::closable` lets individual tabs be closable or not. ([#150](https://github.com/Adanos020/egui_dock/pull/150)) + + +### Breaking changes + +- From [#139](https://github.com/Adanos020/egui_dock/pull/139): + - Moved `TabStyle::inner_margin` to `TabBodyStyle::inner_margin`. + - Moved `TabStyle::fill_tab_bar` to `TabBarStyle::fill_tab_bar`. + - Moved `TabStyle::outline_color` to `TabInteractionStyle::outline_color`. + - Moved `TabStyle::rounding` to `TabInteractionStyle::rounding`. + - Moved `TabStyle::bg_fill` to `TabInteractionStyle::bg_fill`. + - Moved `TabStyle::text_color_unfocused` to `TabStyle::inactive.text_color`. + - Moved `TabStyle::text_color_active_focused` to `TabStyle::focused.text_color`. + - Moved `TabStyle::text_color_active_unfocused` to `TabStyle::active.text_color`. + - Renamed `Style::tabs` to `Style::tab`. + - Removed `TabStyle::text_color_focused`. This style was practically never reachable. + ## 0.6.3 - 2023-06-16 ### Fixed diff --git a/Cargo.toml b/Cargo.toml index 637648c..c96a939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "egui_dock" description = "Docking support for `egui` - an immediate-mode GUI library for Rust" authors = ["lain-dono", "Adam Gąsior (Adanos020)"] -version = "0.6.3" +version = "0.7.0" edition = "2021" rust-version = "1.65" license = "MIT" diff --git a/README.md b/README.md index ed43e48..f505f1b 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Add `egui` and `egui_dock` to your project's dependencies. ```toml [dependencies] egui = "0.22" -egui_dock = "0.6" +egui_dock = "0.7" ``` Then proceed by setting up `egui`, following its [quick start guide](https://github.com/emilk/egui#quick-start). diff --git a/examples/hello.rs b/examples/hello.rs index a8bb0f6..e76bfc4 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -8,7 +8,9 @@ use egui::{ CentralPanel, ComboBox, Frame, Slider, TopBottomPanel, Ui, WidgetText, }; -use egui_dock::{DockArea, Node, NodeIndex, Style, TabViewer, Tree}; +use egui_dock::{ + AllowedSplits, DockArea, Node, NodeIndex, Style, TabInteractionStyle, TabViewer, Tree, +}; fn main() -> eframe::Result<()> { let options = NativeOptions { @@ -32,6 +34,7 @@ struct MyContext { show_add_buttons: bool, draggable_tabs: bool, show_tab_name_on_hover: bool, + allowed_splits: AllowedSplits, } struct MyApp { @@ -52,7 +55,7 @@ impl TabViewer for MyContext { } } - fn context_menu(&mut self, ui: &mut Ui, tab: &mut Self::Tab) { + fn context_menu(&mut self, ui: &mut Ui, tab: &mut Self::Tab, _node: NodeIndex) { match tab.as_str() { "Simple Demo" => self.simple_demo_menu(ui), _ => { @@ -66,6 +69,10 @@ impl TabViewer for MyContext { tab.as_str().into() } + fn closeable(&mut self, tab: &mut Self::Tab) -> bool { + ["Inspector", "Style Editor"].contains(&tab.as_str()) + } + fn on_close(&mut self, tab: &mut Self::Tab) -> bool { self.open_tabs.remove(tab); true @@ -102,6 +109,22 @@ impl MyContext { ui.checkbox(&mut self.show_add_buttons, "Show add buttons"); ui.checkbox(&mut self.draggable_tabs, "Draggable tabs"); ui.checkbox(&mut self.show_tab_name_on_hover, "Show tab name on hover"); + ComboBox::new("cbox:allowed_splits", "Split direction(s)") + .selected_text(format!("{:?}", self.allowed_splits)) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.allowed_splits, AllowedSplits::All, "All"); + ui.selectable_value( + &mut self.allowed_splits, + AllowedSplits::LeftRightOnly, + "LeftRightOnly", + ); + ui.selectable_value( + &mut self.allowed_splits, + AllowedSplits::TopBottomOnly, + "TopBottomOnly", + ); + ui.selectable_value(&mut self.allowed_splits, AllowedSplits::None, "None"); + }); }); let style = self.style.as_mut().unwrap(); @@ -160,18 +183,15 @@ impl MyContext { ui.collapsing("Tabs", |ui| { ui.separator(); - ui.checkbox(&mut style.tabs.fill_tab_bar, "Expand tabs"); - ui.checkbox( - &mut style.tabs.hline_below_active_tab_name, - "Show a line below the active tab name", - ); - - ui.separator(); - + ui.checkbox(&mut style.tab_bar.fill_tab_bar, "Expand tabs"); ui.checkbox( &mut style.tab_bar.show_scroll_bar_on_overflow, "Show scroll bar on tab overflow", ); + ui.checkbox( + &mut style.tab.hline_below_active_tab_name, + "Show a line below the active tab name", + ); ui.horizontal(|ui| { ui.add(Slider::new(&mut style.tab_bar.height, 20.0..=50.0)); ui.label("Tab bar height"); @@ -191,51 +211,64 @@ impl MyContext { ui.separator(); - ui.label("Rounding"); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tabs.rounding.nw, 0.0..=15.0)); - ui.label("North-West"); + fn tab_style_editor_ui(ui: &mut Ui, tab_style: &mut TabInteractionStyle) { + ui.separator(); + + ui.label("Rounding"); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut tab_style.rounding.nw, 0.0..=15.0)); + ui.label("North-West"); + }); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut tab_style.rounding.ne, 0.0..=15.0)); + ui.label("North-East"); + }); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut tab_style.rounding.sw, 0.0..=15.0)); + ui.label("South-West"); + }); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut tab_style.rounding.se, 0.0..=15.0)); + ui.label("South-East"); + }); + + ui.separator(); + + egui::Grid::new("tabs_colors").show(ui, |ui| { + ui.label("Title text color:"); + color_edit_button_srgba(ui, &mut tab_style.text_color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Outline color:") + .on_hover_text("The outline around the active tab name."); + color_edit_button_srgba(ui, &mut tab_style.outline_color, Alpha::OnlyBlend); + ui.end_row(); + + ui.label("Background color:"); + color_edit_button_srgba(ui, &mut tab_style.bg_fill, Alpha::OnlyBlend); + ui.end_row(); + }); + } + + ui.collapsing("Active", |ui| { + tab_style_editor_ui(ui, &mut style.tab.active); }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tabs.rounding.ne, 0.0..=15.0)); - ui.label("North-East"); + + ui.collapsing("Inactive", |ui| { + tab_style_editor_ui(ui, &mut style.tab.inactive); }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tabs.rounding.sw, 0.0..=15.0)); - ui.label("South-West"); + + ui.collapsing("Focused", |ui| { + tab_style_editor_ui(ui, &mut style.tab.focused); }); - ui.horizontal(|ui| { - ui.add(Slider::new(&mut style.tabs.rounding.se, 0.0..=15.0)); - ui.label("South-East"); + + ui.collapsing("Hovered", |ui| { + tab_style_editor_ui(ui, &mut style.tab.hovered); }); ui.separator(); egui::Grid::new("tabs_colors").show(ui, |ui| { - ui.label("Title text color, inactive and unfocused:"); - color_edit_button_srgba(ui, &mut style.tabs.text_color_unfocused, Alpha::OnlyBlend); - ui.end_row(); - - ui.label("Title text color, inactive and focused:"); - color_edit_button_srgba(ui, &mut style.tabs.text_color_focused, Alpha::OnlyBlend); - ui.end_row(); - - ui.label("Title text color, active and unfocused:"); - color_edit_button_srgba( - ui, - &mut style.tabs.text_color_active_unfocused, - Alpha::OnlyBlend, - ); - ui.end_row(); - - ui.label("Title text color, active and focused:"); - color_edit_button_srgba( - ui, - &mut style.tabs.text_color_active_focused, - Alpha::OnlyBlend, - ); - ui.end_row(); - ui.label("Close button color unfocused:"); color_edit_button_srgba(ui, &mut style.buttons.close_tab_color, Alpha::OnlyBlend); ui.end_row(); @@ -256,19 +289,49 @@ impl MyContext { color_edit_button_srgba(ui, &mut style.tab_bar.bg_fill, Alpha::OnlyBlend); ui.end_row(); - ui.label("Outline color:") - .on_hover_text("The outline around the active tab name."); - color_edit_button_srgba(ui, &mut style.tabs.outline_color, Alpha::OnlyBlend); - ui.end_row(); - ui.label("Horizontal line color:").on_hover_text( "The line separating the tab name area from the tab content area", ); color_edit_button_srgba(ui, &mut style.tab_bar.hline_color, Alpha::OnlyBlend); ui.end_row(); + }); + }); + + ui.collapsing("Tab body", |ui| { + ui.separator(); + + ui.label("Rounding"); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut style.tab.tab_body.rounding.nw, 0.0..=15.0)); + ui.label("North-West"); + }); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut style.tab.tab_body.rounding.ne, 0.0..=15.0)); + ui.label("North-East"); + }); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut style.tab.tab_body.rounding.sw, 0.0..=15.0)); + ui.label("South-West"); + }); + ui.horizontal(|ui| { + ui.add(Slider::new(&mut style.tab.tab_body.rounding.se, 0.0..=15.0)); + ui.label("South-East"); + }); + + ui.label("Stroke width:"); + ui.add(Slider::new( + &mut style.tab.tab_body.stroke.width, + 0.0..=10.0, + )); + ui.end_row(); + + egui::Grid::new("tab_body_colors").show(ui, |ui| { + ui.label("Stroke color:"); + color_edit_button_srgba(ui, &mut style.tab.tab_body.stroke.color, Alpha::OnlyBlend); + ui.end_row(); ui.label("Background color:"); - color_edit_button_srgba(ui, &mut style.tabs.bg_fill, Alpha::OnlyBlend); + color_edit_button_srgba(ui, &mut style.tab.tab_body.bg_fill, Alpha::OnlyBlend); ui.end_row(); }); }); @@ -305,6 +368,7 @@ impl Default for MyApp { show_add_buttons: false, draggable_tabs: true, show_tab_name_on_hover: false, + allowed_splits: AllowedSplits::default(), }; Self { context, tree } @@ -353,6 +417,7 @@ impl eframe::App for MyApp { .show_add_buttons(self.context.show_add_buttons) .draggable_tabs(self.context.draggable_tabs) .show_tab_name_on_hover(self.context.show_tab_name_on_hover) + .allowed_splits(self.context.allowed_splits) .show_inside(ui, &mut self.context); }); } diff --git a/examples/tab_add.rs b/examples/tab_add.rs index 240dc87..a8ab39e 100644 --- a/examples/tab_add.rs +++ b/examples/tab_add.rs @@ -58,7 +58,7 @@ impl eframe::App for MyApp { .show_add_buttons(true) .style({ let mut style = Style::from_egui(ctx.style().as_ref()); - style.tabs.fill_tab_bar = true; + style.tab_bar.fill_tab_bar = true; style }) .show( diff --git a/src/style.rs b/src/style.rs index 974ce1b..10f580f 100644 --- a/src/style.rs +++ b/src/style.rs @@ -21,10 +21,12 @@ pub struct Style { pub selection_color: Color32, pub border: Stroke, + pub rounding: Rounding, + pub buttons: ButtonsStyle, pub separator: SeparatorStyle, pub tab_bar: TabBarStyle, - pub tabs: TabStyle, + pub tab: TabStyle, } /// Specifies the look and feel of buttons. @@ -97,14 +99,44 @@ pub struct TabBarStyle { /// Color of th line separating the tab name area from the tab content area. /// By `Default` it's [`Color32::BLACK`]. pub hline_color: Color32, + + /// Whether tab titles expand to fill the width of their tab bars. + pub fill_tab_bar: bool, } -/// Specifies the look and feel of individual tabs. +/// Specifies the look and feel of an individual tab. #[derive(Clone, Debug)] pub struct TabStyle { - /// Inner margin of tab body. By `Default` it's `Margin::same(4.0)` - pub inner_margin: Margin, + /// Style of the tab when it is active. + pub active: TabInteractionStyle, + + /// Style of the tab when it is inactive. + pub inactive: TabInteractionStyle, + + /// Style of the tab when it is focused. + pub focused: TabInteractionStyle, + + /// Style of the tab when it is hovered. + pub hovered: TabInteractionStyle, + + /// Style for the tab body. + pub tab_body: TabBodyStyle, + + /// If `true`, show the hline below the active tabs name. + /// If `false`, show the active tab as merged with the tab ui area. + /// By `Default` it's `false`. + pub hline_below_active_tab_name: bool, + + /// The minimum width of the tab. + /// + /// The tab title or [`TabBarStyle::fill_tab_bar`] may make the tab + /// wider than this but never shorter. + pub minimum_width: Option, +} +/// Specifies the look and feel of individual tabs while they are being interacted with. +#[derive(Clone, Debug)] +pub struct TabInteractionStyle { /// Color of the outline around tabs. By `Default` it's [`Color32::BLACK`]. pub outline_color: Color32, @@ -114,25 +146,24 @@ pub struct TabStyle { /// Colour of the tab's background. By `Default` it's [`Color32::WHITE`] pub bg_fill: Color32, - /// Color of tab title when an inactive tab is unfocused. - pub text_color_unfocused: Color32, - - /// Color of tab title when an inactive tab is focused. - pub text_color_focused: Color32, + /// Color of the title text. + pub text_color: Color32, +} - /// Color of tab title when an active tab is unfocused. - pub text_color_active_unfocused: Color32, +/// Specifies the look and feel of the tab body. +#[derive(Clone, Debug)] +pub struct TabBodyStyle { + /// Inner margin of tab body. By `Default` it's `Margin::same(4.0)` + pub inner_margin: Margin, - /// Color of tab title when an active tab is focused. - pub text_color_active_focused: Color32, + /// The stroke of the tabs border. By `Default` it's ['Stroke::default'] + pub stroke: Stroke, - /// If `true`, show the hline below the active tabs name. - /// If `false`, show the active tab as merged with the tab ui area. - /// By `Default` it's `false`. - pub hline_below_active_tab_name: bool, + /// Tab rounding. By `Default` it's [`Rounding::default`] + pub rounding: Rounding, - /// Whether tab titles expand to fill the width of their tab bars. - pub fill_tab_bar: bool, + /// Colour of the tab's background. By `Default` it's [`Color32::WHITE`] + pub bg_fill: Color32, } impl Default for Style { @@ -140,11 +171,12 @@ impl Default for Style { Self { dock_area_padding: None, border: Stroke::new(f32::default(), Color32::BLACK), + rounding: Rounding::default(), selection_color: Color32::from_rgb(0, 191, 255).linear_multiply(0.5), buttons: ButtonsStyle::default(), separator: SeparatorStyle::default(), tab_bar: TabBarStyle::default(), - tabs: TabStyle::default(), + tab: TabStyle::default(), } } } @@ -186,6 +218,7 @@ impl Default for TabBarStyle { show_scroll_bar_on_overflow: true, rounding: Rounding::default(), hline_color: Color32::BLACK, + fill_tab_bar: false, } } } @@ -193,16 +226,44 @@ impl Default for TabBarStyle { impl Default for TabStyle { fn default() -> Self { Self { - inner_margin: Margin::same(4.0), - bg_fill: Color32::WHITE, - fill_tab_bar: false, + active: TabInteractionStyle::default(), + inactive: TabInteractionStyle { + text_color: Color32::DARK_GRAY, + ..Default::default() + }, + focused: TabInteractionStyle { + text_color: Color32::BLACK, + ..Default::default() + }, + hovered: TabInteractionStyle { + text_color: Color32::BLACK, + ..Default::default() + }, + tab_body: TabBodyStyle::default(), hline_below_active_tab_name: false, + minimum_width: None, + } + } +} + +impl Default for TabInteractionStyle { + fn default() -> Self { + Self { + bg_fill: Color32::WHITE, outline_color: Color32::BLACK, rounding: Rounding::default(), - text_color_unfocused: Color32::DARK_GRAY, - text_color_focused: Color32::BLACK, - text_color_active_unfocused: Color32::DARK_GRAY, - text_color_active_focused: Color32::BLACK, + text_color: Color32::DARK_GRAY, + } + } +} + +impl Default for TabBodyStyle { + fn default() -> Self { + Self { + inner_margin: Margin::same(4.0), + stroke: Stroke::default(), + rounding: Rounding::default(), + bg_fill: Color32::WHITE, } } } @@ -225,15 +286,13 @@ impl Style { /// [`TabStyle::from_egui`] pub fn from_egui(style: &egui::Style) -> Self { Self { - border: Stroke { - color: style.visuals.widgets.active.bg_fill, - ..Stroke::default() - }, + border: Stroke::NONE, + rounding: Rounding::none(), selection_color: style.visuals.selection.bg_fill.linear_multiply(0.5), buttons: ButtonsStyle::from_egui(style), separator: SeparatorStyle::from_egui(style), tab_bar: TabBarStyle::from_egui(style), - tabs: TabStyle::from_egui(style), + tab: TabStyle::from_egui(style), ..Self::default() } } @@ -251,13 +310,13 @@ impl ButtonsStyle { /// - [`ButtonsStyle::add_tab_active_color`] pub fn from_egui(style: &egui::Style) -> Self { Self { - close_tab_bg_fill: style.visuals.widgets.active.bg_fill, + close_tab_bg_fill: style.visuals.widgets.hovered.bg_fill, close_tab_color: style.visuals.text_color(), close_tab_active_color: style.visuals.strong_text_color(), - add_tab_bg_fill: style.visuals.widgets.active.bg_fill, + add_tab_bg_fill: style.visuals.widgets.hovered.bg_fill, add_tab_color: style.visuals.text_color(), add_tab_active_color: style.visuals.strong_text_color(), - add_tab_border_color: style.visuals.widgets.active.bg_fill, + add_tab_border_color: style.visuals.widgets.noninteractive.bg_fill, ..ButtonsStyle::default() } } @@ -286,35 +345,126 @@ impl TabBarStyle { /// /// Fields overwritten by [`egui::Style`] are: /// - [`TabBarStyle::bg_fill`] + /// - [`TabBarStyle::rounding`] /// - [`TabBarStyle::hline_color`] pub fn from_egui(style: &egui::Style) -> Self { Self { - bg_fill: (Rgba::from(style.visuals.window_fill()) * Rgba::from_gray(0.7)).into(), - hline_color: style.visuals.widgets.active.bg_fill, + bg_fill: style.visuals.extreme_bg_color, + rounding: Rounding { + nw: style.visuals.widgets.inactive.rounding.nw + 2.0, + ne: style.visuals.widgets.inactive.rounding.ne + 2.0, + sw: 0.0, + se: 0.0, + }, + hline_color: style.visuals.widgets.noninteractive.bg_stroke.color, ..TabBarStyle::default() } } } impl TabStyle { + /// Derives tab styles from `egui::Style`. + /// + /// See also: [`TabInteractionStyle::from_egui_active`], [`TabInteractionStyle::from_egui_inactive`], + /// [`TabInteractionStyle::from_egui_focused`], [`TabInteractionStyle::from_egui_hovered`], [`TabBodyStyle::from_egui`], + pub fn from_egui(style: &egui::Style) -> TabStyle { + Self { + active: TabInteractionStyle::from_egui_active(style), + inactive: TabInteractionStyle::from_egui_inactive(style), + focused: TabInteractionStyle::from_egui_focused(style), + hovered: TabInteractionStyle::from_egui_hovered(style), + tab_body: TabBodyStyle::from_egui(style), + ..Default::default() + } + } +} + +impl TabInteractionStyle { + /// Derives relevant fields from `egui::Style` for an active tab and sets the remaining fields to their default values. + /// + /// Fields overwritten by [`egui::Style`] are: + /// - [`TabInteractionStyle::outline_color`] + /// - [`TabInteractionStyle::bg_fill`] + /// - [`TabInteractionStyle::text_color`] + /// - [`TabInteractionStyle::rounding`] + pub fn from_egui_active(style: &egui::Style) -> Self { + Self { + outline_color: style.visuals.widgets.noninteractive.bg_stroke.color, + bg_fill: style.visuals.window_fill(), + text_color: style.visuals.text_color(), + rounding: Rounding { + sw: 0.0, + se: 0.0, + ..style.visuals.widgets.active.rounding + }, + } + } + /// Derives relevant fields from `egui::Style` for an inactive tab and sets the remaining fields to their default values. + /// + /// Fields overwritten by [`egui::Style`] are: + /// - [`TabInteractionStyle::outline_color`] + /// - [`TabInteractionStyle::bg_fill`] + /// - [`TabInteractionStyle::text_color`] + /// - [`TabInteractionStyle::rounding`] + pub fn from_egui_inactive(style: &egui::Style) -> Self { + Self { + text_color: style.visuals.text_color(), + bg_fill: egui::ecolor::tint_color_towards( + style.visuals.window_fill, + style.visuals.extreme_bg_color, + ), + outline_color: egui::ecolor::tint_color_towards( + style.visuals.widgets.noninteractive.bg_stroke.color, + style.visuals.extreme_bg_color, + ), + ..TabInteractionStyle::from_egui_active(style) + } + } + + /// Derives relevant fields from `egui::Style` for a focused tab and sets the remaining fields to their default values. + /// + /// Fields overwritten by [`egui::Style`] are: + /// - [`TabInteractionStyle::outline_color`] + /// - [`TabInteractionStyle::bg_fill`] + /// - [`TabInteractionStyle::text_color`] + /// - [`TabInteractionStyle::rounding`] + pub fn from_egui_focused(style: &egui::Style) -> Self { + Self { + text_color: style.visuals.strong_text_color(), + ..TabInteractionStyle::from_egui_active(style) + } + } + + /// Derives relevant fields from `egui::Style` for a hovered tab and sets the remaining fields to their default values. + /// + /// Fields overwritten by [`egui::Style`] are: + /// - [`TabInteractionStyle::outline_color`] + /// - [`TabInteractionStyle::bg_fill`] + /// - [`TabInteractionStyle::text_color`] + /// - [`TabInteractionStyle::rounding`] + pub fn from_egui_hovered(style: &egui::Style) -> Self { + Self { + text_color: style.visuals.strong_text_color(), + outline_color: style.visuals.widgets.hovered.bg_stroke.color, + ..TabInteractionStyle::from_egui_inactive(style) + } + } +} + +impl TabBodyStyle { /// Derives relevant fields from `egui::Style` and sets the remaining fields to their default values. /// /// Fields overwritten by [`egui::Style`] are: - /// - [`TabStyle::outline_color`] - /// - [`TabStyle::bg_fill`] - /// - [`TabStyle::text_color_unfocused`] - /// - [`TabStyle::text_color_focused`] - /// - [`TabStyle::text_color_active_unfocused`] - /// - [`TabStyle::text_color_active_focused`] + /// - [`TabBodyStyle::inner_margin`] + /// - [`TabBodyStyle::stroke] + /// - [`TabBodyStyle::rounding`] + /// - [`TabBodyStyle::bg_fill`] pub fn from_egui(style: &egui::Style) -> Self { Self { - outline_color: style.visuals.widgets.active.bg_fill, + inner_margin: style.spacing.window_margin, + stroke: style.visuals.widgets.noninteractive.bg_stroke, + rounding: style.visuals.widgets.active.rounding, bg_fill: style.visuals.window_fill(), - text_color_unfocused: style.visuals.text_color(), - text_color_focused: style.visuals.strong_text_color(), - text_color_active_unfocused: style.visuals.text_color(), - text_color_active_focused: style.visuals.strong_text_color(), - ..TabStyle::default() } } } diff --git a/src/tree/mod.rs b/src/tree/mod.rs index 28be72d..34bb009 100644 --- a/src/tree/mod.rs +++ b/src/tree/mod.rs @@ -222,6 +222,19 @@ impl Tree { self.split(parent, split, fraction, Node::leaf_with(tabs)) } + ///lpc - add comments + #[inline(always)] + pub fn split_tabs_single( + &mut self, + parent: NodeIndex, + split: Split, + fraction: f32, + tab: Tab, + ) -> [NodeIndex; 2] { + let tabs = vec![tab]; + self.split(parent, split, fraction, Node::leaf_with_single(tabs)) + } + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. /// @@ -241,6 +254,18 @@ impl Tree { self.split(parent, Split::Above, fraction, Node::leaf_with(tabs)) } + ///lpc - add comments + #[inline(always)] + pub fn split_above_single( + &mut self, + parent: NodeIndex, + fraction: f32, + tab: Tab, + ) -> [NodeIndex; 2] { + let tabs = vec![tab]; + self.split(parent, Split::Above, fraction, Node::leaf_with_single(tabs)) + } + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. /// @@ -260,6 +285,18 @@ impl Tree { self.split(parent, Split::Below, fraction, Node::leaf_with(tabs)) } + ///lpc - add comments + #[inline(always)] + pub fn split_below_single( + &mut self, + parent: NodeIndex, + fraction: f32, + tab: Tab, + ) -> [NodeIndex; 2] { + let tabs = vec![tab]; + self.split(parent, Split::Below, fraction, Node::leaf_with_single(tabs)) + } + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. /// @@ -279,6 +316,18 @@ impl Tree { self.split(parent, Split::Left, fraction, Node::leaf_with(tabs)) } + ///lpc - add comments + #[inline(always)] + pub fn split_left_single( + &mut self, + parent: NodeIndex, + fraction: f32, + tab: Tab, + ) -> [NodeIndex; 2] { + let tabs = vec![tab]; + self.split(parent, Split::Left, fraction, Node::leaf_with_single(tabs)) + } + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. /// @@ -298,6 +347,18 @@ impl Tree { self.split(parent, Split::Right, fraction, Node::leaf_with(tabs)) } + ///lpc - add comments + #[inline(always)] + pub fn split_right_single( + &mut self, + parent: NodeIndex, + fraction: f32, + tab: Tab, + ) -> [NodeIndex; 2] { + let tabs = vec![tab]; + self.split(parent, Split::Right, fraction, Node::leaf_with_single(tabs)) + } + /// Creates two new nodes by splitting a given `parent` node and assigns them as its children. The first (old) node /// inherits content of the `parent` from before the split, and the second (new) has `tabs`. /// diff --git a/src/tree/node.rs b/src/tree/node.rs index 3b41849..9a47daf 100644 --- a/src/tree/node.rs +++ b/src/tree/node.rs @@ -23,6 +23,9 @@ pub enum Node { /// Scroll amount of the tab bar. scroll: f32, + + /// Show the label on this leaf. + hide_label: bool, }, /// Parent node in the vertical orientation Vertical { @@ -52,6 +55,7 @@ impl Node { tabs: vec![tab], active: TabIndex(0), scroll: 0.0, + hide_label: false, } } @@ -64,6 +68,20 @@ impl Node { tabs, active: TabIndex(0), scroll: 0.0, + hide_label: false, + } + } + + /// Constructs a leaf node with a given list of `tabs`. + #[inline(always)] + pub const fn leaf_with_single(tabs: Vec) -> Self { + Self::Leaf { + rect: Rect::NOTHING, + viewport: Rect::NOTHING, + tabs, + active: TabIndex(0), + scroll: 0.0, + hide_label: true, } } diff --git a/src/utils.rs b/src/utils.rs index d81f9b5..fac6447 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -25,3 +25,9 @@ pub fn rect_set_size_centered(rect: &mut Rect, size: Vec2) { rect.set_height(size.y); rect.set_center(center); } + +/// Shrink a rectangle so that the stroke is fully contained inside +/// the original rectangle. +pub fn rect_stroke_box(rect: Rect, width: f32) -> Rect { + rect.expand(-f32::ceil(width / 2.0)) +} diff --git a/src/widgets/dock_area/hover_data.rs b/src/widgets/dock_area/hover_data.rs index 56d6a86..38a8013 100644 --- a/src/widgets/dock_area/hover_data.rs +++ b/src/widgets/dock_area/hover_data.rs @@ -1,4 +1,4 @@ -use crate::{NodeIndex, Split, TabDestination, TabIndex}; +use crate::{AllowedSplits, NodeIndex, Split, TabDestination, TabIndex}; use egui::{Pos2, Rect}; #[derive(Debug)] @@ -11,7 +11,7 @@ pub(super) struct HoverData { } impl HoverData { - pub(super) fn resolve(&self) -> (Rect, TabDestination) { + pub(super) fn resolve(&self, allowed_splits: &AllowedSplits) -> (Rect, TabDestination) { if let Some(tab) = self.tab { return (tab.0, TabDestination::Insert(tab.1)); } @@ -22,33 +22,70 @@ impl HoverData { let (rect, pointer) = (self.rect, self.pointer); let center = rect.center(); - let pts = [ - ( + + let pts = match allowed_splits { + AllowedSplits::All => vec![ + ( + center.distance(pointer), + TabDestination::Append, + Rect::EVERYTHING, + ), + ( + rect.left_center().distance(pointer), + TabDestination::Split(Split::Left), + Rect::everything_left_of(center.x), + ), + ( + rect.right_center().distance(pointer), + TabDestination::Split(Split::Right), + Rect::everything_right_of(center.x), + ), + ( + rect.center_top().distance(pointer), + TabDestination::Split(Split::Above), + Rect::everything_above(center.y), + ), + ( + rect.center_bottom().distance(pointer), + TabDestination::Split(Split::Below), + Rect::everything_below(center.y), + ), + ], + AllowedSplits::LeftRightOnly => vec![ + ( + center.distance(pointer), + TabDestination::Append, + Rect::EVERYTHING, + ), + ( + rect.left_center().distance(pointer), + TabDestination::Split(Split::Left), + Rect::everything_left_of(center.x), + ), + ( + rect.right_center().distance(pointer), + TabDestination::Split(Split::Right), + Rect::everything_right_of(center.x), + ), + ], + AllowedSplits::TopBottomOnly => vec![ + ( + rect.center_top().distance(pointer), + TabDestination::Split(Split::Above), + Rect::everything_above(center.y), + ), + ( + rect.center_bottom().distance(pointer), + TabDestination::Split(Split::Below), + Rect::everything_below(center.y), + ), + ], + AllowedSplits::None => vec![( center.distance(pointer), TabDestination::Append, Rect::EVERYTHING, - ), - ( - rect.left_center().distance(pointer), - TabDestination::Split(Split::Left), - Rect::everything_left_of(center.x), - ), - ( - rect.right_center().distance(pointer), - TabDestination::Split(Split::Right), - Rect::everything_right_of(center.x), - ), - ( - rect.center_top().distance(pointer), - TabDestination::Split(Split::Above), - Rect::everything_above(center.y), - ), - ( - rect.center_bottom().distance(pointer), - TabDestination::Split(Split::Below), - Rect::everything_below(center.y), - ), - ]; + )], + }; let (_, tab_dst, overlay) = pts .into_iter() diff --git a/src/widgets/dock_area/mod.rs b/src/widgets/dock_area/mod.rs index 46e5ca5..0817c97 100644 --- a/src/widgets/dock_area/mod.rs +++ b/src/widgets/dock_area/mod.rs @@ -1,8 +1,10 @@ mod hover_data; mod state; +use std::ops::RangeInclusive; + use crate::{ - utils::{expand_to_pixel, map_to_pixel, rect_set_size_centered}, + utils::{expand_to_pixel, map_to_pixel, rect_set_size_centered, rect_stroke_box}, widgets::popup::popup_under_widget, Node, NodeIndex, Style, TabAddAlign, TabIndex, TabStyle, TabViewer, Tree, }; @@ -16,6 +18,20 @@ use hover_data::HoverData; use paste::paste; use state::State; +/// What directions can this dock split in? +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum AllowedSplits { + #[default] + /// Allow splits in any direction (horizontal and vertical). + All, + /// Only allow split in a horizontal direction. + LeftRightOnly, + /// Only allow splits in a vertical direction. + TopBottomOnly, + /// Don't allow splits at all. + None, +} + /// Displays a [`Tree`] in `egui`. pub struct DockArea<'tree, Tab> { id: Id, @@ -28,6 +44,8 @@ pub struct DockArea<'tree, Tab> { draggable_tabs: bool, show_tab_name_on_hover: bool, scroll_area_in_tabs: bool, + allowed_splits: AllowedSplits, + show_label_bar: bool, drag_data: Option<(NodeIndex, TabIndex)>, hover_data: Option, @@ -52,11 +70,13 @@ impl<'tree, Tab> DockArea<'tree, Tab> { draggable_tabs: true, show_tab_name_on_hover: false, scroll_area_in_tabs: true, + allowed_splits: AllowedSplits::default(), drag_data: None, hover_data: None, to_remove: Vec::new(), new_focused: None, tab_hover_rect: None, + show_label_bar: true, } } @@ -122,6 +142,19 @@ impl<'tree, Tab> DockArea<'tree, Tab> { self.scroll_area_in_tabs = scroll_area_in_tabs; self } + + /// What directions can a node be split in: left-right, top-bottom, all, or none. + /// By default it's all. + pub fn allowed_splits(mut self, allowed_splits: AllowedSplits) -> Self { + self.allowed_splits = allowed_splits; + self + } + + /// should the label bar be shown for the tabs. + pub fn show_label_bar(mut self, show_label_bar: bool) -> Self{ + self.show_label_bar = show_label_bar; + self + } } // UI @@ -188,7 +221,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { } } - // Finaly draw separators so that their "interaction zone" is above + // Finally draw separators so that their "interaction zone" is above // bodies (see `SeparatorStyle::extra_interact_width`). for node_index in self.tree.breadth_first_index_iter() { if self.tree[node_index].is_parent() { @@ -212,7 +245,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { && self.tree[dst].is_leaf() && (src != dst || self.tree[dst].tabs_count() > 1) { - let (overlay, tab_dst) = hover.resolve(); + let (overlay, tab_dst) = hover.resolve(&self.allowed_splits); let id = Id::new("overlay"); let layer_id = LayerId::new(Order::Foreground, id); let painter = ui.ctx().layer_painter(layer_id); @@ -233,21 +266,15 @@ impl<'tree, Tab> DockArea<'tree, Tab> { if let Some(margin) = style.dock_area_padding { rect.min += margin.left_top(); rect.max -= margin.right_bottom(); - ui.painter().rect( - rect, - margin.top, - style.separator.color_idle, - Stroke::new(margin.top, style.border.color), - ); } + ui.painter().rect_stroke(rect, style.rounding, style.border); + rect = rect.expand(-style.border.width / 2.0); ui.allocate_rect(rect, Sense::hover()); - if self.tree.is_empty() { - return; + if !self.tree.is_empty() { + self.tree[NodeIndex::root()].set_rect(rect); } - - self.tree[NodeIndex::root()].set_rect(rect); } fn compute_rect_sizes(&mut self, ui: &mut Ui, node_index: NodeIndex) { @@ -267,8 +294,16 @@ impl<'tree, Tab> DockArea<'tree, Tab> { let rect = expand_to_pixel(*rect, pixels_per_point); let midpoint = rect.min.dim_point + rect.dim_size() * *fraction; - let left_separator_border = midpoint - style.separator.width * 0.5; - let right_separator_border = midpoint + style.separator.width * 0.5; + let left_separator_border = map_to_pixel( + midpoint - style.separator.width * 0.5, + pixels_per_point, + f32::round + ); + let right_separator_border = map_to_pixel( + midpoint + style.separator.width * 0.5, + pixels_per_point, + f32::round + ); paste! { let left = rect.intersect(Rect::[](left_separator_border)); @@ -346,6 +381,10 @@ impl<'tree, Tab> DockArea<'tree, Tab> { *fraction = (*fraction + delta / range).clamp(min, max); } } + + if response.double_clicked() { + *fraction = 0.5; + } } } } @@ -360,7 +399,9 @@ impl<'tree, Tab> DockArea<'tree, Tab> { assert!(self.tree[node_index].is_leaf()); let rect = { - let Node::Leaf { rect, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { rect, .. } = &mut self.tree[node_index] else { + unreachable!() + }; *rect }; let ui = &mut ui.child_ui_with_id_source( @@ -372,10 +413,23 @@ impl<'tree, Tab> DockArea<'tree, Tab> { ui.spacing_mut().item_spacing = vec2(0.0, 0.0); ui.set_clip_rect(rect); - let tabbar_response = self.tab_bar(ui, state, node_index, tab_viewer); - self.tab_body(ui, state, node_index, tab_viewer, spacing, tabbar_response); + + let Node::Leaf { hide_label, .. } = &self.tree[node_index] else { + unreachable!() + }; + + if self.show_label_bar && !hide_label{ + let tabbar_response = self.tab_bar(ui, state, node_index, tab_viewer); + self.tab_body_with_label(ui, state, node_index, tab_viewer, spacing, tabbar_response); + }else{ + self.tab_body_without_label(ui, node_index, tab_viewer, spacing); + } - let Node::Leaf { tabs, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { tabs, .. } = &mut self.tree[node_index] else { + unreachable!() + }; + + for (tab_index, tab) in tabs.iter_mut().enumerate() { if tab_viewer.force_close(tab) { self.to_remove.push((node_index, TabIndex(tab_index))); @@ -411,7 +465,9 @@ impl<'tree, Tab> DockArea<'tree, Tab> { } let actual_width = { - let Node::Leaf { tabs, scroll, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { tabs, scroll, .. } = &mut self.tree[node_index] else { + unreachable!() + }; let tabbar_inner_rect = Rect::from_min_size( (tabbar_outer_rect.min - pos2(-*scroll, 0.0)).to_pos2(), @@ -429,7 +485,10 @@ impl<'tree, Tab> DockArea<'tree, Tab> { tabs_ui.set_clip_rect(clip_rect); // Desired size for tabs in "expanded" mode - let expanded_width = available_width / (tabs.len() as f32); + let prefered_width = style + .tab_bar + .fill_tab_bar + .then_some(available_width / (tabs.len() as f32)); self.tabs( tabs_ui, @@ -437,7 +496,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { node_index, tab_viewer, tabbar_outer_rect, - expanded_width, + prefered_width, ); // Draw hline from tab end to edge of tabbar @@ -482,13 +541,15 @@ impl<'tree, Tab> DockArea<'tree, Tab> { node_index: NodeIndex, tab_viewer: &mut impl TabViewer, tabbar_outer_rect: Rect, - expanded_width: f32, + prefered_width: Option, ) { assert!(self.tree[node_index].is_leaf()); let focused = self.tree.focused_leaf(); let tabs_len = { - let Node::Leaf { tabs, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { tabs, .. } = &mut self.tree[node_index] else { + unreachable!() + }; tabs.len() }; @@ -502,17 +563,20 @@ impl<'tree, Tab> DockArea<'tree, Tab> { tabs_ui.output_mut(|o| o.cursor_icon = CursorIcon::Grabbing); } - let (is_active, label, tab_style) = { + let (is_active, label, tab_style, closeable) = { let Node::Leaf { tabs, active, .. } = &mut self.tree[node_index] else { unreachable!() }; let style = self.style.as_ref().unwrap(); - let tab_style = tab_viewer.tab_style_override(&tabs[tab_index.0], &style.tabs); + let tab_style = tab_viewer.tab_style_override(&tabs[tab_index.0], &style.tab); ( *active == tab_index || is_being_dragged, tab_viewer.title(&mut tabs[tab_index.0]), - tab_style.unwrap_or(style.tabs.clone()), + tab_style.unwrap_or(style.tab.clone()), + tab_viewer.closeable(&mut tabs[tab_index.0]), ) }; + let show_close_button = self.show_close_buttons && closeable; + let response = if is_being_dragged { let layer_id = LayerId::new(Order::Tooltip, id); let response = tabs_ui @@ -525,7 +589,8 @@ impl<'tree, Tab> DockArea<'tree, Tab> { is_active && Some(node_index) == focused, is_active, is_being_dragged, - expanded_width, + prefered_width, + show_close_button, ) }) .response; @@ -555,7 +620,8 @@ impl<'tree, Tab> DockArea<'tree, Tab> { is_active && Some(node_index) == focused, is_active, is_being_dragged, - expanded_width, + prefered_width, + show_close_button, ); let (close_hovered, close_clicked) = close_response @@ -568,7 +634,9 @@ impl<'tree, Tab> DockArea<'tree, Tab> { Sense::click_and_drag() }; - let Node::Leaf { tabs, active, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { tabs, active, hide_label, .. } = &mut self.tree[node_index] else { + unreachable!() + }; let tab = &mut tabs[tab_index.0]; if self.show_tab_name_on_hover { response = response.on_hover_ui(|ui| { @@ -578,8 +646,8 @@ impl<'tree, Tab> DockArea<'tree, Tab> { if self.tab_context_menus { response = response.context_menu(|ui| { - tab_viewer.context_menu(ui, tab); - if self.show_close_buttons && ui.button("Close").clicked() { + tab_viewer.context_menu(ui, tab, node_index); + if show_close_button && ui.button("Close").clicked() { if tab_viewer.on_close(tab) { self.to_remove.push((node_index, tab_index)); } else { @@ -607,7 +675,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { // Use response.rect.contains instead of // response.hovered as the dragged tab covers // the underlying tab - if state.drag_start.is_some() && response.rect.contains(pos) { + if state.drag_start.is_some() && response.rect.contains(pos) && !*hide_label { self.tab_hover_rect = Some((response.rect, tab_index)); } } @@ -616,11 +684,13 @@ impl<'tree, Tab> DockArea<'tree, Tab> { }; // Paint hline below each tab unless its active (or option says otherwise) - let Node::Leaf { tabs, active, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { tabs, active, .. } = &mut self.tree[node_index] else { + unreachable!() + }; let tab = &mut tabs[tab_index.0]; let style = self.style.as_ref().unwrap(); - let tab_style = tab_viewer.tab_style_override(tab, &style.tabs); - let tab_style = tab_style.as_ref().unwrap_or(&style.tabs); + let tab_style = tab_viewer.tab_style_override(tab, &style.tab); + let tab_style = tab_style.as_ref().unwrap_or(&style.tab); if !is_active || tab_style.hline_below_active_tab_name { let px = tabs_ui.ctx().pixels_per_point().recip(); @@ -659,7 +729,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { ) { let rect = Rect::from_min_max( tabbar_outer_rect.right_top() - vec2(Style::TAB_ADD_BUTTON_SIZE + offset, 0.0), - tabbar_outer_rect.right_bottom() - vec2(offset, 0.0), + tabbar_outer_rect.right_bottom() - vec2(offset, 2.0), ); let ui = &mut ui.child_ui_with_id_source( @@ -731,71 +801,75 @@ impl<'tree, Tab> DockArea<'tree, Tab> { focused: bool, active: bool, is_being_dragged: bool, - expanded_width: f32, + prefered_width: Option, + show_close_button: bool, ) -> (Response, Option) { let style = self.style.as_ref().unwrap(); - let rounding = tab_style.rounding; let galley = label.into_galley(ui, None, f32::INFINITY, TextStyle::Button); let x_spacing = 8.0; let text_width = galley.size().x + 2.0 * x_spacing; - let close_button_size = if self.show_close_buttons { + let close_button_size = if show_close_button { Style::TAB_CLOSE_BUTTON_SIZE.min(style.tab_bar.height) } else { 0.0 }; - let minimum_width = text_width + close_button_size; // Compute total width of the tab bar - let tab_width = if tab_style.fill_tab_bar { - expanded_width - } else { - minimum_width - } - .at_least(minimum_width); + let minimum_width = tab_style + .minimum_width + .unwrap_or(0.0) + .at_least(text_width + close_button_size); + let tab_width = prefered_width.unwrap_or(0.0).at_least(minimum_width); let (rect, mut response) = ui.allocate_exact_size(vec2(tab_width, ui.available_height()), Sense::hover()); if !ui.memory(|mem| mem.is_anything_being_dragged()) && self.draggable_tabs { - response = response.on_hover_cursor(CursorIcon::Grab); + response = response.on_hover_cursor(CursorIcon::PointingHand); } - if active { - if is_being_dragged { - ui.painter() - .rect_stroke(rect, rounding, Stroke::new(1.0, tab_style.outline_color)); - } else { - let stroke = Stroke::new(1.0, tab_style.outline_color); - ui.painter().rect(rect, rounding, tab_style.bg_fill, stroke); - - // Make the tab name area connect with the tab ui area: - ui.painter().hline( - rect.x_range(), - rect.bottom(), - Stroke::new(2.0, tab_style.bg_fill), - ); - } + let tab_style = if focused || is_being_dragged { + &tab_style.focused + } else if active { + &tab_style.active + } else if response.hovered() { + &tab_style.hovered + } else { + &tab_style.inactive + }; + + // Draw the full tab first and then the stroke ontop to avoid the stroke + // mixing with the background color. + ui.painter() + .rect_filled(rect, tab_style.rounding, tab_style.bg_fill); + let stroke_rect = rect_stroke_box(rect, 1.0); + ui.painter().rect_stroke( + stroke_rect, + tab_style.rounding, + Stroke::new(1.0, tab_style.outline_color), + ); + if !is_being_dragged { + // Make the tab name area connect with the tab ui area: + ui.painter().hline( + RangeInclusive::new( + stroke_rect.min.x + f32::max(tab_style.rounding.sw, 1.5), + stroke_rect.max.x - f32::max(tab_style.rounding.se, 1.5), + ), + stroke_rect.bottom(), + Stroke::new(2.0, tab_style.bg_fill), + ); } let mut text_rect = rect; text_rect.set_width(tab_width - close_button_size); - let text_pos = if tab_style.fill_tab_bar { + let text_pos = { let mut pos = Align2::CENTER_CENTER.pos_in_rect(&text_rect.shrink2(vec2(x_spacing, 0.0))); pos -= galley.size() / 2.0; pos - } else { - let mut pos = Align2::LEFT_CENTER.pos_in_rect(&text_rect.shrink2(vec2(x_spacing, 0.0))); - pos.y -= galley.size().y / 2.0; - pos }; - let override_text_color = (!galley.galley_has_color).then_some(match (active, focused) { - (false, false) => tab_style.text_color_unfocused, - (false, true) => tab_style.text_color_focused, - (true, false) => tab_style.text_color_active_unfocused, - (true, true) => tab_style.text_color_active_focused, - }); + let override_text_color = (!galley.galley_has_color).then_some(tab_style.text_color); ui.painter().add(TextShape { pos: text_pos, @@ -805,7 +879,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { angle: 0.0, }); - let close_response = self.show_close_buttons.then(|| { + let close_response = show_close_button.then(|| { let mut close_button_rect = rect; close_button_rect.set_left(text_rect.right()); close_button_rect = @@ -822,7 +896,7 @@ impl<'tree, Tab> DockArea<'tree, Tab> { }; if response.hovered() { - let mut rounding = rounding; + let mut rounding = tab_style.rounding; rounding.nw = 0.0; rounding.sw = 0.0; @@ -858,7 +932,9 @@ impl<'tree, Tab> DockArea<'tree, Tab> { available_width: f32, tabbar_response: &Response, ) { - let Node::Leaf { scroll, .. } = &mut self.tree[node_index] else { unreachable!() }; + let Node::Leaf { scroll, .. } = &mut self.tree[node_index] else { + unreachable!() + }; let overflow = (actual_width - available_width).at_least(0.0); let style = self.style.as_ref().unwrap(); @@ -930,25 +1006,23 @@ impl<'tree, Tab> DockArea<'tree, Tab> { *scroll = scroll.clamp(-overflow, 0.0); } - fn tab_body( + fn tab_body_general( &mut self, ui: &mut Ui, - state: &mut State, node_index: NodeIndex, tab_viewer: &mut impl TabViewer, spacing: Vec2, - tabbar_response: Response, ) { let (body_rect, _body_response) = ui.allocate_exact_size(ui.available_size_before_wrap(), Sense::click_and_drag()); let Node::Leaf { - rect, tabs, active, viewport, .. - } = &mut self.tree[node_index] else { + } = &mut self.tree[node_index] + else { unreachable!(); }; @@ -964,10 +1038,11 @@ impl<'tree, Tab> DockArea<'tree, Tab> { } let style = self.style.as_ref().unwrap(); - let tabs_style = tab_viewer.tab_style_override(tab, &style.tabs); - let tabs_style = tabs_style.as_ref().unwrap_or(&style.tabs); + let tabs_styles = tab_viewer.tab_style_override(tab, &style.tab); + let tabs_style = tabs_styles.as_ref().unwrap_or(&style.tab); if tab_viewer.clear_background(tab) { - ui.painter().rect_filled(body_rect, 0.0, tabs_style.bg_fill); + ui.painter() + .rect_filled(body_rect, 0.0, tabs_style.tab_body.bg_fill); } // Construct a new ui with the correct tab id @@ -984,14 +1059,30 @@ impl<'tree, Tab> DockArea<'tree, Tab> { body_rect, ui.clip_rect(), ); + ui.set_clip_rect(Rect::from_min_max(ui.cursor().min, ui.clip_rect().max)); // Use initial spacing for ui ui.spacing_mut().item_spacing = spacing; + // Offset the background rectangle up to hide the top border behind the clip rect. + // To avoid anti-aliasing lines when the stroke width is not divisible by two, we + // need to calulate the effective anti aliased stroke width. + let effective_stroke_width = (tabs_style.tab_body.stroke.width / 2.0).ceil() * 2.0; + let tab_body_rect = Rect::from_min_max( + ui.clip_rect().min - vec2(0.0, effective_stroke_width), + ui.clip_rect().max, + ); + ui.painter().rect( + rect_stroke_box(tab_body_rect, tabs_style.tab_body.stroke.width), + tabs_style.tab_body.rounding, + tabs_style.tab_body.bg_fill, + tabs_style.tab_body.stroke, + ); + if self.scroll_area_in_tabs { ScrollArea::both().show(ui, |ui| { Frame::none() - .inner_margin(tabs_style.inner_margin) + .inner_margin(tabs_style.tab_body.inner_margin) .show(ui, |ui| { let available_rect = ui.available_rect_before_wrap(); ui.expand_to_include_rect(available_rect); @@ -1000,12 +1091,42 @@ impl<'tree, Tab> DockArea<'tree, Tab> { }); } else { Frame::none() - .inner_margin(tabs_style.inner_margin) + .inner_margin(tabs_style.tab_body.inner_margin) .show(ui, |ui| { tab_viewer.ui(ui, tab); }); } } + } + + fn tab_body_without_label( + &mut self, + ui: &mut Ui, + node_index: NodeIndex, + tab_viewer: &mut impl TabViewer, + spacing: Vec2, + ) { + self.tab_body_general(ui, node_index, tab_viewer, spacing) + } + + fn tab_body_with_label( + &mut self, + ui: &mut Ui, + state: &mut State, + node_index: NodeIndex, + tab_viewer: &mut impl TabViewer, + spacing: Vec2, + tabbar_response: Response, + ) { + self.tab_body_general(ui, node_index, tab_viewer, spacing); + + let Node::Leaf { + rect, + .. + } = &mut self.tree[node_index] + else { + unreachable!(); + }; if let Some(pointer) = ui.input(|i| i.pointer.hover_pos()) { // Use rect.contains instead of diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 84a4e3b..874839d 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -6,5 +6,5 @@ pub(crate) mod popup; /// Trait for tab-viewing types. pub mod tab_viewer; -pub use dock_area::DockArea; +pub use dock_area::{AllowedSplits, DockArea}; pub use tab_viewer::TabViewer; diff --git a/src/widgets/tab_viewer.rs b/src/widgets/tab_viewer.rs index dbfeef2..4a15a25 100644 --- a/src/widgets/tab_viewer.rs +++ b/src/widgets/tab_viewer.rs @@ -10,7 +10,10 @@ pub trait TabViewer { fn ui(&mut self, ui: &mut Ui, tab: &mut Self::Tab); /// Content inside context_menu. - fn context_menu(&mut self, _ui: &mut Ui, _tab: &mut Self::Tab) {} + /// + /// The `_node` specifies which `Node` or split of the tree that this + /// particular context menu belongs to. + fn context_menu(&mut self, _ui: &mut Ui, _tab: &mut Self::Tab, _node: NodeIndex) {} /// The title to be displayed. fn title(&mut self, tab: &mut Self::Tab) -> WidgetText; @@ -25,6 +28,13 @@ pub trait TabViewer { /// Called after each tab button is shown, so you can add a tooltip, check for clicks, etc. fn on_tab_button(&mut self, _tab: &mut Self::Tab, _response: &egui::Response) {} + /// Called before showing the close button. + /// + /// Return `false` if the close buttons should not be shown. + fn closeable(&mut self, _tab: &mut Self::Tab) -> bool { + true + } + /// This is called when the tabs close button is pressed. /// /// Returns `true` if the tab should close immediately, `false` otherwise.