diff --git a/crates/story/src/popup_story.rs b/crates/story/src/popup_story.rs index c79a4fd1..661a3cb8 100644 --- a/crates/story/src/popup_story.rs +++ b/crates/story/src/popup_story.rs @@ -1,8 +1,8 @@ use gpui::{ - actions, div, impl_actions, px, AnchorCorner, AppContext, DismissEvent, Element, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, MouseButton, - ParentElement as _, Render, SharedString, Styled as _, View, ViewContext, VisualContext, - WindowContext, + actions, div, impl_actions, px, AnchorCorner, AppContext, Axis, DismissEvent, Element, + EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, KeyBinding, + MouseButton, ParentElement as _, Render, SharedString, Styled as _, View, ViewContext, + VisualContext, WindowContext, }; use serde::Deserialize; use ui::{ @@ -208,16 +208,70 @@ impl Render for PopupStory { .items_center() .justify_between() .child( - v_flex().gap_4().child( - Popover::new("info-top-left") - .trigger(Button::new("info-top-left").label("Top Left")) - .content(|cx| { - cx.new_view(|cx| { - PopoverContent::new(cx, |_| { - v_flex() + v_flex() + .gap_4() + .child( + Popover::new("info-top-left") + .trigger(Button::new("info-top-left").label("Top Left")) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |_| { + v_flex() + .gap_4() + .child("Hello, this is a Popover.") + .w(px(400.)) + .child(Divider::horizontal()) + .child( + Button::new("info1") + .label("Yes") + .w(px(80.)) + .small(), + ) + .into_any() + }) + .max_w(px(600.)) + }) + }), + ) + .child( + Popover::new("info-left-top") + .trigger(Button::new("info-left-top").label("Left Top")) + .axis(Axis::Horizontal) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |_| { + v_flex() + .gap_4() + .child("Hello, this is a Popover.") + .w(px(400.)) + .child(Divider::horizontal()) + .child( + Button::new("info1") + .label("Yes") + .w(px(80.)) + .small(), + ) + .into_any() + }) + .max_w(px(600.)) + }) + }), + ), + ) + .child( + v_flex() + .gap_4() + .child( + Popover::new("info-top-right") + .anchor(AnchorCorner::TopRight) + .trigger(Button::new("info-top-right").label("Top Right")) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |_| { + v_flex() .gap_4() - .child("Hello, this is a Popover.") - .w(px(400.)) + .w_96() + .child("Hello, this is a Popover on the Top Right.") .child(Divider::horizontal()) .child( Button::new("info1") @@ -226,34 +280,34 @@ impl Render for PopupStory { .small(), ) .into_any() + }) }) - .max_w(px(600.)) - }) - }), - ), - ) - .child( - Popover::new("info-top-right") - .anchor(AnchorCorner::TopRight) - .trigger(Button::new("info-top-right").label("Top Right")) - .content(|cx| { - cx.new_view(|cx| { - PopoverContent::new(cx, |_| { - v_flex() - .gap_4() - .w_96() - .child("Hello, this is a Popover on the Top Right.") - .child(Divider::horizontal()) - .child( - Button::new("info1") - .label("Yes") - .w(px(80.)) - .small(), - ) - .into_any() - }) - }) - }), + }), + ) + .child( + Popover::new("info-right-top") + .anchor(AnchorCorner::TopRight) + .axis(Axis::Horizontal) + .trigger(Button::new("info-right-top").label("Right Top")) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |_| { + v_flex() + .gap_4() + .w_96() + .child("Hello, this is a Popover on the Right Top.") + .child(Divider::horizontal()) + .child( + Button::new("info1") + .label("Yes") + .w(px(80.)) + .small(), + ) + .into_any() + }) + }) + }), + ), ), ) .child( @@ -322,57 +376,97 @@ impl Render for PopupStory { .items_center() .justify_between() .child( - Popover::new("info-bottom-left") - .anchor(AnchorCorner::BottomLeft) - .trigger(Button::new("pop").label("Popup with Form").w(px(300.))) - .content(move |_| form.clone()), + v_flex() + .gap_4() + .child( + Popover::new("info-left-bottom") + .anchor(AnchorCorner::BottomLeft) + .axis(Axis::Horizontal) + .trigger( + Button::new("pop").label("Left Bottom"), + ) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |_| { + div().child("Hello this is a Popover on the Left Bottom.").into_any() + }) + }) + }), + ) + .child( + Popover::new("info-bottom-left") + .anchor(AnchorCorner::BottomLeft) + .trigger( + Button::new("pop").label("Popup with Form").w(px(300.)), + ) + .content(move |_| form.clone()), + ), ) .child( - Popover::new("info-bottom-right") - .anchor(AnchorCorner::BottomRight) - .mouse_button(MouseButton::Right) - .trigger(Button::new("pop").label("Mouse Right Click").w(px(300.))) - .content(|cx| { - cx.new_view(|cx| { - PopoverContent::new(cx, |cx| { - v_flex() - .gap_2() - .child( - "Hello, this is a Popover on the Bottom Right.", - ) - .child(Divider::horizontal()) - .child( - h_flex() + v_flex() + .gap_4() + .child( + Popover::new("info-right-bottom") + .anchor(AnchorCorner::BottomLeft) + .axis(Axis::Horizontal) + .trigger( + Button::new("info-right-bottom").label("Right Bottom"), + ) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |_| { + div().child("Hello this is a Popover on the Right Bottom.").into_any() + }) + }) + }) + ).child( + Popover::new("info-bottom-right") + .anchor(AnchorCorner::BottomRight) + .mouse_button(MouseButton::Right) + .trigger(Button::new("pop").label("Mouse Right Click").w(px(300.))) + .content(|cx| { + cx.new_view(|cx| { + PopoverContent::new(cx, |cx| { + v_flex() .gap_2() .child( - Button::new("info1") - .label("Ok") - .w(px(80.)) - .small() - .on_click(cx.listener( - |_, _, cx| { - cx.push_notification( - "You have clicked Ok.", - ); - cx.emit(DismissEvent); - }, - )), + "Hello, this is a Popover on the Bottom Right.", ) + .child(Divider::horizontal()) .child( - Button::new("close") - .label("Cancel") - .small() - .on_click(cx.listener( - |_, _, cx| { - cx.emit(DismissEvent); - }, - )), - ), - ) - .into_any() + h_flex() + .gap_2() + .child( + Button::new("info1") + .label("Ok") + .w(px(80.)) + .small() + .on_click(cx.listener( + |_, _, cx| { + cx.push_notification( + "You have clicked Ok.", + ); + cx.emit(DismissEvent); + }, + )), + ) + .child( + Button::new("close") + .label("Cancel") + .small() + .on_click(cx.listener( + |_, _, cx| { + cx.emit(DismissEvent); + }, + )), + ), + ) + .into_any() + }) + }) }) - }) - }), + ), + ), ), ) diff --git a/crates/ui/src/popover.rs b/crates/ui/src/popover.rs index 33fd3d62..c66a9653 100644 --- a/crates/ui/src/popover.rs +++ b/crates/ui/src/popover.rs @@ -1,15 +1,17 @@ use gpui::{ - actions, anchored, deferred, div, prelude::FluentBuilder as _, px, AnchorCorner, AnyElement, - AppContext, Bounds, DismissEvent, DispatchPhase, Element, ElementId, EventEmitter, FocusHandle, - FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, - LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, - Style, StyleRefinement, Styled, View, ViewContext, VisualContext, WindowContext, + actions, anchored, deferred, div, prelude::FluentBuilder as _, px, Along, AnchorCorner, + AnyElement, AppContext, Axis, Bounds, DismissEvent, DispatchPhase, Element, ElementId, + EventEmitter, FocusHandle, FocusableView, GlobalElementId, Hitbox, InteractiveElement as _, + IntoElement, KeyBinding, LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, + Pixels, Point, Render, Style, StyleRefinement, Styled, View, ViewContext, VisualContext, + WindowContext, }; use std::{cell::RefCell, rc::Rc}; -use crate::{Selectable, StyledExt as _}; +use crate::{AxisExt, Selectable, StyledExt as _}; const CONTEXT: &str = "Popover"; +const SNAP_MARGIN: Pixels = px(8.); actions!(popover, [Escape]); @@ -65,6 +67,7 @@ impl Render for PopoverContent { pub struct Popover { id: ElementId, anchor: AnchorCorner, + axis: Axis, trigger: Option AnyElement + 'static>>, content: Option View + 'static>>, /// Style for trigger element. @@ -83,6 +86,7 @@ where Self { id: id.into(), anchor: AnchorCorner::TopLeft, + axis: Axis::Vertical, trigger: None, trigger_style: None, content: None, @@ -91,11 +95,27 @@ where } } + /// Set the anchor corner of the popover, default is `AnchorCorner::TopLeft`. pub fn anchor(mut self, anchor: AnchorCorner) -> Self { self.anchor = anchor; self } + /// Set the axis of the popover, default is `Axis::Vertical`. + /// + /// The axis is used for the popover to determine the position of the popover. + /// + /// For exampleL + /// + /// - If the axis is `Axis::Vertical` and the anchor is `AnchorCorner::TopLeft`, + /// the popover will be positioned below the trigger element. + /// - If the axis is `Axis::Horizontal` and the anchor is `AnchorCorner::TopLeft`, + /// the popover will be positioned to the right of the trigger element. + pub fn axis(mut self, axis: Axis) -> Self { + self.axis = axis; + self + } + /// Set the mouse button to trigger the popover, default is `MouseButton::Left`. pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self { self.mouse_button = mouse_button; @@ -147,14 +167,43 @@ where (trigger)(is_open, cx) } - fn resolved_corner(&self, bounds: Bounds) -> Point { - match self.anchor { + fn resolved_corner( + &self, + trigger_bounds: Bounds, + popover_bounds: Bounds, + ) -> Point { + let mut p = match self.anchor { AnchorCorner::TopLeft => AnchorCorner::BottomLeft, AnchorCorner::TopRight => AnchorCorner::BottomRight, AnchorCorner::BottomLeft => AnchorCorner::TopLeft, AnchorCorner::BottomRight => AnchorCorner::TopRight, } - .corner(bounds) + .corner(trigger_bounds); + + if self.axis.is_horizontal() { + match self.anchor { + AnchorCorner::TopLeft => { + p.x = p.x - trigger_bounds.size.width - popover_bounds.size.width + SNAP_MARGIN; + p.y = p.y - trigger_bounds.size.height - SNAP_MARGIN; + } + AnchorCorner::TopRight => { + p.x = p.x + trigger_bounds.size.width - SNAP_MARGIN; + p.y = p.y - trigger_bounds.size.height - SNAP_MARGIN; + } + AnchorCorner::BottomLeft => { + p.x = p.x - trigger_bounds.size.width - popover_bounds.size.width + SNAP_MARGIN; + p.y = + p.y + trigger_bounds.size.height - popover_bounds.size.height + SNAP_MARGIN; + } + AnchorCorner::BottomRight => { + p.x = p.x + trigger_bounds.size.width - SNAP_MARGIN; + p.y = + p.y + trigger_bounds.size.height - popover_bounds.size.height + SNAP_MARGIN; + } + } + } + + p } fn with_element_state( @@ -193,6 +242,8 @@ pub struct PopoverElementState { content_view: Rc>>>, /// Trigger bounds for positioning the popover. trigger_bounds: Option>, + /// Popover bounds for open window size. + popover_bounds: Option>, } impl Default for PopoverElementState { @@ -204,6 +255,7 @@ impl Default for PopoverElementState { trigger_element: None, content_view: Rc::new(RefCell::new(None)), trigger_bounds: None, + popover_bounds: None, } } } @@ -212,6 +264,8 @@ pub struct PrepaintState { hitbox: Hitbox, /// Trigger bounds for limit a rect to handle mouse click. trigger_bounds: Option>, + /// Popover bounds for open window size. + popover_bounds: Option>, } impl Element for Popover { @@ -251,10 +305,13 @@ impl Element for Popover { is_open = true; let mut anchored = anchored() - .snap_to_window_with_margin(px(8.)) + .snap_to_window_with_margin(SNAP_MARGIN) .anchor(view.anchor); + if let Some(trigger_bounds) = element_state.trigger_bounds { - anchored = anchored.position(view.resolved_corner(trigger_bounds)); + let popover_bounds = element_state.popover_bounds.unwrap_or_default(); + anchored = + anchored.position(view.resolved_corner(trigger_bounds, popover_bounds)); } let mut element = { @@ -317,33 +374,39 @@ impl Element for Popover { fn prepaint( &mut self, - _id: Option<&gpui::GlobalElementId>, + id: Option<&gpui::GlobalElementId>, _bounds: gpui::Bounds, request_layout: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) -> Self::PrepaintState { - if let Some(element) = &mut request_layout.trigger_element { - element.prepaint(cx); - } - if let Some(element) = &mut request_layout.popover_element { - element.prepaint(cx); - } + self.with_element_state(id.unwrap(), cx, |_, element_state, cx| { + if let Some(element) = &mut request_layout.trigger_element { + element.prepaint(cx); + } + if let Some(element) = &mut request_layout.popover_element { + element.prepaint(cx); + } - let trigger_bounds = request_layout - .trigger_layout_id - .map(|id| cx.layout_bounds(id)); + let trigger_bounds = request_layout + .trigger_layout_id + .map(|id| cx.layout_bounds(id)); - // Prepare the popover, for get the bounds of it for open window size. - let _ = request_layout - .popover_layout_id - .map(|id| cx.layout_bounds(id)); + // Prepare the popover, for get the bounds of it for open window size. + let popover_bounds = request_layout + .popover_layout_id + .map(|id| cx.layout_bounds(id)); - let hitbox = cx.insert_hitbox(trigger_bounds.unwrap_or_default(), false); + let hitbox = cx.insert_hitbox(trigger_bounds.unwrap_or_default(), false); - PrepaintState { - trigger_bounds, - hitbox, - } + element_state.popover_bounds = popover_bounds; + element_state.trigger_bounds = trigger_bounds; + + PrepaintState { + trigger_bounds, + popover_bounds, + hitbox, + } + }) } fn paint( @@ -356,6 +419,7 @@ impl Element for Popover { ) { self.with_element_state(id.unwrap(), cx, |this, element_state, cx| { element_state.trigger_bounds = prepaint.trigger_bounds; + element_state.popover_bounds = prepaint.popover_bounds; if let Some(mut element) = request_layout.trigger_element.take() { element.paint(cx); diff --git a/crates/ui/src/styled.rs b/crates/ui/src/styled.rs index b70955a9..c0ca4241 100644 --- a/crates/ui/src/styled.rs +++ b/crates/ui/src/styled.rs @@ -355,10 +355,12 @@ pub trait AxisExt { } impl AxisExt for Axis { + #[inline] fn is_horizontal(self) -> bool { self == Axis::Horizontal } + #[inline] fn is_vertical(self) -> bool { self == Axis::Vertical } @@ -384,6 +386,7 @@ impl Display for Placement { } impl Placement { + #[inline] pub fn is_horizontal(&self) -> bool { match self { Placement::Left | Placement::Right => true, @@ -391,6 +394,7 @@ impl Placement { } } + #[inline] pub fn is_vertical(&self) -> bool { match self { Placement::Top | Placement::Bottom => true, @@ -398,6 +402,7 @@ impl Placement { } } + #[inline] pub fn axis(&self) -> Axis { match self { Placement::Top | Placement::Bottom => Axis::Vertical, @@ -414,6 +419,7 @@ pub enum Side { } impl Side { + #[inline] pub(crate) fn is_left(&self) -> bool { matches!(self, Self::Left) } diff --git a/crates/ui/src/switch.rs b/crates/ui/src/switch.rs index 3f5a46a4..aa281bff 100644 --- a/crates/ui/src/switch.rs +++ b/crates/ui/src/switch.rs @@ -1,4 +1,4 @@ -use crate::{h_flex, theme::ActiveTheme, Disableable, Side, Sizable, Size}; +use crate::{h_flex, theme::ActiveTheme, Disableable, Side, Sizable, Size, StyledExt}; use gpui::{ div, prelude::FluentBuilder as _, px, Animation, AnimationExt as _, AnyElement, Element, ElementId, GlobalElementId, InteractiveElement, IntoElement, LayoutId, ParentElement as _,