T03 layout: stacks, grids, surfaces, scroll, splits, sheets, dock, panes, workspace, viewport
This commit is contained in:
37 files changed
+5283
-50
No files matched your search
@@ -0,0 +1,232 @@
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use gpui::{
|
||||
AnyElement, App, Axis, CursorStyle, DragMoveEvent, ElementId, EmptyView, EntityId,
|
||||
InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce, Stateful,
|
||||
StatefulInteractiveElement, Styled, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::seeded::use_seeded;
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
/// Thin divider with a wide grab area. `axis` is the split's direction.
|
||||
pub fn resize_handle(id: impl Into<ElementId>, axis: Axis, cx: &App) -> Stateful<gpui::Div> {
|
||||
let theme = cx.theme();
|
||||
let (line, lit, hit) = (theme.colors.border, theme.colors.focus, theme.handle_hit());
|
||||
let base = div()
|
||||
.id(id)
|
||||
.group("resize")
|
||||
.flex_none()
|
||||
.flex()
|
||||
.justify_center()
|
||||
.items_center();
|
||||
match axis {
|
||||
Axis::Horizontal => base
|
||||
.w(hit)
|
||||
.h_full()
|
||||
.cursor(CursorStyle::ResizeLeftRight)
|
||||
.child(
|
||||
div()
|
||||
.h_full()
|
||||
.border_l_1()
|
||||
.border_color(line)
|
||||
.group_hover("resize", |s| s.border_color(lit)),
|
||||
),
|
||||
Axis::Vertical => base
|
||||
.h(hit)
|
||||
.w_full()
|
||||
.cursor(CursorStyle::ResizeUpDown)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.border_t_1()
|
||||
.border_color(line)
|
||||
.group_hover("resize", |s| s.border_color(lit)),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
struct DividerDrag {
|
||||
owner: EntityId,
|
||||
ix: usize,
|
||||
start: Vec<f32>,
|
||||
anchor: Rc<Cell<Option<Pixels>>>,
|
||||
}
|
||||
|
||||
/// Moves divider `ix` by `delta` of the whole, keeping each pane at `floor` or more.
|
||||
fn shift(start: &[f32], ix: usize, delta: f32, floor: f32) -> Vec<f32> {
|
||||
let pair = start[ix] + start[ix + 1];
|
||||
let floor = floor.min(pair / 2.0);
|
||||
let left = (start[ix] + delta).clamp(floor, pair - floor);
|
||||
let mut next = start.to_vec();
|
||||
next[ix] = left;
|
||||
next[ix + 1] = pair - left;
|
||||
next
|
||||
}
|
||||
|
||||
/// Weights are finite and non-negative, with a finite, positive sum.
|
||||
pub(crate) fn check_weights(weights: &[f32]) -> anyhow::Result<f32> {
|
||||
anyhow::ensure!(
|
||||
weights
|
||||
.iter()
|
||||
.all(|weight| weight.is_finite() && *weight >= 0.0),
|
||||
"split weights {weights:?} must be finite and non-negative"
|
||||
);
|
||||
let total: f32 = weights.iter().sum();
|
||||
anyhow::ensure!(
|
||||
total.is_finite() && total > 0.0,
|
||||
"split weights {weights:?} need a finite, positive sum"
|
||||
);
|
||||
Ok(total)
|
||||
}
|
||||
|
||||
type OnResize = Rc<dyn Fn(&[f32], &mut Window, &mut App)>;
|
||||
|
||||
/// Panes side by side or stacked, split by draggable handles.
|
||||
#[derive(IntoElement)]
|
||||
pub struct SplitPane {
|
||||
id: ElementId,
|
||||
axis: Axis,
|
||||
panes: Vec<AnyElement>,
|
||||
sizes: Option<Vec<f32>>,
|
||||
min: Pixels,
|
||||
on_resize: Option<OnResize>,
|
||||
}
|
||||
|
||||
impl SplitPane {
|
||||
/// `min` is the smallest a pane may shrink to.
|
||||
pub fn new(id: impl Into<ElementId>, axis: Axis, min: Pixels) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
axis,
|
||||
panes: Vec::new(),
|
||||
sizes: None,
|
||||
min,
|
||||
on_resize: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pane(mut self, pane: impl IntoElement) -> Self {
|
||||
self.panes.push(pane.into_any_element());
|
||||
self
|
||||
}
|
||||
|
||||
/// Shares, one per pane, normalized. A new value replaces dragged ones.
|
||||
pub fn sizes(mut self, sizes: &[f32]) -> Self {
|
||||
self.sizes = Some(sizes.to_vec());
|
||||
self
|
||||
}
|
||||
|
||||
/// Reports shares after each drag step.
|
||||
pub fn on_resize(mut self, handler: impl Fn(&[f32], &mut Window, &mut App) + 'static) -> Self {
|
||||
self.on_resize = Some(Rc::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for SplitPane {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let count = self.panes.len();
|
||||
assert!(count >= 2, "split pane {:?} needs two panes", self.id);
|
||||
let weights = self.sizes.unwrap_or_else(|| vec![1.0; count]);
|
||||
assert_eq!(
|
||||
weights.len(),
|
||||
count,
|
||||
"split pane {:?}: one size per pane",
|
||||
self.id
|
||||
);
|
||||
let total = check_weights(&weights)
|
||||
.unwrap_or_else(|error| panic!("split pane {:?}: {error:#}", self.id));
|
||||
let normalized: Vec<f32> = weights.iter().map(|weight| weight / total).collect();
|
||||
let fractions = use_seeded(self.id.clone(), normalized, window, cx);
|
||||
let owner = fractions.entity_id();
|
||||
let on_resize = self.on_resize;
|
||||
let shares = fractions.read(cx).value.clone();
|
||||
let (axis, min) = (self.axis, self.min);
|
||||
let mut root = div().id(self.id).size_full().flex().on_drag_move(
|
||||
move |event: &DragMoveEvent<DividerDrag>, window, cx| {
|
||||
let drag = event.drag(cx);
|
||||
if drag.owner != owner {
|
||||
return;
|
||||
}
|
||||
let (along, extent) = match axis {
|
||||
Axis::Horizontal => (event.event.position.x, event.bounds.size.width),
|
||||
Axis::Vertical => (event.event.position.y, event.bounds.size.height),
|
||||
};
|
||||
let anchor = match drag.anchor.get() {
|
||||
Some(anchor) => anchor,
|
||||
None => {
|
||||
drag.anchor.set(Some(along));
|
||||
along
|
||||
}
|
||||
};
|
||||
let next = shift(
|
||||
&drag.start,
|
||||
drag.ix,
|
||||
(along - anchor) / extent,
|
||||
min / extent,
|
||||
);
|
||||
if let Some(report) = &on_resize {
|
||||
report(&next, window, cx);
|
||||
}
|
||||
fractions.update(cx, |shares, cx| {
|
||||
shares.value = next;
|
||||
cx.notify();
|
||||
});
|
||||
},
|
||||
);
|
||||
root = match axis {
|
||||
Axis::Horizontal => root.flex_row(),
|
||||
Axis::Vertical => root.flex_col(),
|
||||
};
|
||||
let mut children = Vec::with_capacity(count * 2);
|
||||
for (ix, pane) in self.panes.into_iter().enumerate() {
|
||||
if ix > 0 {
|
||||
let drag = DividerDrag {
|
||||
owner,
|
||||
ix: ix - 1,
|
||||
start: shares.clone(),
|
||||
anchor: Rc::new(Cell::new(None)),
|
||||
};
|
||||
children.push(
|
||||
resize_handle(("divider", ix), axis, cx)
|
||||
.on_drag(drag, |_, _, _, cx| cx.new(|_| EmptyView))
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
let mut cell = div()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.flex_basis(gpui::relative(0.0));
|
||||
cell.style().flex_grow = Some(shares[ix]);
|
||||
children.push(cell.child(pane).into_any_element());
|
||||
}
|
||||
root.children(children)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{check_weights, shift};
|
||||
|
||||
#[test]
|
||||
fn weights_must_be_finite_non_negative_and_sum_positive() {
|
||||
assert_eq!(check_weights(&[1.0, 3.0]).unwrap(), 4.0);
|
||||
assert_eq!(check_weights(&[0.0, 1.0]).unwrap(), 1.0);
|
||||
assert!(check_weights(&[0.0, 0.0]).is_err());
|
||||
assert!(check_weights(&[-1.0, 2.0]).is_err());
|
||||
assert!(check_weights(&[f32::NAN, 1.0]).is_err());
|
||||
assert!(check_weights(&[f32::MAX, f32::MAX]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_moves_one_boundary_and_respects_the_floor() {
|
||||
let moved = shift(&[0.5, 0.5], 0, 0.1, 0.1);
|
||||
assert!((moved[0] - 0.6).abs() < 1e-6 && (moved[1] - 0.4).abs() < 1e-6);
|
||||
let floored = shift(&[0.5, 0.5], 0, -0.9, 0.2);
|
||||
assert!((floored[0] - 0.2).abs() < 1e-6);
|
||||
let three = shift(&[0.2, 0.3, 0.5], 1, 0.1, 0.05);
|
||||
assert!((three[1] - 0.4).abs() < 1e-6 && (three[2] - 0.4).abs() < 1e-6);
|
||||
assert!((three[0] - 0.2).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user