fix: exclude scroll area padding from pane sizing

This commit is contained in:
2026-11-09 08:35:07 +07:00
parent d8663593b6
commit 8c438ed8a6
9 changed files with 269 additions and 209 deletions
+5
View File
@@ -1106,14 +1106,17 @@ fn render_pane(
let state = state.clone();
let active = pane.active.clone();
let panels: Vec<SharedString> = pane.tabs.iter().map(|tab| tab.panel.clone()).collect();
move |_, _| {
let mut items = Vec::new();
if let Some(active) = &active {
items.push(MenuItem::new("Close").icon(IconRole::Close).handler({
let state = state.clone();
let active = active.clone();
move |_, cx| state.update(cx, |dock, cx| dock.close_panel(&active, cx))
}));
if panels.len() > 1 {
items.push(MenuItem::new("Close Others").handler({
let state = state.clone();
@@ -1122,6 +1125,7 @@ fn render_pane(
}));
}
}
if let Some(first) = panels.first() {
items.push(MenuItem::new("Close Pane").handler({
let state = state.clone();
@@ -1129,6 +1133,7 @@ fn render_pane(
move |_, cx| state.update(cx, |dock, cx| dock.close_pane(&first, cx))
}));
}
items
}
});
+12 -31
View File
@@ -6,7 +6,6 @@ use gpui::{
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
StyleRefinement, Styled, Window, canvas, div, px,
};
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::StyledExt as _;
use crate::resize_handle::ResizeHandle;
@@ -58,37 +57,30 @@ impl ResizableState {
}
/// The equal split of the measured extent.
fn equal_split(&self, pane_count: usize, thickness: Pixels) -> Pixels {
fn equal_split(&self, pane_count: usize) -> Pixels {
match self.measured {
Some(extent) => (extent - thickness * (pane_count as f32 - 1.0)) / pane_count as f32,
Some(extent) => extent / pane_count as f32,
None => px(DEFAULT_PANE_SIZE),
}
}
/// The size pane `ix` renders at: its stored size, or the equal split.
fn pane_size(&self, ix: usize, pane_count: usize, thickness: Pixels) -> Pixels {
fn pane_size(&self, ix: usize, pane_count: usize) -> Pixels {
self.sizes
.get(ix)
.copied()
.unwrap_or_else(|| self.equal_split(pane_count, thickness))
.unwrap_or_else(|| self.equal_split(pane_count))
}
/// Applies a drag at divider `ix` (between pane ix and ix+1).
fn resize(
&mut self,
ix: usize,
delta: Pixels,
mins: &[Pixels],
thickness: Pixels,
cx: &mut Context<Self>,
) {
fn resize(&mut self, ix: usize, delta: Pixels, mins: &[Pixels], cx: &mut Context<Self>) {
let Some(&floor) = mins.get(ix) else {
return;
};
let pane_count = mins.len();
let mut size = self.pane_size(ix, pane_count, thickness) + delta;
let mut size = self.pane_size(ix, pane_count) + delta;
size = size.max(floor);
if let Some(extent) = self.measured {
@@ -97,7 +89,7 @@ impl ResizableState {
.enumerate()
.filter(|(j, _)| *j != ix)
.fold(px(0.), |sum, (_, min)| sum + *min);
let ceiling = extent - thickness * (pane_count as f32 - 1.0) - floors;
let ceiling = extent - floors;
size = size.min(ceiling);
}
@@ -110,14 +102,8 @@ impl ResizableState {
}
/// Restores pane `ix` to the equal split, as a double-click does.
fn reset_pane(
&mut self,
ix: usize,
pane_count: usize,
thickness: Pixels,
cx: &mut Context<Self>,
) {
let split = self.equal_split(pane_count, thickness);
fn reset_pane(&mut self, ix: usize, pane_count: usize, cx: &mut Context<Self>) {
let split = self.equal_split(pane_count);
while self.sizes.len() <= ix {
self.sizes.push(px(DEFAULT_PANE_SIZE));
}
@@ -223,7 +209,6 @@ impl Styled for Resizable {
impl RenderOnce for Resizable {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let thickness = cx.theme().sizes.resolve(Size::default()).dense_padding_x;
let axis = self.axis;
let state = self.state.clone();
@@ -232,7 +217,7 @@ impl RenderOnce for Resizable {
let debug_selector = self.debug_selector;
let sizes: Vec<Pixels> = (0..pane_count)
.map(|ix| state.read(cx).pane_size(ix, pane_count, thickness))
.map(|ix| state.read(cx).pane_size(ix, pane_count))
.collect();
let mut group = div()
@@ -274,18 +259,14 @@ impl RenderOnce for Resizable {
let state = state.clone();
let mins = mins.clone();
move |delta, _, cx| {
state.update(cx, |state, cx| {
state.resize(ix, delta, &mins, thickness, cx)
});
state.update(cx, |state, cx| state.resize(ix, delta, &mins, cx));
}
})
.on_reset({
let state = state.clone();
let mins = mins.clone();
move |_, cx| {
state.update(cx, |state, cx| {
state.reset_pane(ix, mins.len(), thickness, cx)
});
state.update(cx, |state, cx| state.reset_pane(ix, mins.len(), cx));
}
});
+159 -141
View File
@@ -7,8 +7,8 @@ use gpui::{
App, Axis, Bounds, CursorStyle, DispatchPhase, ElementId, HitboxBehavior,
InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, RenderOnce, Role,
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, div, fill, point,
px,
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div,
fill, point, px,
};
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::{focus_handle, focus_ring};
@@ -100,6 +100,9 @@ impl RenderOnce for ResizeHandle {
let border = theme.colors.border;
let ring = theme.colors.ring;
let thickness = theme.sizes.resolve(Size::default()).dense_padding_x;
// Half the handle overlaps each pane so the hairline lands on their shared boundary,
// leaving the handle no layout footprint of its own.
let straddle = -thickness / 2.0;
let label = self
.a11y_label
@@ -114,6 +117,7 @@ impl RenderOnce for ResizeHandle {
})
.clone()
});
let focus = focus_handle(&self.id, window, cx);
let dragging = drag.read(cx).dragging;
let axis = self.axis;
@@ -126,153 +130,167 @@ impl RenderOnce for ResizeHandle {
let on_reset = self.on_reset;
div()
.id(self.id.clone())
.debug_selector(|| "resize-handle".to_string())
.role(Role::Splitter)
.aria_label(label)
.flex_none()
.when(axis == Axis::Horizontal, |handle| {
handle.w(thickness).h_full()
})
.when(axis == Axis::Vertical, |handle| {
handle.w_full().h(thickness)
})
.when(self.disabled, |handle| handle.opacity(0.5))
.when(!self.disabled, |handle| {
handle
.track_focus(&focus)
.focusable()
.tab_stop(true)
.focus_visible(|style| style.shadow(focus_ring(ring)))
.on_mouse_down(MouseButton::Left, {
let drag = drag.clone();
let focus = focus.clone();
move |event: &MouseDownEvent, window, cx| {
window.focus(&focus, cx);
cx.stop_propagation();
if event.click_count >= 2 {
deferred(
div()
.id(self.id.clone())
.debug_selector(|| "resize-handle".to_string())
.role(Role::Splitter)
.aria_label(label)
.flex_none()
.when(axis == Axis::Horizontal, |handle| {
handle.w(thickness).h_full().mx(straddle)
})
.when(axis == Axis::Vertical, |handle| {
handle.w_full().h(thickness).my(straddle)
})
.when(self.disabled, |handle| handle.opacity(0.5))
.when(!self.disabled, |handle| {
handle
.track_focus(&focus)
.focusable()
.tab_stop(true)
.focus_visible(|style| style.shadow(focus_ring(ring)))
.on_mouse_down(MouseButton::Left, {
let drag = drag.clone();
let focus = focus.clone();
move |event: &MouseDownEvent, window, cx| {
window.focus(&focus, cx);
cx.stop_propagation();
if event.click_count >= 2 {
drag.update(cx, |drag, cx| {
drag.dragging = false;
cx.notify();
});
if let Some(on_reset) = on_reset.clone() {
on_reset(window, cx);
}
return;
}
drag.update(cx, |drag, cx| {
drag.dragging = false;
drag.dragging = true;
drag.last = event.position;
cx.notify();
});
if let Some(on_reset) = on_reset.clone() {
on_reset(window, cx);
}
return;
}
drag.update(cx, |drag, cx| {
drag.dragging = true;
drag.last = event.position;
cx.notify();
});
}
})
.on_key_down({
let on_resize = self.on_resize.clone();
move |event: &KeyDownEvent, window, cx| {
let delta = match (axis, event.keystroke.key.as_str()) {
(Axis::Horizontal, "left") => Some(px(-KEYBOARD_STEP)),
(Axis::Horizontal, "right") => Some(px(KEYBOARD_STEP)),
(Axis::Vertical, "up") => Some(px(-KEYBOARD_STEP)),
(Axis::Vertical, "down") => Some(px(KEYBOARD_STEP)),
_ => None,
};
if let Some(delta) = delta {
cx.stop_propagation();
if let Some(on_resize) = on_resize.clone() {
on_resize(delta, window, cx);
}
}
}
})
})
.child(
canvas(
{
move |bounds, window, _| {
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
(hitbox, bounds)
}
},
{
let drag = drag.clone();
let on_resize = self.on_resize.clone();
move |_, (hitbox, bounds), window, _cx| {
let hovered = hitbox.is_hovered(window);
if !disabled {
window.set_cursor_style(cursor, &hitbox);
}
// The hairline highlights while hovered or dragged.
let line_color = if !disabled && (dragging || hovered) {
ring
} else {
border
};
let line = match axis {
Axis::Horizontal => {
let x = bounds.center().x - px(0.5);
Bounds::from_corners(
point(x, bounds.top()),
point(x + px(1.), bounds.bottom()),
)
}
Axis::Vertical => {
let y = bounds.center().y - px(0.5);
Bounds::from_corners(
point(bounds.left(), y),
point(bounds.right(), y + px(1.)),
)
}
};
window.paint_quad(fill(line, line_color));
window.on_mouse_event({
let drag = drag.clone();
let on_resize = on_resize.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
let (dragging, last) = {
let drag = drag.read(cx);
(drag.dragging, drag.last)
};
if !dragging {
return;
}
let delta = match axis {
Axis::Horizontal => event.position.x - last.x,
Axis::Vertical => event.position.y - last.y,
};
drag.update(cx, |drag, _| drag.last = event.position);
})
.on_key_down({
let on_resize = self.on_resize.clone();
move |event: &KeyDownEvent, window, cx| {
let delta = match (axis, event.keystroke.key.as_str()) {
(Axis::Horizontal, "left") => Some(px(-KEYBOARD_STEP)),
(Axis::Horizontal, "right") => Some(px(KEYBOARD_STEP)),
(Axis::Vertical, "up") => Some(px(-KEYBOARD_STEP)),
(Axis::Vertical, "down") => Some(px(KEYBOARD_STEP)),
_ => None,
};
if let Some(delta) = delta {
cx.stop_propagation();
if let Some(on_resize) = on_resize.clone() {
on_resize(delta, window, cx);
}
}
});
window.on_mouse_event({
let drag = drag.clone();
move |event: &MouseUpEvent, phase, _, cx| {
if phase != DispatchPhase::Bubble
|| event.button != MouseButton::Left
{
return;
}
if drag.read(cx).dragging {
drag.update(cx, |drag, cx| {
drag.dragging = false;
cx.notify();
});
}
}
})
})
.child(
canvas(
{
move |bounds, window, _| {
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
(hitbox, bounds)
}
},
{
let drag = drag.clone();
let on_resize = self.on_resize.clone();
move |_, (hitbox, bounds), window, _cx| {
let hovered = hitbox.is_hovered(window);
if !disabled {
window.set_cursor_style(cursor, &hitbox);
}
});
}
},
)
.absolute()
.inset_0(),
)
// The hairline highlights while hovered or dragged.
let line_color = if !disabled && (dragging || hovered) {
ring
} else {
border
};
let line = match axis {
Axis::Horizontal => {
let x = bounds.center().x - px(0.5);
Bounds::from_corners(
point(x, bounds.top()),
point(x + px(1.), bounds.bottom()),
)
}
Axis::Vertical => {
let y = bounds.center().y - px(0.5);
Bounds::from_corners(
point(bounds.left(), y),
point(bounds.right(), y + px(1.)),
)
}
};
window.paint_quad(fill(line, line_color));
window.on_mouse_event({
let drag = drag.clone();
let on_resize = on_resize.clone();
move |event: &MouseMoveEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble {
return;
}
let (dragging, last) = {
let drag = drag.read(cx);
(drag.dragging, drag.last)
};
if !dragging {
return;
}
let delta = match axis {
Axis::Horizontal => event.position.x - last.x,
Axis::Vertical => event.position.y - last.y,
};
drag.update(cx, |drag, _| drag.last = event.position);
if let Some(on_resize) = on_resize.clone() {
on_resize(delta, window, cx);
}
}
});
window.on_mouse_event({
let drag = drag.clone();
move |event: &MouseUpEvent, phase, _, cx| {
if phase != DispatchPhase::Bubble
|| event.button != MouseButton::Left
{
return;
}
if drag.read(cx).dragging {
drag.update(cx, |drag, cx| {
drag.dragging = false;
cx.notify();
});
}
}
});
}
},
)
.absolute()
.inset_0(),
),
)
}
}