mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-04 17:50:08 +00:00
docking: wheel selects tabs and middle click closes a tab
Both are on in the Frame profile and off by default in the Dock profile; DockStyle::wheel_selects_tabs and middle_click_closes_tabs set them. A middle click closes only a closable panel.
This commit is contained in:
1 parent
d3079230da
commit
b94df6c8b6
4 files changed
+102
No files matched your search
@@ -629,3 +629,40 @@ fn tab_focus_follows_only_the_users_input() {
|
||||
d.step(vec![]);
|
||||
assert_eq!(d.ctx.memory(|m| m.focused()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_tabs_follow_the_wheel_and_close_on_a_middle_click() {
|
||||
let second = |d: &Driver| match &d.layout.root {
|
||||
Some(Node::Split { second, .. }) => (**second).clone(),
|
||||
_ => panic!("{:?}", d.layout.root),
|
||||
};
|
||||
let wheel = |unit, y| Event::MouseWheel { unit, delta: vec2(0.0, y), phase: egui::TouchPhase::Move, modifiers: Modifiers::NONE };
|
||||
let middle = |pos, pressed| Event::PointerButton { pos, button: PointerButton::Middle, pressed, modifiers: Modifiers::NONE };
|
||||
|
||||
// Off by default in the Dock profile.
|
||||
let mut d = Driver::new(frames());
|
||||
let over = d.tab(2).center();
|
||||
d.step(vec![Event::PointerMoved(over), wheel(egui::MouseWheelUnit::Line, 1.0)]);
|
||||
d.step(vec![middle(over, true)]);
|
||||
d.step(vec![middle(over, false)]);
|
||||
assert_eq!(second(&d), tabs(&[2, 3], 1));
|
||||
|
||||
let mut d = Driver::new(frames());
|
||||
d.tweak = |style| style.profile = Profile::Frame;
|
||||
d.step(vec![]);
|
||||
let over = d.tab(2).center();
|
||||
d.step(vec![Event::PointerMoved(over), wheel(egui::MouseWheelUnit::Line, 1.0)]);
|
||||
assert_eq!(second(&d), tabs(&[2, 3], 0), "wheel up selects the previous tab");
|
||||
d.step(vec![wheel(egui::MouseWheelUnit::Line, 1.0)]);
|
||||
assert_eq!(second(&d), tabs(&[2, 3], 0), "and stops at the first");
|
||||
d.step(vec![wheel(egui::MouseWheelUnit::Point, -30.0)]);
|
||||
assert_eq!(second(&d), tabs(&[2, 3], 0), "trackpad scrolling adds up to a notch");
|
||||
let out = d.step(vec![wheel(egui::MouseWheelUnit::Point, -30.0)]);
|
||||
assert_eq!(second(&d), tabs(&[2, 3], 1));
|
||||
assert_eq!(out.focused, Some(3));
|
||||
|
||||
d.step(vec![middle(over, true)]);
|
||||
let out = d.step(vec![middle(over, false)]);
|
||||
assert_eq!(out.actions, vec![Action::Close { panel: 2 }]);
|
||||
assert_eq!(second(&d), tabs(&[3], 0));
|
||||
}
|
||||
@@ -32,6 +32,9 @@ const BORDER_BAND: f32 = 4.0;
|
||||
/// Width of the "…" button that lists the panels a strip has no room for.
|
||||
const OVERFLOW_WIDTH: f32 = 22.0;
|
||||
|
||||
/// Points of smooth (trackpad) scrolling that make one wheel notch.
|
||||
const WHEEL_NOTCH: f32 = 40.0;
|
||||
|
||||
impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
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 };
|
||||
@@ -127,6 +130,10 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
// Registered last, so the band wins over the tabs below it.
|
||||
let border = mirror(Rect::from_min_max(strip.min, pos2(strip.right() - header_width, (strip.top() + BORDER_BAND).min(strip.bottom()))));
|
||||
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);
|
||||
}
|
||||
// Logical order, hidden tabs included without a hit area, for no-op detection.
|
||||
let tabs =
|
||||
panels.iter().map(|panel| (panel.clone(), shown.iter().find(|(p, _)| p == panel).map_or(Rect::NOTHING, |(_, rect)| *rect))).collect();
|
||||
@@ -200,6 +207,42 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
egui::Popup::menu(&response).open_memory(command).show(|ui| self.group_menu(ui, owner, owner.selected));
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
let key = self.id.with(("tab-wheel", panels.first()));
|
||||
if !ui.rect_contains_pointer(strip) {
|
||||
ui.ctx().data_mut(|d| d.remove::<f32>(key));
|
||||
return;
|
||||
}
|
||||
let notches: f32 = ui.input(|i| {
|
||||
i.events
|
||||
.iter()
|
||||
.map(|event| match event {
|
||||
egui::Event::MouseWheel { unit, delta, modifiers, .. } if !modifiers.command && !modifiers.ctrl => match unit {
|
||||
egui::MouseWheelUnit::Point => delta.y / WHEEL_NOTCH,
|
||||
egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => delta.y,
|
||||
},
|
||||
_ => 0.0,
|
||||
})
|
||||
.sum()
|
||||
});
|
||||
if notches == 0.0 || !notches.is_finite() {
|
||||
return;
|
||||
}
|
||||
let total = ui.ctx().data(|d| d.get_temp::<f32>(key)).unwrap_or(0.0) + notches;
|
||||
let steps = total.trunc();
|
||||
ui.ctx().data_mut(|d| d.insert_temp(key, total - steps));
|
||||
// A wheel turned down scrolls content up: a negative delta selects the next tab.
|
||||
let to = (active as i64 - steps as i64).clamp(0, panels.len().saturating_sub(1) as i64) as usize;
|
||||
if to != active
|
||||
&& let Some(panel) = panels.get(to)
|
||||
{
|
||||
self.output.actions.push(Action::Activate { panel: panel.clone() });
|
||||
self.output.focused = Some(panel.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn start_group_drag(&mut self, ui: &Ui, panels: &[P], rect: Rect) {
|
||||
let Some(first) = panels.first() else { return };
|
||||
let origin = ui.input(|i| i.pointer.press_origin()).or_else(|| ui.ctx().pointer_interact_pos()).unwrap_or(rect.center());
|
||||
@@ -361,6 +404,9 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
|
||||
self.output.actions.push(Action::SetMinimized { panel: panel.clone(), minimized: !minimized });
|
||||
}
|
||||
}
|
||||
if self.style.middle_click_closes_tabs() && policy.closable && response.clicked_by(egui::PointerButton::Middle) {
|
||||
self.output.actions.push(Action::Close { panel: panel.clone() });
|
||||
}
|
||||
if policy.movable && response.drag_started() && begin_drag(ui.ctx(), self.id, panel.clone(), group) {
|
||||
self.latch_undock(ui.ctx(), owner.map(|owner| owner.kind));
|
||||
}
|
||||
|
||||
@@ -99,6 +99,12 @@ pub struct DockStyle {
|
||||
pub close_menu_items: bool,
|
||||
/// Show × on the title bar of a floating window whose panels are all closable.
|
||||
pub close_button: bool,
|
||||
/// The mouse wheel over a tab strip selects the next or previous tab, one per notch.
|
||||
/// `None`: on in [`Profile::Frame`], off in [`Profile::Dock`].
|
||||
pub wheel_selects_tabs: Option<bool>,
|
||||
/// A middle click on a tab closes its panel when the panel is closable. `None`: on in
|
||||
/// [`Profile::Frame`], off in [`Profile::Dock`].
|
||||
pub middle_click_closes_tabs: Option<bool>,
|
||||
}
|
||||
|
||||
/// Which arrangement a [`DockStyle`] renders and labels.
|
||||
@@ -167,9 +173,19 @@ impl DockStyle {
|
||||
new_window_label: "Move Panel to New Window".into(),
|
||||
close_menu_items: true,
|
||||
close_button: true,
|
||||
wheel_selects_tabs: None,
|
||||
middle_click_closes_tabs: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn wheel_selects_tabs(&self) -> bool {
|
||||
self.wheel_selects_tabs.unwrap_or(self.profile == Profile::Frame)
|
||||
}
|
||||
|
||||
pub(super) fn middle_click_closes_tabs(&self) -> bool {
|
||||
self.middle_click_closes_tabs.unwrap_or(self.profile == Profile::Frame)
|
||||
}
|
||||
|
||||
pub(super) fn valid(&self) -> bool {
|
||||
[
|
||||
self.tab_height,
|
||||
|
||||
@@ -176,6 +176,9 @@ A flyout fits its content the same way, without the title bar.
|
||||
- Drop previews show the destination: an outline for tabs, a line for a new group, column or dock,
|
||||
a filled half for a split. Escape or losing window focus cancels a drag.
|
||||
- Double-click a tab or drag bar to minimize a group to its strip.
|
||||
- In the Frame profile, the mouse wheel over a tab strip selects the next or previous tab, and a
|
||||
middle click on a closable panel's tab closes it. `DockStyle::wheel_selects_tabs` and
|
||||
`middle_click_closes_tabs` turn either on or off in any profile.
|
||||
- A column's »/« button collapses it to icons, one icon per panel with a separator between groups.
|
||||
Widen an iconic column to show panel names. Click an icon to open its group as a flyout on the
|
||||
canvas side. Drag an icon to move its panel, or the grip above an icon group to move the group.
|
||||
|
||||
Reference in new issue
Block a user