feat: add resizable component
This commit is contained in:
@@ -6,6 +6,9 @@ pub mod dock;
|
||||
#[cfg(feature = "resize_handle")]
|
||||
pub mod resize_handle;
|
||||
|
||||
#[cfg(feature = "resizable")]
|
||||
pub mod resizable;
|
||||
|
||||
#[cfg(feature = "shell")]
|
||||
pub mod shell;
|
||||
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
//! The resizable group: stacked panes with drag dividers, outside the dock.
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, Axis, Bounds, Context, ElementId, Entity,
|
||||
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;
|
||||
|
||||
/// The size a pane starts at before the group has measured its extent.
|
||||
const DEFAULT_PANE_SIZE: f32 = 200.0;
|
||||
/// The floor a pane keeps while dragging unless the caller lowers it.
|
||||
const DEFAULT_PANE_MIN: f32 = 48.0;
|
||||
|
||||
type DebugSelector = std::rc::Rc<dyn Fn() -> String>;
|
||||
|
||||
/// The retained pane sizes of one resizable group.
|
||||
pub struct ResizableState {
|
||||
/// Sizes of every pane except the remainder pane, which fills what's left.
|
||||
sizes: Vec<Pixels>,
|
||||
/// The group's main-axis extent, captured at paint for drag clamping.
|
||||
measured: Option<Pixels>,
|
||||
}
|
||||
|
||||
impl ResizableState {
|
||||
/// Creates state whose panes start at the default size.
|
||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|_| Self {
|
||||
sizes: Vec::new(),
|
||||
measured: None,
|
||||
})
|
||||
}
|
||||
|
||||
/// The stored size of each fixed pane; the remainder pane isn't listed.
|
||||
pub fn sizes(&self) -> &[Pixels] {
|
||||
&self.sizes
|
||||
}
|
||||
|
||||
/// Restores pane sizes, e.g. from a persisted layout.
|
||||
pub fn set_sizes<I>(&mut self, sizes: I, cx: &mut Context<Self>)
|
||||
where
|
||||
I: IntoIterator<Item = Pixels>,
|
||||
{
|
||||
self.sizes = sizes.into_iter().collect();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Records the group's measured extent, notifying only when it changes.
|
||||
fn set_measured(&mut self, extent: Pixels, cx: &mut Context<Self>) {
|
||||
if self.measured != Some(extent) {
|
||||
self.measured = Some(extent);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// The equal split of the measured extent.
|
||||
fn equal_split(&self, pane_count: usize, thickness: Pixels) -> Pixels {
|
||||
match self.measured {
|
||||
Some(extent) => (extent - thickness * (pane_count as f32 - 1.0)) / 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 {
|
||||
self.sizes
|
||||
.get(ix)
|
||||
.copied()
|
||||
.unwrap_or_else(|| self.equal_split(pane_count, thickness))
|
||||
}
|
||||
|
||||
/// 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>,
|
||||
) {
|
||||
let Some(&floor) = mins.get(ix) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let pane_count = mins.len();
|
||||
|
||||
let mut size = self.pane_size(ix, pane_count, thickness) + delta;
|
||||
size = size.max(floor);
|
||||
|
||||
if let Some(extent) = self.measured {
|
||||
let floors = mins
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(j, _)| *j != ix)
|
||||
.fold(px(0.), |sum, (_, min)| sum + *min);
|
||||
let ceiling = extent - thickness * (pane_count as f32 - 1.0) - floors;
|
||||
size = size.min(ceiling);
|
||||
}
|
||||
|
||||
while self.sizes.len() <= ix {
|
||||
self.sizes.push(px(DEFAULT_PANE_SIZE));
|
||||
}
|
||||
|
||||
self.sizes[ix] = size;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// 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);
|
||||
while self.sizes.len() <= ix {
|
||||
self.sizes.push(px(DEFAULT_PANE_SIZE));
|
||||
}
|
||||
self.sizes[ix] = split;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// One pane of a resizable group: its content and its drag floor.
|
||||
pub struct ResizablePane {
|
||||
content: AnyElement,
|
||||
min: Pixels,
|
||||
}
|
||||
|
||||
impl ResizablePane {
|
||||
/// The floor a pane keeps while dragging unless the caller lowers it.
|
||||
pub const DEFAULT_MIN: Pixels = px(DEFAULT_PANE_MIN);
|
||||
|
||||
/// Wraps this element as a pane.
|
||||
pub fn new<E>(content: E) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
Self {
|
||||
content: content.into_any_element(),
|
||||
min: Self::DEFAULT_MIN,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the floor this pane keeps while dragging.
|
||||
pub fn min(mut self, min: Pixels) -> Self {
|
||||
self.min = min;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A group of panes stacked along one axis, separated by drag dividers.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Resizable {
|
||||
id: ElementId,
|
||||
state: Entity<ResizableState>,
|
||||
axis: Axis,
|
||||
panes: Vec<ResizablePane>,
|
||||
debug_selector: Option<DebugSelector>,
|
||||
style: StyleRefinement,
|
||||
}
|
||||
|
||||
impl Resizable {
|
||||
/// Creates a group stacking its panes vertically, bound to `state`.
|
||||
pub fn new<I>(id: I, state: Entity<ResizableState>) -> Self
|
||||
where
|
||||
I: Into<ElementId>,
|
||||
{
|
||||
Self {
|
||||
id: id.into(),
|
||||
state,
|
||||
axis: Axis::Vertical,
|
||||
panes: Vec::new(),
|
||||
debug_selector: None,
|
||||
style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the axis the panes stack along.
|
||||
pub fn axis(mut self, axis: Axis) -> Self {
|
||||
self.axis = axis;
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a pane with the default drag floor.
|
||||
pub fn pane<E>(mut self, content: E) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
self.panes.push(ResizablePane::new(content));
|
||||
self
|
||||
}
|
||||
|
||||
/// Adds a pane that can't be dragged below `min`.
|
||||
pub fn pane_with_min<E>(mut self, content: E, min: Pixels) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
self.panes.push(ResizablePane::new(content).min(min));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the debug-bounds selector of each pane, suffixed with its index.
|
||||
pub fn debug_selector<F>(mut self, selector: F) -> Self
|
||||
where
|
||||
F: Fn() -> String + 'static,
|
||||
{
|
||||
self.debug_selector = Some(std::rc::Rc::new(selector));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for Resizable {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
let pane_count = self.panes.len();
|
||||
let mins: Vec<Pixels> = self.panes.iter().map(|pane| pane.min).collect();
|
||||
let debug_selector = self.debug_selector;
|
||||
|
||||
let sizes: Vec<Pixels> = (0..pane_count)
|
||||
.map(|ix| state.read(cx).pane_size(ix, pane_count, thickness))
|
||||
.collect();
|
||||
|
||||
let mut group = div()
|
||||
.flex()
|
||||
.overflow_hidden()
|
||||
.when(axis == Axis::Vertical, |group| group.flex_col())
|
||||
.when(axis == Axis::Horizontal, |group| group.flex_row())
|
||||
.refine_style(&self.style);
|
||||
|
||||
for (ix, pane) in self.panes.into_iter().enumerate() {
|
||||
let remainder = ix + 1 == pane_count;
|
||||
let content = pane.content;
|
||||
let mut pane_element = if remainder {
|
||||
div()
|
||||
.flex_1()
|
||||
.overflow_hidden()
|
||||
.when(axis == Axis::Vertical, |p| p.min_h(pane.min))
|
||||
.when(axis == Axis::Horizontal, |p| p.min_w(pane.min))
|
||||
} else {
|
||||
div()
|
||||
.flex_none()
|
||||
.overflow_hidden()
|
||||
.when(axis == Axis::Vertical, |p| p.h(sizes[ix]).w_full())
|
||||
.when(axis == Axis::Horizontal, |p| p.w(sizes[ix]).h_full())
|
||||
};
|
||||
|
||||
if let Some(selector) = &debug_selector {
|
||||
let prefix = selector();
|
||||
pane_element = pane_element.debug_selector(move || format!("{prefix}-{ix}"));
|
||||
}
|
||||
|
||||
group = group.child(pane_element.child(content));
|
||||
|
||||
if !remainder {
|
||||
let divider =
|
||||
ResizeHandle::new(SharedString::from(format!("{}-divider-{ix}", self.id)))
|
||||
.axis(axis)
|
||||
.on_resize({
|
||||
let state = state.clone();
|
||||
let mins = mins.clone();
|
||||
move |delta, _, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.resize(ix, delta, &mins, thickness, 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)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
group = group.child(divider);
|
||||
}
|
||||
}
|
||||
|
||||
if pane_count > 0 {
|
||||
group = group.child(
|
||||
canvas(
|
||||
{
|
||||
let state = state.clone();
|
||||
move |bounds: Bounds<Pixels>, _, cx: &mut App| {
|
||||
let extent = match axis {
|
||||
Axis::Vertical => bounds.size.height,
|
||||
Axis::Horizontal => bounds.size.width,
|
||||
};
|
||||
state.update(cx, |state, cx| state.set_measured(extent, cx));
|
||||
}
|
||||
},
|
||||
|_, _, _, _| {},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
);
|
||||
}
|
||||
|
||||
group
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user