forked from reya/gpui-native-kit
fix: exclude scroll area padding from pane sizing
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+159
-141
@@ -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,153 +130,167 @@ impl RenderOnce for ResizeHandle {
|
|||||||
|
|
||||||
let on_reset = self.on_reset;
|
let on_reset = self.on_reset;
|
||||||
|
|
||||||
div()
|
deferred(
|
||||||
.id(self.id.clone())
|
div()
|
||||||
.debug_selector(|| "resize-handle".to_string())
|
.id(self.id.clone())
|
||||||
.role(Role::Splitter)
|
.debug_selector(|| "resize-handle".to_string())
|
||||||
.aria_label(label)
|
.role(Role::Splitter)
|
||||||
.flex_none()
|
.aria_label(label)
|
||||||
.when(axis == Axis::Horizontal, |handle| {
|
.flex_none()
|
||||||
handle.w(thickness).h_full()
|
.when(axis == Axis::Horizontal, |handle| {
|
||||||
})
|
handle.w(thickness).h_full().mx(straddle)
|
||||||
.when(axis == Axis::Vertical, |handle| {
|
})
|
||||||
handle.w_full().h(thickness)
|
.when(axis == Axis::Vertical, |handle| {
|
||||||
})
|
handle.w_full().h(thickness).my(straddle)
|
||||||
.when(self.disabled, |handle| handle.opacity(0.5))
|
})
|
||||||
.when(!self.disabled, |handle| {
|
.when(self.disabled, |handle| handle.opacity(0.5))
|
||||||
handle
|
.when(!self.disabled, |handle| {
|
||||||
.track_focus(&focus)
|
handle
|
||||||
.focusable()
|
.track_focus(&focus)
|
||||||
.tab_stop(true)
|
.focusable()
|
||||||
.focus_visible(|style| style.shadow(focus_ring(ring)))
|
.tab_stop(true)
|
||||||
.on_mouse_down(MouseButton::Left, {
|
.focus_visible(|style| style.shadow(focus_ring(ring)))
|
||||||
let drag = drag.clone();
|
.on_mouse_down(MouseButton::Left, {
|
||||||
let focus = focus.clone();
|
let drag = drag.clone();
|
||||||
move |event: &MouseDownEvent, window, cx| {
|
let focus = focus.clone();
|
||||||
window.focus(&focus, cx);
|
|
||||||
cx.stop_propagation();
|
move |event: &MouseDownEvent, window, cx| {
|
||||||
if event.click_count >= 2 {
|
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.update(cx, |drag, cx| {
|
||||||
drag.dragging = false;
|
drag.dragging = true;
|
||||||
|
drag.last = event.position;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
});
|
||||||
if let Some(on_reset) = on_reset.clone() {
|
|
||||||
on_reset(window, cx);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
drag.update(cx, |drag, cx| {
|
})
|
||||||
drag.dragging = true;
|
.on_key_down({
|
||||||
drag.last = event.position;
|
let on_resize = self.on_resize.clone();
|
||||||
cx.notify();
|
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)),
|
||||||
.on_key_down({
|
(Axis::Vertical, "up") => Some(px(-KEYBOARD_STEP)),
|
||||||
let on_resize = self.on_resize.clone();
|
(Axis::Vertical, "down") => Some(px(KEYBOARD_STEP)),
|
||||||
move |event: &KeyDownEvent, window, cx| {
|
_ => None,
|
||||||
let delta = match (axis, event.keystroke.key.as_str()) {
|
};
|
||||||
(Axis::Horizontal, "left") => Some(px(-KEYBOARD_STEP)),
|
if let Some(delta) = delta {
|
||||||
(Axis::Horizontal, "right") => Some(px(KEYBOARD_STEP)),
|
cx.stop_propagation();
|
||||||
(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);
|
|
||||||
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({
|
})
|
||||||
let drag = drag.clone();
|
})
|
||||||
move |event: &MouseUpEvent, phase, _, cx| {
|
.child(
|
||||||
if phase != DispatchPhase::Bubble
|
canvas(
|
||||||
|| event.button != MouseButton::Left
|
{
|
||||||
{
|
move |bounds, window, _| {
|
||||||
return;
|
let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
|
||||||
}
|
(hitbox, bounds)
|
||||||
if drag.read(cx).dragging {
|
}
|
||||||
drag.update(cx, |drag, cx| {
|
},
|
||||||
drag.dragging = false;
|
{
|
||||||
cx.notify();
|
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
|
||||||
.absolute()
|
} else {
|
||||||
.inset_0(),
|
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(),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user