Files
zed/crates/picker/src/shape.rs
T
Piotr Osiewicz c24358d96c deps: Bump pathfinder_simd & fix upcoming rustc warnings (#62170)
While building Zed with nightly rustc I've noticed it doesn't compile
because of good old pathfinder_simd. It also emits a bunch of warnings
about use of f64 literals where f32 is expected, so I've fixed them - it
should make future upgrades more straightforward.
2026-08-07 10:16:57 +00:00

679 lines
22 KiB
Rust

use gpui::Window;
use gpui::{Pixels, Rems, Size};
use ui::{Div, Styled, rems_from_px};
use crate::preview::Layout;
#[derive(Debug, Clone, Copy)]
pub(crate) struct PositionAndShape {
/// Absolute position of left most side of the picker
pub(crate) left: Pixels,
/// Absolute position of right most side of the picker
pub(crate) right: Pixels,
/// Absolute position of top most side of the picker
pub(crate) top: Pixels,
/// Absolute position of bottom most side of the picker
pub(crate) bottom: Pixels,
/// Relative position of divide between results and preview,
/// either a height or a width depends on previews layoutmode.
/// Should be zero when preview is disabled or hidden
pub(crate) preview: Pixels,
}
impl PositionAndShape {
pub(crate) fn width(&self) -> Pixels {
self.right - self.left
}
}
macro_rules! relative_size {
($name:ident, $accessor:ident) => {
/// Size type that is the sum of a relative size to the viewport and a
/// size relative to the font size (Rems). You can
/// add/subtract/multiple/divide to your harts content but once you
/// need a single unit you must provide a window to get it.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct $name {
viewport_fraction: f32,
rems: Rems,
}
impl From<Rems> for $name {
fn from(v: Rems) -> Self {
Self::rems(v)
}
}
impl $name {
pub const FULL: Self = Self {
viewport_fraction: 1.0,
rems: Rems::ZERO,
};
pub const fn viewport(fraction: f32) -> Self {
debug_assert!(fraction <= 1.0);
debug_assert!(fraction >= 0.0);
Self {
viewport_fraction: fraction.clamp(0.0, 1.0),
rems: Rems::ZERO,
}
}
pub const fn rems(val: Rems) -> Self {
Self {
viewport_fraction: 0.0,
rems: val,
}
}
pub fn as_pixels(&self, window: &Window) -> Pixels {
self.viewport_fraction * window.viewport_size().$accessor
+ self.rems * window.rem_size()
}
pub fn from_pixels(width: Pixels, window: &Window) -> Self {
Self {
viewport_fraction: width / window.viewport_size().$accessor,
rems: Rems::ZERO,
}
}
/// Returns this size as [`Rems`] when it has no viewport-relative
/// component. Used to derive a rems-based minimum from an initial
/// size without needing a [`Window`].
pub fn as_rems(&self) -> Option<Rems> {
(self.viewport_fraction == 0.0).then_some(self.rems)
}
pub fn as_viewport_fraction(&self, window: &Window) -> ViewportFraction {
ViewportFraction(
self.viewport_fraction
+ self.rems * window.rem_size() / window.viewport_size().$accessor,
)
}
}
impl std::ops::Add for $name {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
viewport_fraction: self.viewport_fraction + rhs.viewport_fraction,
rems: self.rems + rhs.rems,
}
}
}
impl std::ops::Sub for $name {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
Self {
viewport_fraction: self.viewport_fraction - rhs.viewport_fraction,
rems: self.rems - rhs.rems,
}
}
}
impl std::ops::Sub<Rems> for $name {
type Output = Self;
fn sub(self, rhs: Rems) -> Self::Output {
Self {
viewport_fraction: self.viewport_fraction,
rems: self.rems - rhs,
}
}
}
impl std::ops::Div<f32> for $name {
type Output = Self;
fn div(mut self, rhs: f32) -> Self::Output {
self.viewport_fraction /= rhs;
self.rems = Rems(self.rems.0 / rhs);
self
}
}
impl std::ops::Mul<f32> for $name {
type Output = Self;
fn mul(mut self, rhs: f32) -> Self::Output {
self.viewport_fraction *= rhs;
self.rems = Rems(self.rems.0 * rhs);
self
}
}
};
}
relative_size!(RelativeHeight, height);
relative_size!(RelativeWidth, width);
#[derive(Debug, Clone, Copy)]
pub struct ViewportFraction(f32);
impl ViewportFraction {
pub(crate) const ZERO: Self = Self(0.0);
pub(crate) fn fraction(v: f32) -> Self {
debug_assert!(
(0.0..=1.0).contains(&v),
"ViewportFraction must be between zero and one"
);
Self(v)
}
pub(crate) fn width_as_pixels(&self, window: &Window) -> Pixels {
window.viewport_size().width * self.0
}
pub(crate) fn height_as_pixels(&self, window: &Window) -> Pixels {
window.viewport_size().height * self.0
}
pub(crate) fn from_height_pixels(preview: Pixels, window: &Window) -> Self {
Self(preview / window.viewport_size().height)
}
pub(crate) fn from_width_pixels(preview: Pixels, window: &Window) -> Self {
Self(preview / window.viewport_size().width)
}
/// Returns the fraction of the viewport that this describes.
/// Guaranteed to be between zero and one
pub(crate) fn raw(&self) -> f32 {
self.0
}
}
impl std::ops::Mul<f32> for ViewportFraction {
type Output = Self;
fn mul(self, rhs: f32) -> Self::Output {
Self(self.0 * rhs)
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Centered {
pub(crate) width: RelativeWidth,
pub(crate) height: RelativeHeight,
pub(crate) preview_size: ViewportFraction,
}
impl Centered {
/// The default size for a plain picker (no preview): a fixed standard width
/// and a standard *max* height that the picker shrinks below when it has
/// little content.
pub(crate) fn simple() -> Self {
Centered {
width: RelativeWidth::rems(crate::DEFAULT_MODAL_WIDTH),
height: RelativeHeight::rems(crate::DEFAULT_MODAL_MAX_HEIGHT),
preview_size: ViewportFraction::ZERO,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Shape {
Resizing(PositionAndShape),
/// This may be persisted between zero and three times:
/// - for a user resized picker without preview
/// - for a user resized picker with preview below
/// - for a user resized picker with preview right
HorizontallyCentered(Centered),
}
#[derive(Debug)]
pub struct SizeBounds {
pub(crate) max_width: RelativeWidth,
pub(crate) max_height: RelativeHeight,
pub(crate) min_results: Size<Rems>,
/// Minimum size of the preview pane. Along the split axis this only needs to
/// be large enough to grab the divider and shrink it back.
pub(crate) min_preview: Size<Rems>,
}
impl Default for SizeBounds {
fn default() -> Self {
Self {
max_width: RelativeWidth::viewport(0.95),
// Modals are placed at 5 rems from the top we also do not want them to go
// over the lower bar so clear another 5 rems there.
max_height: (RelativeHeight::FULL - Rems(10.0)) * 0.95,
min_results: Size {
width: rems_from_px(280_f32),
height: rems_from_px(320_f32),
},
min_preview: Size {
width: rems_from_px(128_f32),
height: rems_from_px(96_f32),
},
}
}
}
impl SizeBounds {
/// Minimum total picker width for the given layout, composed from the
/// results and preview minimums (they stack along the split axis and share
/// the cross axis).
fn min_width(&self, layout: Option<Layout>, window: &Window) -> Pixels {
let rem = window.rem_size();
let results = self.min_results.width * rem;
let preview = self.min_preview.width * rem;
match layout {
Some(Layout::Right) => results + preview,
Some(Layout::Below) => results.max(preview),
Some(Layout::Hidden) | None => results,
}
}
/// Minimum total picker height for the given layout. See [`Self::min_width`].
fn min_height(&self, layout: Option<Layout>, window: &Window) -> Pixels {
let rem = window.rem_size();
let results = self.min_results.height * rem;
let preview = self.min_preview.height * rem;
match layout {
Some(Layout::Below) => results + preview,
Some(Layout::Right) => results.max(preview),
Some(Layout::Hidden) | None => results,
}
}
/// Clamps a width in pixels to the configured min/max width.
pub(crate) fn clamp_width(
&self,
width: Pixels,
layout: Option<Layout>,
window: &Window,
) -> Pixels {
width
.min(self.max_width.as_pixels(window))
.max(self.min_width(layout, window))
}
/// Clamps a height in pixels to the configured min/max height.
pub(crate) fn clamp_height(
&self,
height: Pixels,
layout: Option<Layout>,
window: &Window,
) -> Pixels {
height
.min(self.max_height.as_pixels(window))
.max(self.min_height(layout, window))
}
/// Clamps the picker's width by moving its left edge back into bounds.
///
/// For a preview-to-the-right drag the preview is corrected by the same
/// amount any snap moves the edge, so the results pane keeps its size at the
/// boundary.
pub(crate) fn clamp_left_edge(
&self,
working: &mut PositionAndShape,
layout: Option<Layout>,
window: &Window,
) {
let target_width = self.clamp_width(working.right - working.left, layout, window);
let new_left = working.right - target_width;
let correction = new_left - working.left;
working.left = new_left;
if layout == Some(Layout::Right) {
working.preview += correction;
}
}
/// Clamps the picker's width by moving its right edge back into bounds.
/// See [`Self::clamp_left_edge`].
pub(crate) fn clamp_right_edge(
&self,
working: &mut PositionAndShape,
layout: Option<Layout>,
window: &Window,
) {
let target_width = self.clamp_width(working.right - working.left, layout, window);
let new_right = working.left + target_width;
let correction = new_right - working.right;
working.right = new_right;
if layout == Some(Layout::Right) {
working.preview += correction;
}
}
/// Clamps the picker's height by moving its bottom edge back into bounds.
/// See [`Self::clamp_left_edge`].
pub(crate) fn clamp_bottom_edge(
&self,
working: &mut PositionAndShape,
layout: Option<Layout>,
window: &Window,
) {
let target_height = self.clamp_height(working.bottom - working.top, layout, window);
let new_bottom = working.top + target_height;
let correction = new_bottom - working.bottom;
working.bottom = new_bottom;
if layout == Some(Layout::Below) {
working.preview += correction;
}
}
/// Clamps the divider between the results and the preview so both panes stay
/// above their minimums. Runs last in each side's clamp so the divider gets
/// the final say after the outer edges have been bounded.
pub(crate) fn clamp_divider(
&self,
working: &mut PositionAndShape,
layout: Option<Layout>,
window: &Window,
) {
let rem = window.rem_size();
let (total, min_preview, min_results) = match layout {
Some(Layout::Right) => (
working.right - working.left,
self.min_preview.width * rem,
self.min_results.width * rem,
),
Some(Layout::Below) => (
working.bottom - working.top,
self.min_preview.height * rem,
self.min_results.height * rem,
),
Some(Layout::Hidden) | None => return,
};
let max_preview = (total - min_results).max(min_preview);
working.preview = working.preview.clamp(min_preview, max_preview);
}
pub(crate) fn would_clamp_width_if_horizontal(&self, shape: &Shape, window: &Window) -> bool {
let min_width = self.min_width(Some(Layout::Right), window);
let unbounded_width = shape
.picker_position_and_size(Some(Layout::Right), window)
.width();
unbounded_width <= min_width
}
/// Clamps a whole picker rect (results + preview) into bounds: the total size
/// against the per-layout min/max, then the divider so both panes keep their
/// minimums. Width is clamped about its center, height anchored at the top.
///
/// This is idempotent on an already-bounded rect, so it can run on every
/// render and drag-start read without shifting an in-bounds shape.
pub(crate) fn clamp_position_and_size(
&self,
working: &mut PositionAndShape,
layout: Option<Layout>,
window: &Window,
) {
let target_width = self.clamp_width(working.right - working.left, layout, window);
let center = (working.left + working.right) / 2.0;
working.left = center - target_width / 2.0;
working.right = center + target_width / 2.0;
let target_height = self.clamp_height(working.bottom - working.top, layout, window);
working.bottom = working.top + target_height;
self.clamp_divider(working, layout, window);
}
}
impl Shape {
pub(crate) fn picker_position_and_size(
&self,
layout: impl Into<Option<Layout>>,
window: &Window,
) -> PositionAndShape {
match self {
Shape::Resizing(pos) => *pos,
Shape::HorizontallyCentered(Centered {
width,
height,
preview_size,
}) => PositionAndShape {
// W V: full width xxxxx: picker modal
// -----xxxxx------ left = (V - W) / 2
// L R right = left + W = (V/2 - W/2) + W = V/2 + W/2
left: ((RelativeWidth::FULL - *width) / 2.0).as_pixels(window),
right: (RelativeWidth::FULL / 2.0 + *width / 2.0).as_pixels(window),
top: Pixels::ZERO,
bottom: height.as_pixels(window),
preview: match layout.into() {
Some(Layout::Below) => preview_size.height_as_pixels(window),
Some(Layout::Right) => preview_size.width_as_pixels(window),
Some(Layout::Hidden) | None => Pixels::ZERO,
},
},
}
}
/// The picker rect with all bounds applied (total size and divider). This is
/// the single source of truth for sizing during render and for seeding a
/// drag, so a shape left dirty (e.g. after a layout toggle changes the
/// minimums) is sanitized identically everywhere and never jumps on the
/// first drag.
pub(crate) fn clamped_position_and_size(
&self,
layout: Option<Layout>,
bounds: &SizeBounds,
window: &Window,
) -> PositionAndShape {
let mut pos = self.picker_position_and_size(layout, window);
bounds.clamp_position_and_size(&mut pos, layout, window);
pos
}
pub(crate) fn results_position_and_size(
&self,
layout: Layout,
bounds: &SizeBounds,
window: &Window,
) -> PositionAndShape {
let mut pos = self.clamped_position_and_size(Some(layout), bounds, window);
match layout {
Layout::Below => pos.bottom -= pos.preview,
Layout::Right => pos.right -= pos.preview,
Layout::Hidden => (),
}
pos
}
pub(crate) fn preview_position_and_size(
&self,
layout: Layout,
bounds: &SizeBounds,
window: &Window,
) -> PositionAndShape {
let mut pos = self.clamped_position_and_size(Some(layout), bounds, window);
match layout {
Layout::Below => pos.top = pos.bottom - pos.preview,
Layout::Right => pos.left = pos.right - pos.preview,
Layout::Hidden => (),
}
pos
}
/// How far the center of the picker has been moved during dragging
/// this allows extending it on one side without the picker centering during
/// the resize. The drag is clamped to the size bounds (see
/// [`Side::current_position_and_shape`]), so the center stays in bounds too.
pub(crate) fn horizontal_offset(&self, window: &Window) -> Pixels {
let Shape::Resizing(PositionAndShape { left, right, .. }) = self else {
return Pixels::ZERO; // picker should be centered
};
let center = (*left + *right) / 2.0;
let viewport_center = window.viewport_size().width / 2.0;
center - viewport_center // shifting the picker by this uncenters it again
}
pub(crate) fn apply_results_size(
&self,
layout: impl Into<Option<Layout>>,
bounds: &SizeBounds,
fill_height: bool,
div: Div,
window: &Window,
) -> Div {
let layout = layout.into();
// Work from the total picker size (full) to keep the divider from
// growing the picker when dragged.
let full = self.clamped_position_and_size(layout, bounds, window);
let width = match layout {
Some(Layout::Right) => (full.right - full.left) - full.preview,
_ => full.right - full.left,
};
let div = div.w(width);
if fill_height {
let height = match layout {
Some(Layout::Below) => (full.bottom - full.top) - full.preview,
_ => full.bottom - full.top,
};
div.h(height)
} else {
div
}
}
pub(crate) fn results_max_height(
&self,
bounds: &SizeBounds,
fill_height: bool,
window: &Window,
) -> Option<Pixels> {
if fill_height {
None
} else {
Some(self.height(None, bounds, window))
}
}
/// The clamped total height of the picker for the given layout.
pub(crate) fn height(
&self,
layout: Option<Layout>,
bounds: &SizeBounds,
window: &Window,
) -> Pixels {
let pos = self.clamped_position_and_size(layout, bounds, window);
pos.bottom - pos.top
}
pub(crate) fn apply_height(
&self,
layout: Option<Layout>,
bounds: &SizeBounds,
div: Div,
window: &Window,
) -> Div {
div.h(self.height(layout, bounds, window))
}
pub(crate) fn results_height(
&self,
layout: Layout,
bounds: &SizeBounds,
window: &Window,
) -> Pixels {
let pos = self.results_position_and_size(layout, bounds, window);
pos.bottom - pos.top
}
pub(crate) fn preview_width(
&self,
layout: Layout,
bounds: &SizeBounds,
window: &Window,
) -> Pixels {
let pos = self.preview_position_and_size(layout, bounds, window);
pos.right - pos.left
}
pub(crate) fn preview_height(
&self,
layout: Layout,
bounds: &SizeBounds,
window: &Window,
) -> Pixels {
let pos = self.preview_position_and_size(layout, bounds, window);
pos.bottom - pos.top
}
pub(crate) fn centered_and_relative(
pos: PositionAndShape,
layout: impl Into<Option<Layout>>,
window: &Window,
) -> Centered {
use Layout as Pl;
let preview_size = match layout.into() {
Some(Pl::Below) => ViewportFraction::from_height_pixels(pos.preview, window),
Some(Pl::Right) => ViewportFraction::from_width_pixels(pos.preview, window),
Some(Pl::Hidden) | None => ViewportFraction::ZERO,
};
Centered {
width: RelativeWidth::from_pixels(pos.right - pos.left, window),
height: RelativeHeight::from_pixels(pos.bottom - pos.top, window),
preview_size,
}
}
pub(crate) fn set_initial_width(&mut self, w: impl Into<RelativeWidth>) {
if let Shape::HorizontallyCentered(Centered { width, .. }) = self {
*width = w.into();
}
}
pub(crate) fn set_initial_height(&mut self, h: impl Into<RelativeHeight>) {
if let Shape::HorizontallyCentered(Centered { height, .. }) = self {
*height = h.into();
}
}
pub(crate) fn reset_width(&mut self, default: &Centered) {
if let Shape::HorizontallyCentered(Centered { width, .. }) = self {
*width = default.width;
}
}
pub(crate) fn reset_height(&mut self, default: &Centered) {
if let Shape::HorizontallyCentered(Centered { height, .. }) = self {
*height = default.height;
}
}
pub(crate) fn center_divider(&mut self, layout: Layout, window: &Window) {
if let Shape::HorizontallyCentered(Centered {
width,
height,
preview_size,
}) = self
{
let total = match layout {
Layout::Right => width.as_viewport_fraction(window),
Layout::Below => height.as_viewport_fraction(window),
Layout::Hidden => return,
};
*preview_size = total * 0.5;
}
}
}
impl Default for Centered {
fn default() -> Self {
Centered {
width: RelativeWidth::viewport(0.6),
height: RelativeHeight::viewport(0.6),
preview_size: ViewportFraction::fraction(0.3),
}
}
}
impl Default for Shape {
fn default() -> Self {
Self::HorizontallyCentered(Centered::default())
}
}