mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-06 18:43:09 +00:00
docking: apps can draw extra tabs after a group's panel tabs (#356)
DockContent::group_extra_tabs_width and group_extra_tabs draw host tabs right after a group's panel tabs, before the drag bar and the header. They fit with the panel tabs as one more tab that never overflows: both shrink widest first to min_tab_width, and in a narrower row the panel tabs overflow while the extra tabs keep that width.
This commit is contained in:
1 parent
edc41f68fa
commit
512b54355c
4 files changed
+108
-17
No files matched your search
@@ -49,6 +49,9 @@ struct TestContent {
|
||||
header_clicks: usize,
|
||||
/// The strip width the last header was sized for.
|
||||
strip_width: std::cell::Cell<f32>,
|
||||
/// Width the extra tabs ask for, and the rect they were last drawn in.
|
||||
extra: f32,
|
||||
extra_rect: Option<Rect>,
|
||||
floating_size: Option<Vec2>,
|
||||
host_owned: bool,
|
||||
whole_group: bool,
|
||||
@@ -95,6 +98,12 @@ impl DockContent<u32> for TestContent {
|
||||
self.header_clicks += 1;
|
||||
}
|
||||
}
|
||||
fn group_extra_tabs_width(&self, _: &[u32], _: &u32, _: bool) -> f32 {
|
||||
self.extra
|
||||
}
|
||||
fn group_extra_tabs(&mut self, ui: &mut egui::Ui, _: &[u32], _: &u32, _: bool) {
|
||||
self.extra_rect = Some(ui.max_rect());
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders a layout through `show_content` in `area` of an 800x600 window and applies what
|
||||
@@ -288,6 +297,33 @@ fn drag_bar_survives_a_strip_full_of_tabs() {
|
||||
assert_eq!(d.layout.floating[0].groups()[0].panels.len(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// The extra tabs follow the last panel tab, before the drag bar and the header. In a narrow row
|
||||
/// they shrink to the minimum tab width while the panel tabs overflow, and the bar keeps its
|
||||
/// width; a tab dropped on them goes after the last panel tab.
|
||||
fn extra_tabs_follow_the_panel_tabs_and_overflow_with_them() {
|
||||
let content = TestContent { header: 24.0, extra: 100.0, ..Default::default() };
|
||||
let layout = Layout { root: Some(tabs(&[1, 2], 0)), floating: Vec::new() };
|
||||
let mut d = Driver::with(layout, content, Rect::from_min_size(pos2(0.0, 0.0), vec2(600.0, 400.0)));
|
||||
let (extra, bar) = (d.content.extra_rect.expect("extra tabs drawn"), d.rect("drag-bar", 1));
|
||||
assert_eq!((extra.left(), extra.width()), (d.tab(2).right(), 100.0));
|
||||
assert_eq!((bar.left(), bar.right()), (extra.right(), 600.0 - 24.0), "the bar and the header follow");
|
||||
d.drag(d.tab(1).center(), extra.center());
|
||||
assert_eq!(d.layout.root, Some(tabs(&[2, 1], 1)), "a drop on the extra tabs appends");
|
||||
|
||||
let content = TestContent { header: 24.0, extra: 100.0, ..Default::default() };
|
||||
let layout = Layout { root: Some(tabs(&[1, 2, 3], 0)), floating: Vec::new() };
|
||||
let d = Driver::with(layout, content, Rect::from_min_size(pos2(0.0, 0.0), vec2(200.0, 400.0)));
|
||||
let overflow = d.ctx.read_response(Id::unique(AREA).with(("overflow", &1u32))).expect("the panel tabs overflow").rect;
|
||||
let (extra, bar) = (d.content.extra_rect.expect("extra tabs drawn"), d.rect("drag-bar", 1));
|
||||
assert_eq!(overflow.left(), d.tab(1).right());
|
||||
assert_eq!(extra.left(), overflow.right(), "the extra tabs follow the overflow button");
|
||||
let min = DockStyle::default().min_tab_width;
|
||||
assert!(extra.width() >= min - 0.5 && extra.width() < 100.0, "{extra:?}");
|
||||
assert_eq!((bar.left(), bar.right()), (extra.right(), 200.0 - 24.0));
|
||||
assert!(bar.width() >= 40.0, "{bar:?}");
|
||||
}
|
||||
|
||||
/// Panels with ordinary names.
|
||||
struct Named;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::{fmt::Debug, hash::Hash, sync::Arc};
|
||||
use egui::{Color32, Galley, Id, Key, Rect, Sense, Stroke, StrokeKind, Ui, UiBuilder, pos2, vec2};
|
||||
|
||||
use super::{Renderer, clamp_float, stripless};
|
||||
use crate::docking::{Action, DropTarget, GroupDrag, GroupGesture, Placement, Target, begin_drag, surface::TAB_PADDING};
|
||||
use crate::docking::{Action, DockContent, DropTarget, GroupDrag, GroupGesture, Placement, Target, begin_drag, surface::TAB_PADDING};
|
||||
use crate::tabs;
|
||||
|
||||
/// Where a group is drawn. It decides the drop targets the group offers and its gestures.
|
||||
@@ -42,9 +42,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
/// its whole rect is the body and its only tab has `Rect::NOTHING`. Side splits are offered
|
||||
/// only for a `Frame` or `Floating` group whose selected panel accepts splits. Tab labels
|
||||
/// shrink widest-first to the minimum width, then the remaining tabs move to the overflow menu;
|
||||
/// hidden overflow tabs still appear in the drop target with `Rect::NOTHING`. `minimized` hides
|
||||
/// the body but keeps the strip. Tabs, strip, header and drag bar are mirrored in right-to-left
|
||||
/// layout.
|
||||
/// hidden overflow tabs still appear in the drop target with `Rect::NOTHING`. The host's extra
|
||||
/// tabs follow the panel tabs and fit with them ([`fit_with_extra`]). `minimized` hides the body
|
||||
/// but keeps the strip. Tabs, strip, header and drag bar are mirrored in right-to-left layout.
|
||||
pub(super) fn group(&mut self, ui: &mut Ui, panels: &[P], active: usize, minimized: bool, rect: Rect, kind: GroupKind) {
|
||||
let Some(selected) = panels.get(active) else { return };
|
||||
self.record_float(panels, rect.size(), rect.size());
|
||||
@@ -81,13 +81,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
let header_width = if requested.is_finite() { requested.clamp(0.0, strip.width()) } else { 0.0 };
|
||||
if header_width > 0.0 {
|
||||
let header = mirror(Rect::from_min_max(pos2(strip.right() - header_width, strip.top()), strip.max));
|
||||
let mut child = ui.new_child(UiBuilder::new().scope_id(self.id.with(("header", selected))).max_rect(header));
|
||||
child.set_clip_rect(header.intersect(ui.clip_rect()));
|
||||
self.content.group_header(&mut child, panels, selected, floating);
|
||||
// A press in the header focuses the shown panel, as a press on its tab does.
|
||||
if child.rect_contains_pointer(header) && child.input(|i| i.pointer.any_pressed()) {
|
||||
self.output.focused = Some(selected.clone());
|
||||
}
|
||||
self.host_area(ui, "header", header, selected, |content, ui| content.group_header(ui, panels, selected, floating));
|
||||
}
|
||||
// The drag bar keeps its reserved width: tabs shrink, then overflow, before it narrows.
|
||||
let bar_width = self.style.drag_bar_width.clamp(0.0, (strip.width() - header_width).max(0.0));
|
||||
@@ -103,10 +97,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
+ TAB_PADDING
|
||||
})
|
||||
.collect();
|
||||
let wanted = self.content.group_extra_tabs_width(panels, selected, floating);
|
||||
let wanted = if wanted.is_finite() { wanted.clamp(0.0, tabs_width) } else { 0.0 };
|
||||
// The widest tabs shrink first, to a common width no narrower than `min_tab_width`; only
|
||||
// when every tab at that width cannot fit do the rest go to the overflow menu. A tab
|
||||
// narrower than its name cuts it with "…" and names it in a tooltip.
|
||||
let fit = tabs::fit(&widths, active, tabs_width, self.style.min_tab_width, OVERFLOW_WIDTH);
|
||||
let (fit, extra_width) = fit_with_extra(&widths, active, tabs_width, wanted, self.style.min_tab_width);
|
||||
let owner = TabGroup { panels, selected, minimized, kind, rect };
|
||||
let mut x = strip.left();
|
||||
let mut shown = Vec::new();
|
||||
@@ -138,6 +134,12 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
});
|
||||
x += fit.overflow_width;
|
||||
}
|
||||
let mut extra = Rect::NOTHING;
|
||||
if extra_width > 0.0 {
|
||||
extra = mirror(Rect::from_min_size(pos2(x, strip.top()), vec2(extra_width, strip.height())));
|
||||
self.host_area(ui, "extra-tabs", extra, selected, |content, ui| content.group_extra_tabs(ui, panels, selected, floating));
|
||||
x += extra_width;
|
||||
}
|
||||
let bar =
|
||||
mirror(Rect::from_min_max(pos2(x.min(strip.right() - header_width), strip.top()), pos2(strip.right() - header_width, strip.bottom())));
|
||||
self.group_handle(ui, "drag-bar", bar, &owner, movable);
|
||||
@@ -146,7 +148,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.group_handle(ui, "drag-border", border, &owner, movable);
|
||||
if self.style.wheel_selects_tabs {
|
||||
let tabs_area = mirror(Rect::from_min_max(strip.min, pos2(strip.right() - header_width, strip.bottom())));
|
||||
self.wheel_tabs(ui, tabs_area, panels, active);
|
||||
self.wheel_tabs(ui, tabs_area, extra, panels, active);
|
||||
}
|
||||
// Logical order, hidden tabs included without a hit area, for no-op detection.
|
||||
let tabs =
|
||||
@@ -166,6 +168,17 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
ui.painter().rect_stroke(rect, 0.0, self.style.border, StrokeKind::Inside);
|
||||
}
|
||||
|
||||
/// A part of the strip the host draws (`header` or `extra-tabs`) at `rect`. A press in it
|
||||
/// focuses the shown panel, as a press on its tab does.
|
||||
fn host_area(&mut self, ui: &mut Ui, name: &'static str, rect: Rect, selected: &P, draw: impl FnOnce(&mut dyn DockContent<P>, &mut Ui)) {
|
||||
let mut child = ui.new_child(UiBuilder::new().scope_id(self.id.with((name, selected))).max_rect(rect));
|
||||
child.set_clip_rect(rect.intersect(ui.clip_rect()));
|
||||
draw(&mut *self.content, &mut child);
|
||||
if child.rect_contains_pointer(rect) && child.input(|i| i.pointer.any_pressed()) {
|
||||
self.output.focused = Some(selected.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// The drag bar (`drag-bar`: drag, double-click, and the group menu on a right click, Enter
|
||||
/// or Space) or the top border band (`drag-border`: drag only). Both move the whole group;
|
||||
/// a left click on the bar only focuses it.
|
||||
@@ -209,10 +222,11 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
}
|
||||
|
||||
/// The wheel over a group's strip: down selects the next tab, up the previous, one tab per
|
||||
/// notch, stopping at the ends. Trackpad scrolling adds up to a notch.
|
||||
fn wheel_tabs(&mut self, ui: &Ui, strip: Rect, panels: &[P], active: usize) {
|
||||
/// notch, stopping at the ends. Trackpad scrolling adds up to a notch. The host's extra tabs
|
||||
/// (`extra`) keep the wheel for their own widgets.
|
||||
fn wheel_tabs(&mut self, ui: &Ui, strip: Rect, extra: Rect, panels: &[P], active: usize) {
|
||||
let key = self.id.with(("tab-wheel", panels.first()));
|
||||
if !ui.rect_contains_pointer(strip) {
|
||||
if !ui.rect_contains_pointer(strip) || ui.rect_contains_pointer(extra) {
|
||||
ui.ctx().data_mut(|d| d.remove::<f32>(key));
|
||||
return;
|
||||
}
|
||||
@@ -476,3 +490,21 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Fit the panel tabs and the host's extra tabs (`extra` wide), which fit as one more tab that
|
||||
/// never overflows: in a row too narrow for every tab at its minimum width, the panel tabs
|
||||
/// overflow into the room the extra tabs leave at theirs. Returns the extra tabs' width.
|
||||
fn fit_with_extra(widths: &[f32], active: usize, available: f32, extra: f32, minimum: f32) -> (tabs::TabFit, f32) {
|
||||
if extra <= 0.0 {
|
||||
return (tabs::fit(widths, active, available, minimum, OVERFLOW_WIDTH), 0.0);
|
||||
}
|
||||
let all: Vec<f32> = widths.iter().copied().chain([extra]).collect();
|
||||
let mut fit = tabs::fit(&all, active, available, minimum, OVERFLOW_WIDTH);
|
||||
if fit.overflow.is_empty() {
|
||||
let width = fit.shown.pop().filter(|(index, _)| *index == widths.len()).map_or(0.0, |(_, width)| width);
|
||||
return (fit, width);
|
||||
}
|
||||
let fit = tabs::fit(widths, active, (available - extra.min(minimum)).max(0.0), minimum, OVERFLOW_WIDTH);
|
||||
let used = fit.shown.iter().map(|(_, width)| width).sum::<f32>() + fit.overflow_width;
|
||||
(fit, extra.min(available - used).max(0.0))
|
||||
}
|
||||
@@ -911,6 +911,19 @@ pub trait DockContent<P> {
|
||||
/// [`DockArea::new`], `active` and the widget's rect when its response reports
|
||||
/// `drag_started()`; the panel then moves as its tab would.
|
||||
fn group_header(&mut self, _ui: &mut Ui, _panels: &[P], _active: &P, _floating: bool) {}
|
||||
/// Width the group's extra tabs want: the host's own tabs, drawn right after the group's
|
||||
/// panel tabs (and their "…" button), before the drag bar. They fit with the panel tabs:
|
||||
/// both shrink widest first to [`DockStyle::min_tab_width`] (the extra tabs as one tab); in a
|
||||
/// row narrower still, the panel tabs overflow and the extra tabs keep that width. Zero draws
|
||||
/// none.
|
||||
fn group_extra_tabs_width(&self, _panels: &[P], _active: &P, _floating: bool) -> f32 {
|
||||
0.0
|
||||
}
|
||||
/// Draw the extra tabs into `ui.max_rect()`, which can be narrower than
|
||||
/// [`Self::group_extra_tabs_width`] asked for; the host fits its tabs into it, with an
|
||||
/// overflow of its own. Their widgets take their own input as the header's do, and a press
|
||||
/// among them focuses the shown panel. A drop on them adds the panel after the last panel tab.
|
||||
fn group_extra_tabs(&mut self, _ui: &mut Ui, _panels: &[P], _active: &P, _floating: bool) {}
|
||||
}
|
||||
|
||||
/// Host-owned drop resolution request, see [`DockContent::resolve_drag`].
|
||||
@@ -995,6 +1008,12 @@ impl<P, C: DockContent<P> + ?Sized> DockContent<P> for Overrides<'_, P, C> {
|
||||
fn group_header(&mut self, ui: &mut Ui, panels: &[P], active: &P, floating: bool) {
|
||||
self.inner.group_header(ui, panels, active, floating);
|
||||
}
|
||||
fn group_extra_tabs_width(&self, panels: &[P], active: &P, floating: bool) -> f32 {
|
||||
self.inner.group_extra_tabs_width(panels, active, floating)
|
||||
}
|
||||
fn group_extra_tabs(&mut self, ui: &mut Ui, panels: &[P], active: &P, floating: bool) {
|
||||
self.inner.group_extra_tabs(ui, panels, active, floating);
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional renderer for applications without an existing docking surface. Apps with custom
|
||||
|
||||
+5
-1
@@ -131,7 +131,11 @@ its own command path, so pointer, keyboard and control commands share one path.
|
||||
`DockContent<P>` is the app's adapter. It draws each panel body and gives its label, icon,
|
||||
permissions, size limits, dock column heights, preferred floating size, panel menu and group header. The group header
|
||||
sits at the end of the tab strip; `group_header_width` gets the strip width, so a header that
|
||||
draws its own tabs can size them to the room left. An icon is an image (`icon`), or drawn in code by `paint_icon`, which
|
||||
draws its own tabs can size them to the room left. Extra tabs (`group_extra_tabs_width` and
|
||||
`group_extra_tabs`) sit right after the group's panel tabs, before the drag bar, and fit with
|
||||
them: both shrink widest first to `min_tab_width`, the extra tabs as one tab, and in a row
|
||||
narrower still the panel tabs overflow while the extra tabs keep that width. The host fits its
|
||||
own tabs into the rect it gets. An icon is an image (`icon`), or drawn in code by `paint_icon`, which
|
||||
gets a painter, the icon rect and the tint color and returns true when it painted. Panel bodies keep a stable egui id when they move, so widget memory
|
||||
survives.
|
||||
|
||||
|
||||
Reference in new issue
Block a user