use gpui::{point, px, Pixels, Point, Size};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AnchorCorner {
#[default]
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl AnchorCorner {
pub fn flip_horizontal(self) -> Self {
match self {
Self::TopLeft => Self::TopRight,
Self::TopRight => Self::TopLeft,
Self::BottomLeft => Self::BottomRight,
Self::BottomRight => Self::BottomLeft,
}
}
pub fn flip_vertical(self) -> Self {
match self {
Self::TopLeft => Self::BottomLeft,
Self::TopRight => Self::BottomRight,
Self::BottomLeft => Self::TopLeft,
Self::BottomRight => Self::TopRight,
}
}
pub fn opposite(self) -> Self {
match self {
Self::TopLeft => Self::BottomRight,
Self::TopRight => Self::BottomLeft,
Self::BottomLeft => Self::TopRight,
Self::BottomRight => Self::TopLeft,
}
}
pub fn is_top(self) -> bool {
matches!(self, Self::TopLeft | Self::TopRight)
}
pub fn is_bottom(self) -> bool {
matches!(self, Self::BottomLeft | Self::BottomRight)
}
pub fn is_left(self) -> bool {
matches!(self, Self::TopLeft | Self::BottomLeft)
}
pub fn is_right(self) -> bool {
matches!(self, Self::TopRight | Self::BottomRight)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum AnchorEdge {
Top,
#[default]
Bottom,
Left,
Right,
}
impl AnchorEdge {
pub fn opposite(self) -> Self {
match self {
Self::Top => Self::Bottom,
Self::Bottom => Self::Top,
Self::Left => Self::Right,
Self::Right => Self::Left,
}
}
pub fn is_horizontal(self) -> bool {
matches!(self, Self::Left | Self::Right)
}
pub fn is_vertical(self) -> bool {
matches!(self, Self::Top | Self::Bottom)
}
pub fn to_anchor_corner(self, align_start: bool) -> AnchorCorner {
match (self, align_start) {
(Self::Top, true) => AnchorCorner::BottomLeft,
(Self::Top, false) => AnchorCorner::BottomRight,
(Self::Bottom, true) => AnchorCorner::TopLeft,
(Self::Bottom, false) => AnchorCorner::TopRight,
(Self::Left, true) => AnchorCorner::TopRight,
(Self::Left, false) => AnchorCorner::BottomRight,
(Self::Right, true) => AnchorCorner::TopLeft,
(Self::Right, false) => AnchorCorner::BottomLeft,
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum FitMode {
#[default]
SwitchAnchor,
SnapToViewport,
None,
}
#[derive(Debug, Clone)]
pub struct PortalPosition {
pub anchor_corner: AnchorCorner,
pub preferred_edge: AnchorEdge,
pub offset: Point<Pixels>,
pub fit_mode: FitMode,
pub viewport_margin: Pixels,
}
impl Default for PortalPosition {
fn default() -> Self {
Self {
anchor_corner: AnchorCorner::TopLeft,
preferred_edge: AnchorEdge::Bottom,
offset: point(px(0.), px(0.)),
fit_mode: FitMode::SwitchAnchor,
viewport_margin: px(8.),
}
}
}
impl PortalPosition {
pub fn new() -> Self {
Self::default()
}
pub fn anchor(mut self, corner: AnchorCorner) -> Self {
self.anchor_corner = corner;
self
}
pub fn preferred_edge(mut self, edge: AnchorEdge) -> Self {
self.preferred_edge = edge;
self.anchor_corner = edge.to_anchor_corner(true);
self
}
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
self.offset = offset;
self
}
pub fn fit_mode(mut self, mode: FitMode) -> Self {
self.fit_mode = mode;
self
}
pub fn viewport_margin(mut self, margin: Pixels) -> Self {
self.viewport_margin = margin;
self
}
pub fn tooltip() -> Self {
Self::new()
.preferred_edge(AnchorEdge::Top)
.offset(point(px(0.), px(-4.)))
}
pub fn dropdown() -> Self {
Self::new()
.preferred_edge(AnchorEdge::Bottom)
.offset(point(px(0.), px(4.)))
}
pub fn popover_right() -> Self {
Self::new()
.preferred_edge(AnchorEdge::Right)
.offset(point(px(4.), px(0.)))
}
pub fn popover_left() -> Self {
Self::new()
.preferred_edge(AnchorEdge::Left)
.offset(point(px(-4.), px(0.)))
}
pub fn calculate_position(
&self,
trigger_origin: Point<Pixels>,
trigger_size: Size<Pixels>,
portal_size: Size<Pixels>,
viewport_size: Size<Pixels>,
) -> Point<Pixels> {
let anchor_point = self.get_anchor_point_on_trigger(trigger_origin, trigger_size);
let mut position = self.position_from_anchor(anchor_point, portal_size);
position.x = position.x + self.offset.x;
position.y = position.y + self.offset.y;
match self.fit_mode {
FitMode::SwitchAnchor => {
position = self.apply_switch_anchor(
position,
portal_size,
viewport_size,
trigger_origin,
trigger_size,
);
}
FitMode::SnapToViewport => {
position = self.apply_snap_to_viewport(position, portal_size, viewport_size);
}
FitMode::None => {}
}
position
}
fn get_anchor_point_on_trigger(
&self,
trigger_origin: Point<Pixels>,
trigger_size: Size<Pixels>,
) -> Point<Pixels> {
match self.preferred_edge {
AnchorEdge::Top => point(trigger_origin.x + trigger_size.width / 2., trigger_origin.y),
AnchorEdge::Bottom => point(
trigger_origin.x + trigger_size.width / 2.,
trigger_origin.y + trigger_size.height,
),
AnchorEdge::Left => point(
trigger_origin.x,
trigger_origin.y + trigger_size.height / 2.,
),
AnchorEdge::Right => point(
trigger_origin.x + trigger_size.width,
trigger_origin.y + trigger_size.height / 2.,
),
}
}
fn position_from_anchor(
&self,
anchor_point: Point<Pixels>,
portal_size: Size<Pixels>,
) -> Point<Pixels> {
match self.anchor_corner {
AnchorCorner::TopLeft => anchor_point,
AnchorCorner::TopRight => point(anchor_point.x - portal_size.width, anchor_point.y),
AnchorCorner::BottomLeft => point(anchor_point.x, anchor_point.y - portal_size.height),
AnchorCorner::BottomRight => point(
anchor_point.x - portal_size.width,
anchor_point.y - portal_size.height,
),
}
}
fn apply_switch_anchor(
&self,
position: Point<Pixels>,
portal_size: Size<Pixels>,
viewport_size: Size<Pixels>,
trigger_origin: Point<Pixels>,
trigger_size: Size<Pixels>,
) -> Point<Pixels> {
let mut result = position;
let mut current_corner = self.anchor_corner;
let right_edge = result.x + portal_size.width;
let overflows_right = right_edge > viewport_size.width;
let overflows_left = result.x < px(0.);
if overflows_right && !overflows_left {
current_corner = current_corner.flip_horizontal();
let anchor_point =
self.get_anchor_point_for_corner(current_corner, trigger_origin, trigger_size);
result = self.position_from_anchor_corner(anchor_point, portal_size, current_corner);
result.x = result.x + self.offset.x;
} else if overflows_left && !overflows_right {
current_corner = current_corner.flip_horizontal();
let anchor_point =
self.get_anchor_point_for_corner(current_corner, trigger_origin, trigger_size);
result = self.position_from_anchor_corner(anchor_point, portal_size, current_corner);
result.x = result.x + self.offset.x;
}
let bottom_edge = result.y + portal_size.height;
let overflows_bottom = bottom_edge > viewport_size.height;
let overflows_top = result.y < px(0.);
if overflows_bottom && !overflows_top {
current_corner = current_corner.flip_vertical();
let anchor_point =
self.get_anchor_point_for_corner(current_corner, trigger_origin, trigger_size);
result = self.position_from_anchor_corner(anchor_point, portal_size, current_corner);
result.y = result.y - self.offset.y; } else if overflows_top && !overflows_bottom {
current_corner = current_corner.flip_vertical();
let anchor_point =
self.get_anchor_point_for_corner(current_corner, trigger_origin, trigger_size);
result = self.position_from_anchor_corner(anchor_point, portal_size, current_corner);
result.y = result.y - self.offset.y;
}
result
}
fn get_anchor_point_for_corner(
&self,
corner: AnchorCorner,
trigger_origin: Point<Pixels>,
trigger_size: Size<Pixels>,
) -> Point<Pixels> {
match corner {
AnchorCorner::TopLeft => {
point(trigger_origin.x, trigger_origin.y + trigger_size.height)
}
AnchorCorner::TopRight => point(
trigger_origin.x + trigger_size.width,
trigger_origin.y + trigger_size.height,
),
AnchorCorner::BottomLeft => trigger_origin,
AnchorCorner::BottomRight => {
point(trigger_origin.x + trigger_size.width, trigger_origin.y)
}
}
}
fn position_from_anchor_corner(
&self,
anchor_point: Point<Pixels>,
portal_size: Size<Pixels>,
corner: AnchorCorner,
) -> Point<Pixels> {
match corner {
AnchorCorner::TopLeft => anchor_point,
AnchorCorner::TopRight => point(anchor_point.x - portal_size.width, anchor_point.y),
AnchorCorner::BottomLeft => point(anchor_point.x, anchor_point.y - portal_size.height),
AnchorCorner::BottomRight => point(
anchor_point.x - portal_size.width,
anchor_point.y - portal_size.height,
),
}
}
fn apply_snap_to_viewport(
&self,
position: Point<Pixels>,
portal_size: Size<Pixels>,
viewport_size: Size<Pixels>,
) -> Point<Pixels> {
let mut result = position;
let margin = self.viewport_margin;
let right_edge = result.x + portal_size.width;
if right_edge > viewport_size.width - margin {
result.x = viewport_size.width - portal_size.width - margin;
}
if result.x < margin {
result.x = margin;
}
let bottom_edge = result.y + portal_size.height;
if bottom_edge > viewport_size.height - margin {
result.y = viewport_size.height - portal_size.height - margin;
}
if result.y < margin {
result.y = margin;
}
result
}
}
pub trait Portal {
fn position(&self) -> &PortalPosition;
fn with_position(self, position: PortalPosition) -> Self;
fn preferred_edge(self, edge: AnchorEdge) -> Self
where
Self: Sized,
{
let mut position = self.position().clone();
position = position.preferred_edge(edge);
self.with_position(position)
}
fn offset(self, offset: Point<Pixels>) -> Self
where
Self: Sized,
{
let mut position = self.position().clone();
position = position.offset(offset);
self.with_position(position)
}
fn fit_mode(self, mode: FitMode) -> Self
where
Self: Sized,
{
let mut position = self.position().clone();
position = position.fit_mode(mode);
self.with_position(position)
}
}