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
+19 -14
View File
@@ -222,6 +222,9 @@ pub(crate) fn thumb_geometry(
impl RenderOnce for ScrollArea { impl RenderOnce for ScrollArea {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let mut pane_style = self.style.clone();
pane_style.padding = Default::default();
let mut pane = div() let mut pane = div()
.id(self.id.clone()) .id(self.id.clone())
.overflow_hidden() .overflow_hidden()
@@ -239,7 +242,7 @@ impl RenderOnce for ScrollArea {
// Keep wheel events from scrolling an outer scroll area too. // Keep wheel events from scrolling an outer scroll area too.
cx.stop_propagation(); cx.stop_propagation();
}) })
.refine_style(&self.style); .refine_style(&pane_style);
// Extend with children // Extend with children
pane.extend(self.children); pane.extend(self.children);
@@ -248,17 +251,18 @@ impl RenderOnce for ScrollArea {
wrapper.extend(std::iter::once(pane.into_any_element())); wrapper.extend(std::iter::once(pane.into_any_element()));
if let Some(handle) = self.handle { if let Some(handle) = self.handle {
let id = self.id.clone();
let autohide = match self.visibility { let autohide = match self.visibility {
ScrollbarVisibility::Platform => cx.theme().scrollbar.autohide, ScrollbarVisibility::Platform => cx.theme().scrollbar.autohide,
ScrollbarVisibility::Always => false, ScrollbarVisibility::Always => false,
ScrollbarVisibility::Autohide => true, ScrollbarVisibility::Autohide => true,
}; };
let id = self.id.clone();
wrapper.extend(std::iter::once( wrapper.extend(std::iter::once(
canvas(|_, _, _| {}, { canvas(|bounds, _, _| bounds, {
let handle = handle.clone(); let handle = handle.clone();
move |_, _, window, cx| { move |overlay, _, window, cx| {
paint_scrollbar(&handle, &id, autohide, window, cx); paint_scrollbar(&handle, &id, autohide, overlay, window, cx);
} }
}) })
.absolute() .absolute()
@@ -276,6 +280,7 @@ fn paint_scrollbar(
handle: &ScrollHandle, handle: &ScrollHandle,
key: &ElementId, key: &ElementId,
autohide: bool, autohide: bool,
overlay: Bounds<Pixels>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
@@ -287,10 +292,10 @@ fn paint_scrollbar(
let inset = theme.scrollbar.inset; let inset = theme.scrollbar.inset;
let min_thumb = theme.scrollbar.min_thumb; let min_thumb = theme.scrollbar.min_thumb;
let viewport = handle.bounds();
let max_offset = handle.max_offset().y; let max_offset = handle.max_offset().y;
let bounds = handle.bounds();
if max_offset <= Pixels::ZERO || bounds.size.height <= Pixels::ZERO { if max_offset <= Pixels::ZERO || viewport.size.height <= Pixels::ZERO {
return; return;
} }
@@ -306,21 +311,21 @@ fn paint_scrollbar(
_ => 1.0, _ => 1.0,
}; };
let track = bounds.size.height - inset * 2.0; let track = overlay.size.height - inset * 2.0;
let (thumb_length, thumb_top) = thumb_geometry( let (thumb_length, thumb_top) = thumb_geometry(
bounds.size.height, viewport.size.height,
bounds.size.height + max_offset, viewport.size.height + max_offset,
handle.offset().y, handle.offset().y,
track, track,
min_thumb, min_thumb,
); );
let thumb_x = bounds.right() - width - inset; let thumb_x = overlay.right() - width - inset;
let thumb_bounds = Bounds::from_corners( let thumb_bounds = Bounds::from_corners(
point(thumb_x, bounds.top() + inset + thumb_top), point(thumb_x, overlay.top() + inset + thumb_top),
point( point(
thumb_x + width, thumb_x + width,
bounds.top() + inset + thumb_top + thumb_length, overlay.top() + inset + thumb_top + thumb_length,
), ),
); );
@@ -332,7 +337,7 @@ fn paint_scrollbar(
window.on_mouse_event({ window.on_mouse_event({
let state = state.clone(); let state = state.clone();
move |event: &ScrollEvent, phase, window, cx| { move |event: &ScrollEvent, phase, window, cx| {
if phase != DispatchPhase::Bubble || !bounds.contains(&event.position) { if phase != DispatchPhase::Bubble || !overlay.contains(&event.position) {
return; return;
} }
state.touch(window, cx); state.touch(window, cx);
+50 -3
View File
@@ -1,7 +1,7 @@
use gpui::{ use gpui::{
Context, IntoElement, Modifiers, ParentElement as _, Render, ScrollDelta, ScrollEvent, Context, InteractiveElement as _, IntoElement, Modifiers, ParentElement as _, Render,
ScrollHandle, SharedString, Styled as _, TestAppContext, VisualTestContext, Window, div, point, ScrollDelta, ScrollEvent, ScrollHandle, SharedString, Styled as _, TestAppContext,
px, VisualTestContext, Window, div, point, px,
}; };
use gpui_native_core::scroll_area::ScrollArea; use gpui_native_core::scroll_area::ScrollArea;
use gpui_native_theme::Theme; use gpui_native_theme::Theme;
@@ -116,6 +116,53 @@ impl Render for GalleryShell {
} }
} }
/// A window-filling scroll area with `p_4`, over one full-width row.
struct PaddedShell {
scroll: ScrollHandle,
}
impl Render for PaddedShell {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.size_full()
.debug_selector(|| "padded-root".to_string())
.child(
ScrollArea::new("padded-scroll")
.handle(self.scroll.clone())
.size_full()
.p_4()
.child(
div()
.debug_selector(|| "padded-row".to_string())
.h(px(20.))
.w_full(),
),
)
}
}
#[test]
fn padding_insets_the_content_exactly_once() {
let mut cx = TestAppContext::single();
cx.update(Theme::init);
let handle = ScrollHandle::new();
let handle_for_view = handle.clone();
let window = cx.add_window(move |_, _| PaddedShell {
scroll: handle_for_view,
});
let mut visual = VisualTestContext::from_window(window.into(), &cx);
visual.update(|window, cx| {
window.draw(cx).clear(cx);
});
let root = visual.debug_bounds("padded-root").expect("root rendered");
let row = visual.debug_bounds("padded-row").expect("row rendered");
// p_4 = 16px: the padding reaches only the wrapper, so the content is
// inset once, not once per wrapper and scroll pane.
assert_eq!(row.origin.x - root.origin.x, px(16.));
assert_eq!(root.size.width - row.size.width, px(32.));
}
#[test] #[test]
fn a_gallery_style_window_fill_scroll_area_scrolls_tall_flex_content() { fn a_gallery_style_window_fill_scroll_area_scrolls_tall_flex_content() {
let mut cx = TestAppContext::single(); let mut cx = TestAppContext::single();
+1
View File
@@ -230,6 +230,7 @@ impl RenderOnce for Sidebar {
ScrollArea::new(scroll_area_id) ScrollArea::new(scroll_area_id)
.handle(scroll.read(cx).clone()) .handle(scroll.read(cx).clone())
.h_full() .h_full()
.px_1()
.gap_0p5() .gap_0p5()
.children(children), .children(children),
) )
+5
View File
@@ -1106,14 +1106,17 @@ fn render_pane(
let state = state.clone(); let state = state.clone();
let active = pane.active.clone(); let active = pane.active.clone();
let panels: Vec<SharedString> = pane.tabs.iter().map(|tab| tab.panel.clone()).collect(); let panels: Vec<SharedString> = pane.tabs.iter().map(|tab| tab.panel.clone()).collect();
move |_, _| { move |_, _| {
let mut items = Vec::new(); let mut items = Vec::new();
if let Some(active) = &active { if let Some(active) = &active {
items.push(MenuItem::new("Close").icon(IconRole::Close).handler({ items.push(MenuItem::new("Close").icon(IconRole::Close).handler({
let state = state.clone(); let state = state.clone();
let active = active.clone(); let active = active.clone();
move |_, cx| state.update(cx, |dock, cx| dock.close_panel(&active, cx)) move |_, cx| state.update(cx, |dock, cx| dock.close_panel(&active, cx))
})); }));
if panels.len() > 1 { if panels.len() > 1 {
items.push(MenuItem::new("Close Others").handler({ items.push(MenuItem::new("Close Others").handler({
let state = state.clone(); let state = state.clone();
@@ -1122,6 +1125,7 @@ fn render_pane(
})); }));
} }
} }
if let Some(first) = panels.first() { if let Some(first) = panels.first() {
items.push(MenuItem::new("Close Pane").handler({ items.push(MenuItem::new("Close Pane").handler({
let state = state.clone(); let state = state.clone();
@@ -1129,6 +1133,7 @@ fn render_pane(
move |_, cx| state.update(cx, |dock, cx| dock.close_pane(&first, cx)) move |_, cx| state.update(cx, |dock, cx| dock.close_pane(&first, cx))
})); }));
} }
items items
} }
}); });
+12 -31
View File
@@ -6,7 +6,6 @@ use gpui::{
InteractiveElement as _, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString, InteractiveElement as _, IntoElement, ParentElement as _, Pixels, RenderOnce, SharedString,
StyleRefinement, Styled, Window, canvas, div, px, StyleRefinement, Styled, Window, canvas, div, px,
}; };
use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::StyledExt as _; use gpui_native_utils::StyledExt as _;
use crate::resize_handle::ResizeHandle; use crate::resize_handle::ResizeHandle;
@@ -58,37 +57,30 @@ impl ResizableState {
} }
/// The equal split of the measured extent. /// 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 { 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), None => px(DEFAULT_PANE_SIZE),
} }
} }
/// The size pane `ix` renders at: its stored size, or the equal split. /// 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 self.sizes
.get(ix) .get(ix)
.copied() .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). /// Applies a drag at divider `ix` (between pane ix and ix+1).
fn resize( fn resize(&mut self, ix: usize, delta: Pixels, mins: &[Pixels], cx: &mut Context<Self>) {
&mut self,
ix: usize,
delta: Pixels,
mins: &[Pixels],
thickness: Pixels,
cx: &mut Context<Self>,
) {
let Some(&floor) = mins.get(ix) else { let Some(&floor) = mins.get(ix) else {
return; return;
}; };
let pane_count = mins.len(); 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); size = size.max(floor);
if let Some(extent) = self.measured { if let Some(extent) = self.measured {
@@ -97,7 +89,7 @@ impl ResizableState {
.enumerate() .enumerate()
.filter(|(j, _)| *j != ix) .filter(|(j, _)| *j != ix)
.fold(px(0.), |sum, (_, min)| sum + *min); .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); size = size.min(ceiling);
} }
@@ -110,14 +102,8 @@ impl ResizableState {
} }
/// Restores pane `ix` to the equal split, as a double-click does. /// Restores pane `ix` to the equal split, as a double-click does.
fn reset_pane( fn reset_pane(&mut self, ix: usize, pane_count: usize, cx: &mut Context<Self>) {
&mut self, let split = self.equal_split(pane_count);
ix: usize,
pane_count: usize,
thickness: Pixels,
cx: &mut Context<Self>,
) {
let split = self.equal_split(pane_count, thickness);
while self.sizes.len() <= ix { while self.sizes.len() <= ix {
self.sizes.push(px(DEFAULT_PANE_SIZE)); self.sizes.push(px(DEFAULT_PANE_SIZE));
} }
@@ -223,7 +209,6 @@ impl Styled for Resizable {
impl RenderOnce for Resizable { impl RenderOnce for Resizable {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { 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 axis = self.axis;
let state = self.state.clone(); let state = self.state.clone();
@@ -232,7 +217,7 @@ impl RenderOnce for Resizable {
let debug_selector = self.debug_selector; let debug_selector = self.debug_selector;
let sizes: Vec<Pixels> = (0..pane_count) 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(); .collect();
let mut group = div() let mut group = div()
@@ -274,18 +259,14 @@ impl RenderOnce for Resizable {
let state = state.clone(); let state = state.clone();
let mins = mins.clone(); let mins = mins.clone();
move |delta, _, cx| { move |delta, _, cx| {
state.update(cx, |state, cx| { state.update(cx, |state, cx| state.resize(ix, delta, &mins, cx));
state.resize(ix, delta, &mins, thickness, cx)
});
} }
}) })
.on_reset({ .on_reset({
let state = state.clone(); let state = state.clone();
let mins = mins.clone(); let mins = mins.clone();
move |_, cx| { move |_, cx| {
state.update(cx, |state, cx| { state.update(cx, |state, cx| state.reset_pane(ix, mins.len(), cx));
state.reset_pane(ix, mins.len(), thickness, cx)
});
} }
}); });
+22 -4
View File
@@ -7,8 +7,8 @@ use gpui::{
App, Axis, Bounds, CursorStyle, DispatchPhase, ElementId, HitboxBehavior, App, Axis, Bounds, CursorStyle, DispatchPhase, ElementId, HitboxBehavior,
InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent, InteractiveElement as _, IntoElement, KeyDownEvent, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, RenderOnce, Role, MouseMoveEvent, MouseUpEvent, ParentElement as _, Pixels, Point, RenderOnce, Role,
SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, div, fill, point, SharedString, StatefulInteractiveElement as _, Styled as _, Window, canvas, deferred, div,
px, fill, point, px,
}; };
use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_theme::{ActiveTheme as _, Size};
use gpui_native_utils::{focus_handle, focus_ring}; use gpui_native_utils::{focus_handle, focus_ring};
@@ -100,6 +100,9 @@ impl RenderOnce for ResizeHandle {
let border = theme.colors.border; let border = theme.colors.border;
let ring = theme.colors.ring; let ring = theme.colors.ring;
let thickness = theme.sizes.resolve(Size::default()).dense_padding_x; 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 let label = self
.a11y_label .a11y_label
@@ -114,6 +117,7 @@ impl RenderOnce for ResizeHandle {
}) })
.clone() .clone()
}); });
let focus = focus_handle(&self.id, window, cx); let focus = focus_handle(&self.id, window, cx);
let dragging = drag.read(cx).dragging; let dragging = drag.read(cx).dragging;
let axis = self.axis; let axis = self.axis;
@@ -126,6 +130,7 @@ impl RenderOnce for ResizeHandle {
let on_reset = self.on_reset; let on_reset = self.on_reset;
deferred(
div() div()
.id(self.id.clone()) .id(self.id.clone())
.debug_selector(|| "resize-handle".to_string()) .debug_selector(|| "resize-handle".to_string())
@@ -133,10 +138,10 @@ impl RenderOnce for ResizeHandle {
.aria_label(label) .aria_label(label)
.flex_none() .flex_none()
.when(axis == Axis::Horizontal, |handle| { .when(axis == Axis::Horizontal, |handle| {
handle.w(thickness).h_full() handle.w(thickness).h_full().mx(straddle)
}) })
.when(axis == Axis::Vertical, |handle| { .when(axis == Axis::Vertical, |handle| {
handle.w_full().h(thickness) handle.w_full().h(thickness).my(straddle)
}) })
.when(self.disabled, |handle| handle.opacity(0.5)) .when(self.disabled, |handle| handle.opacity(0.5))
.when(!self.disabled, |handle| { .when(!self.disabled, |handle| {
@@ -148,9 +153,11 @@ impl RenderOnce for ResizeHandle {
.on_mouse_down(MouseButton::Left, { .on_mouse_down(MouseButton::Left, {
let drag = drag.clone(); let drag = drag.clone();
let focus = focus.clone(); let focus = focus.clone();
move |event: &MouseDownEvent, window, cx| { move |event: &MouseDownEvent, window, cx| {
window.focus(&focus, cx); window.focus(&focus, cx);
cx.stop_propagation(); cx.stop_propagation();
if event.click_count >= 2 { if event.click_count >= 2 {
drag.update(cx, |drag, cx| { drag.update(cx, |drag, cx| {
drag.dragging = false; drag.dragging = false;
@@ -161,6 +168,7 @@ impl RenderOnce for ResizeHandle {
} }
return; return;
} }
drag.update(cx, |drag, cx| { drag.update(cx, |drag, cx| {
drag.dragging = true; drag.dragging = true;
drag.last = event.position; drag.last = event.position;
@@ -210,6 +218,7 @@ impl RenderOnce for ResizeHandle {
} else { } else {
border border
}; };
let line = match axis { let line = match axis {
Axis::Horizontal => { Axis::Horizontal => {
let x = bounds.center().x - px(0.5); let x = bounds.center().x - px(0.5);
@@ -226,6 +235,7 @@ impl RenderOnce for ResizeHandle {
) )
} }
}; };
window.paint_quad(fill(line, line_color)); window.paint_quad(fill(line, line_color));
window.on_mouse_event({ window.on_mouse_event({
@@ -235,23 +245,29 @@ impl RenderOnce for ResizeHandle {
if phase != DispatchPhase::Bubble { if phase != DispatchPhase::Bubble {
return; return;
} }
let (dragging, last) = { let (dragging, last) = {
let drag = drag.read(cx); let drag = drag.read(cx);
(drag.dragging, drag.last) (drag.dragging, drag.last)
}; };
if !dragging { if !dragging {
return; return;
} }
let delta = match axis { let delta = match axis {
Axis::Horizontal => event.position.x - last.x, Axis::Horizontal => event.position.x - last.x,
Axis::Vertical => event.position.y - last.y, Axis::Vertical => event.position.y - last.y,
}; };
drag.update(cx, |drag, _| drag.last = event.position); drag.update(cx, |drag, _| drag.last = event.position);
if let Some(on_resize) = on_resize.clone() { if let Some(on_resize) = on_resize.clone() {
on_resize(delta, window, cx); on_resize(delta, window, cx);
} }
} }
}); });
window.on_mouse_event({ window.on_mouse_event({
let drag = drag.clone(); let drag = drag.clone();
move |event: &MouseUpEvent, phase, _, cx| { move |event: &MouseUpEvent, phase, _, cx| {
@@ -260,6 +276,7 @@ impl RenderOnce for ResizeHandle {
{ {
return; return;
} }
if drag.read(cx).dragging { if drag.read(cx).dragging {
drag.update(cx, |drag, cx| { drag.update(cx, |drag, cx| {
drag.dragging = false; drag.dragging = false;
@@ -273,6 +290,7 @@ impl RenderOnce for ResizeHandle {
) )
.absolute() .absolute()
.inset_0(), .inset_0(),
),
) )
} }
} }
+2 -1
View File
@@ -274,8 +274,9 @@ fn splitting_at_a_target_pane_places_the_new_pane_there() {
let moved = bounds(&mut visual, "test-dock-pane-3").expect("the new pane missing"); let moved = bounds(&mut visual, "test-dock-pane-3").expect("the new pane missing");
let target = bounds(&mut visual, "test-dock-pane-1").expect("target pane missing"); let target = bounds(&mut visual, "test-dock-pane-1").expect("target pane missing");
// The flex split may round the shared seam by a hair.
assert!( assert!(
moved.right() <= target.left(), moved.right() <= target.left() + px(1.),
"the new pane must sit left of the anchored pane" "the new pane must sit left of the anchored pane"
); );
assert!(bounds(&mut visual, "test-dock-pane-0").is_none()); assert!(bounds(&mut visual, "test-dock-pane-0").is_none());
+7 -16
View File
@@ -4,7 +4,7 @@ use gpui::{
VisualTestContext, Window, div, point, px, VisualTestContext, Window, div, point, px,
}; };
use gpui_native_layout::resizable::{Resizable, ResizablePane, ResizableState}; use gpui_native_layout::resizable::{Resizable, ResizablePane, ResizableState};
use gpui_native_theme::{ActiveTheme as _, Size, Theme}; use gpui_native_theme::Theme;
const GROUP_WIDTH: f32 = 200.0; const GROUP_WIDTH: f32 = 200.0;
const GROUP_HEIGHT: f32 = 300.0; const GROUP_HEIGHT: f32 = 300.0;
@@ -84,20 +84,18 @@ fn a_drag_moves_the_pane_and_its_rendered_height() {
#[test] #[test]
fn a_drag_stops_where_the_remainder_keeps_its_floor() { fn a_drag_stops_where_the_remainder_keeps_its_floor() {
let (mut cx, state, mut visual) = split_window([px(100.)]); let (mut cx, state, mut visual) = split_window([px(100.)]);
let thickness = cx.update(|cx| cx.theme().sizes.resolve(Size::default()).dense_padding_x);
let floor = ResizablePane::DEFAULT_MIN; let floor = ResizablePane::DEFAULT_MIN;
drag_divider(&mut visual, px(1000.)); drag_divider(&mut visual, px(1000.));
state.update(&mut cx, |state, _| { state.update(&mut cx, |state, _| {
assert_eq!(state.sizes(), &[px(GROUP_HEIGHT) - thickness - floor]); assert_eq!(state.sizes(), &[px(GROUP_HEIGHT) - floor]);
}); });
} }
#[test] #[test]
fn a_double_click_resets_the_pane_to_the_equal_split() { fn a_double_click_resets_the_pane_to_the_equal_split() {
let (mut cx, state, mut visual) = split_window([px(240.)]); let (mut cx, state, mut visual) = split_window([px(240.)]);
let thickness = cx.update(|cx| cx.theme().sizes.resolve(Size::default()).dense_padding_x);
let start = divider_center(&mut visual); let start = divider_center(&mut visual);
visual.simulate_event(MouseDownEvent { visual.simulate_event(MouseDownEvent {
@@ -109,10 +107,7 @@ fn a_double_click_resets_the_pane_to_the_equal_split() {
}); });
state.update(&mut cx, |state, _| { state.update(&mut cx, |state, _| {
assert_eq!( assert_eq!(state.sizes(), &[px(GROUP_HEIGHT / 2.)]);
state.sizes(),
&[px((GROUP_HEIGHT - thickness.as_f32()) / 2.)]
);
}); });
} }
@@ -154,12 +149,8 @@ fn the_panes_and_the_divider_fill_the_group_exactly() {
let divider = visual let divider = visual
.debug_bounds("resize-handle") .debug_bounds("resize-handle")
.expect("divider rendered"); .expect("divider rendered");
assert_eq!( // The flex split may round the panes' shared edges by a hair.
first.size.height + divider.size.height + second.size.height, assert!((first.size.height + second.size.height - px(GROUP_HEIGHT)).abs() <= px(1.));
px(GROUP_HEIGHT) // The divider owns no layout space: it straddles the seam between the panes.
); assert!((divider.center().y - first.bottom()).abs() <= px(1.));
assert_eq!(
second.size.height,
px(GROUP_HEIGHT) - first.size.height - divider.size.height
);
} }
+14 -3
View File
@@ -1,6 +1,7 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
Axis, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _, Axis, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement as _,
Pixels, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, Pixels, Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px,
@@ -26,11 +27,17 @@ impl Render for Harness {
) -> impl gpui::IntoElement { ) -> impl gpui::IntoElement {
let deltas = self.deltas.clone(); let deltas = self.deltas.clone();
let resets = self.resets.clone(); let resets = self.resets.clone();
let horizontal = self.axis == Axis::Horizontal;
div() div()
.size_full() .size_full()
.flex() .flex()
.flex_row() .when(horizontal, |layout| layout.flex_row())
.child(div().w(px(120.)).h_full()) .when(!horizontal, |layout| layout.flex_col())
.child(if horizontal {
div().w(px(120.)).h_full()
} else {
div().h(px(120.)).w_full()
})
.child( .child(
ResizeHandle::new("test-handle") ResizeHandle::new("test-handle")
.axis(self.axis) .axis(self.axis)
@@ -38,7 +45,11 @@ impl Render for Harness {
.on_resize(move |delta, _, _| deltas.borrow_mut().push(delta)) .on_resize(move |delta, _, _| deltas.borrow_mut().push(delta))
.on_reset(move |_, _| *resets.borrow_mut() += 1), .on_reset(move |_, _| *resets.borrow_mut() += 1),
) )
.child(div().flex_1().h_full()) .child(if horizontal {
div().flex_1().h_full()
} else {
div().flex_1().w_full()
})
} }
} }