use crate::popover::{self, Chain, placement::Placement, submenu};
use gpui::{
Anchor, AnyElement, App, AvailableSpace, Bounds, Display, Element, FlexDirection,
GlobalElementId, InspectorElementId, IntoElement, LayoutId, Pixels, Point, Position,
SharedString, Style, Window, div, point, prelude::*, px, relative, size,
};
use theme::Theme;
pub(super) fn pinned_layer(layer: AnyElement) -> AnyElement {
div()
.absolute()
.top_0()
.left_0()
.size_0()
.child(layer)
.into_any_element()
}
pub(super) fn exit_progress(since: web_time::Instant) -> f32 {
let total = motion::MENU_OUT
.total()
.mul_f32(motion::speed_scale())
.as_secs_f32();
let raw = if total <= 0.0 {
1.0
} else {
(since.elapsed().as_secs_f32() / total).clamp(0.0, 1.0)
};
motion::MENU_OUT.progress(raw)
}
pub(super) fn material_menu(content: AnyElement) -> AnyElement {
crate::surface::popover(Theme::surface_radius(), content).into_any_element()
}
pub(super) fn menu_motion(id: SharedString, exit: Option<f32>, inner: gpui::Div) -> AnyElement {
if let Some(t) = exit {
let inner = inner.relative().child(div().absolute().inset_0().occlude());
motion::menu_out(SharedString::from(format!("{id}-out")), t, inner).into_any_element()
} else {
motion::menu_in(id, inner).into_any_element()
}
}
pub(super) fn menu_layer(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
anchor: Anchor,
position: Option<Point<Pixels>>,
gap: f32,
) -> AnyElement {
gpui::deferred(Layer {
content: menu_motion(
id.into(),
closing.map(exit_progress),
div()
.occlude()
.flex()
.flex_col()
.min_h_0()
.child(material_menu(content)),
),
anchor,
position,
gap,
})
.priority(popover::MENU_LAYER)
.into_any_element()
}
struct Layer {
content: AnyElement,
anchor: Anchor,
position: Option<Point<Pixels>>,
gap: f32,
}
impl Element for Layer {
type RequestLayoutState = LayoutId;
type PrepaintState = ();
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, LayoutId) {
let child = self.content.request_layout(window, cx);
let layout = window.request_layout(
Style {
position: Position::Absolute,
display: Display::Flex,
..Style::default()
},
[],
cx,
);
(layout, child)
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
child: &mut LayoutId,
window: &mut Window,
cx: &mut App,
) {
let margin = px(popover::SNAP) + window.client_inset().unwrap_or(Pixels::ZERO);
let mut style = Style {
display: Display::Flex,
flex_direction: FlexDirection::Column,
..Style::default()
};
style.max_size.height = relative(1.).into();
let constrained = window.request_layout(style, [*child], cx);
window.compute_layout(
constrained,
size(
AvailableSpace::MaxContent,
AvailableSpace::Definite(
(window.viewport_size().height - margin * 2.).max(Pixels::ZERO),
),
),
cx,
);
let natural = window.layout_bounds(*child).size;
let placement = Placement::new(
self.anchor,
self.position.unwrap_or(bounds.origin),
natural.height,
window.viewport_size(),
margin,
px(self.gap),
);
window.compute_layout(
constrained,
size(
AvailableSpace::MaxContent,
AvailableSpace::Definite(placement.available_height),
),
cx,
);
let measured = window.layout_bounds(*child).size;
let origin = placement.origin(measured);
self.content
.prepaint_at(point(origin.x.round(), origin.y.round()), window, cx);
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
_: &mut LayoutId,
_: &mut (),
window: &mut Window,
cx: &mut App,
) {
self.content.paint(window, cx);
}
}
impl IntoElement for Layer {
type Element = Self;
fn into_element(self) -> Self {
self
}
}
pub fn anchored_menu(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
pinned_layer(menu_layer(id, content, closing, Anchor::TopLeft, None, 6.0))
}
pub fn anchored_menu_below(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
anchored_menu_below_gap(id, content, closing, 6.0)
}
pub fn anchored_menu_below_gap(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
gap: f32,
) -> AnyElement {
div()
.absolute()
.bottom_0()
.left_0()
.size_0()
.child(menu_layer(id, content, closing, Anchor::TopLeft, None, gap))
.into_any_element()
}
pub fn anchored_submenu(
id: impl Into<SharedString>,
content: AnyElement,
chain: &Chain,
closing: Option<web_time::Instant>,
) -> AnyElement {
let content = div().occlude().child(material_menu(content));
submenu::layer(
menu_motion(id.into(), closing.map(exit_progress), content),
chain,
)
}
pub fn anchored_menu_above(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
pinned_layer(menu_layer(
id,
content,
closing,
Anchor::BottomLeft,
None,
6.0,
))
}
pub fn anchored_menu_above_at(
id: impl Into<SharedString>,
position: Point<Pixels>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
div()
.absolute()
.left(position.x)
.top(position.y)
.size_0()
.child(anchored_menu_above(id, content, closing))
.into_any_element()
}
pub fn anchored_menu_above_end(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
div()
.absolute()
.top_0()
.right_0()
.size_0()
.child(menu_layer(
id,
content,
closing,
Anchor::BottomRight,
None,
6.0,
))
.into_any_element()
}
pub fn anchored_menu_below_end(
id: impl Into<SharedString>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
div()
.absolute()
.bottom_0()
.right_0()
.size_0()
.child(menu_layer(
id,
content,
closing,
Anchor::TopRight,
None,
6.0,
))
.into_any_element()
}
pub fn menu_at(
id: impl Into<SharedString>,
position: Point<Pixels>,
content: AnyElement,
closing: Option<web_time::Instant>,
) -> AnyElement {
menu_layer(id, content, closing, Anchor::TopLeft, Some(position), 0.0)
}