diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index f521948be0..31a8827109 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -22,7 +22,6 @@ use util::ResultExt; const DRAG_THRESHOLD: f64 = 2.; const TOOLTIP_DELAY: Duration = Duration::from_millis(500); -const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); pub struct GroupStyle { pub group: SharedString, @@ -419,9 +418,8 @@ pub trait StatefulInteractiveComponent>: InteractiveCo self.interactivity().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); - self.interactivity().tooltip_builder = Some(Rc::new(move |view_state, cx| { - build_tooltip(view_state, cx).into() - })); + self.interactivity().tooltip_builder = + Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx))); self } @@ -965,7 +963,7 @@ where waiting: None, tooltip: Some(AnyTooltip { view: tooltip_builder(view_state, cx), - cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, + cursor_offset: cx.mouse_position(), }), }); cx.notify(); diff --git a/crates/gpui2/src/elements/mod.rs b/crates/gpui2/src/elements/mod.rs index eb061f7d34..12c57958ea 100644 --- a/crates/gpui2/src/elements/mod.rs +++ b/crates/gpui2/src/elements/mod.rs @@ -1,11 +1,13 @@ mod div; mod img; +mod overlay; mod svg; mod text; mod uniform_list; pub use div::*; pub use img::*; +pub use overlay::*; pub use svg::*; pub use text::*; pub use uniform_list::*; diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs new file mode 100644 index 0000000000..a190337f04 --- /dev/null +++ b/crates/gpui2/src/elements/overlay.rs @@ -0,0 +1,203 @@ +use smallvec::SmallVec; + +use crate::{ + point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point, + Size, Style, +}; + +pub struct OverlayState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +pub struct Overlay { + children: SmallVec<[AnyElement; 2]>, + anchor_corner: AnchorCorner, + fit_mode: OverlayFitMode, + // todo!(); + // anchor_position: Option, + // position_mode: OverlayPositionMode, +} + +/// overlay gives you a floating element that will avoid overflowing the window bounds. +/// Its children should have no margin to avoid measurement issues. +pub fn overlay() -> Overlay { + Overlay { + children: SmallVec::new(), + anchor_corner: AnchorCorner::TopLeft, + fit_mode: OverlayFitMode::SwitchAnchor, + } +} + +impl Overlay { + /// Sets which corner of the overlay should be anchored to the current position. + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor_corner = anchor; + self + } + + /// Snap to window edge instead of switching anchor corner when an overflow would occur. + pub fn snap_to_window(mut self) -> Self { + self.fit_mode = OverlayFitMode::SnapToWindow; + self + } +} + +impl ParentComponent for Overlay { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl Element for Overlay { + type ElementState = OverlayState; + + fn element_id(&self) -> Option { + None + } + + fn layout( + &mut self, + view_state: &mut V, + _: Option, + cx: &mut crate::ViewContext, + ) -> (crate::LayoutId, Self::ElementState) { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied()); + + (layout_id, OverlayState { child_layout_ids }) + } + + fn paint( + &mut self, + bounds: crate::Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + if element_state.child_layout_ids.is_empty() { + return; + } + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + for child_layout_id in &element_state.child_layout_ids { + let child_bounds = cx.layout_bounds(*child_layout_id); + child_min = child_min.min(&child_bounds.origin); + child_max = child_max.max(&child_bounds.lower_right()); + } + let size: Size = (child_max - child_min).into(); + let origin = bounds.origin; + + let mut desired = self.anchor_corner.get_bounds(origin, size); + let limits = Bounds { + origin: Point::zero(), + size: cx.viewport_size(), + }; + + match self.fit_mode { + OverlayFitMode::SnapToWindow => { + // Snap the horizontal edges of the overlay to the horizontal edges of the window if + // its horizontal bounds overflow + if desired.right() > limits.right() { + desired.origin.x -= desired.right() - limits.right(); + } else if desired.left() < limits.left() { + desired.origin.x = limits.origin.x; + } + + // Snap the vertical edges of the overlay to the vertical edges of the window if + // its vertical bounds overflow. + if desired.bottom() > limits.bottom() { + desired.origin.y -= desired.bottom() - limits.bottom(); + } else if desired.top() < limits.top() { + desired.origin.y = limits.origin.y; + } + } + OverlayFitMode::SwitchAnchor => { + let mut anchor_corner = self.anchor_corner; + + if desired.left() < limits.left() || desired.right() > limits.right() { + anchor_corner = anchor_corner.switch_axis(Axis::Horizontal); + } + + if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() { + anchor_corner = anchor_corner.switch_axis(Axis::Vertical); + } + + // Update bounds if needed + if anchor_corner != self.anchor_corner { + desired = anchor_corner.get_bounds(origin, size) + } + } + OverlayFitMode::None => {} + } + + cx.with_element_offset(desired.origin - bounds.origin, |cx| { + for child in &mut self.children { + child.paint(view_state, cx); + } + }) + } +} + +enum Axis { + Horizontal, + Vertical, +} + +#[derive(Copy, Clone)] +pub enum OverlayFitMode { + SnapToWindow, + SwitchAnchor, + None, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum AnchorCorner { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +impl AnchorCorner { + fn get_bounds(&self, origin: Point, size: Size) -> Bounds { + let origin = match self { + Self::TopLeft => origin, + Self::TopRight => Point { + x: origin.x - size.width, + y: origin.y, + }, + Self::BottomLeft => Point { + x: origin.x, + y: origin.y - size.height, + }, + Self::BottomRight => Point { + x: origin.x - size.width, + y: origin.y - size.height, + }, + }; + + Bounds { origin, size } + } + + fn switch_axis(self, axis: Axis) -> Self { + match axis { + Axis::Vertical => match self { + AnchorCorner::TopLeft => AnchorCorner::BottomLeft, + AnchorCorner::TopRight => AnchorCorner::BottomRight, + AnchorCorner::BottomLeft => AnchorCorner::TopLeft, + AnchorCorner::BottomRight => AnchorCorner::TopRight, + }, + Axis::Horizontal => match self { + AnchorCorner::TopLeft => AnchorCorner::TopRight, + AnchorCorner::TopRight => AnchorCorner::TopLeft, + AnchorCorner::BottomLeft => AnchorCorner::BottomRight, + AnchorCorner::BottomRight => AnchorCorner::BottomLeft, + }, + } + } +} diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index e07300951e..854453101e 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -421,6 +421,22 @@ impl Bounds where T: Add + Clone + Default + Debug, { + pub fn top(&self) -> T { + self.origin.y.clone() + } + + pub fn bottom(&self) -> T { + self.origin.y.clone() + self.size.height.clone() + } + + pub fn left(&self) -> T { + self.origin.x.clone() + } + + pub fn right(&self) -> T { + self.origin.x.clone() + self.size.width.clone() + } + pub fn upper_right(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone(), diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index f9530269d5..f1bb7b4e7c 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,5 +1,6 @@ use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; use theme2::ActiveTheme; +use ui::Tooltip; pub struct ScrollStory; @@ -35,16 +36,18 @@ impl Render for ScrollStory { } else { color_2 }; - div().id(id).bg(bg).size(px(100. as f32)).when( - row >= 5 && column >= 5, - |d| { + div() + .id(id) + .tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx)) + .bg(bg) + .size(px(100. as f32)) + .when(row >= 5 && column >= 5, |d| { d.overflow_scroll() .child(div().size(px(50.)).bg(color_1)) .child(div().size(px(50.)).bg(color_2)) .child(div().size(px(50.)).bg(color_1)) .child(div().size(px(50.)).bg(color_2)) - }, - ) + }) })) })) } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index c4366234c5..a8dae6c97f 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,4 +1,4 @@ -use gpui::{Action, AnyView, Div, Render, VisualContext}; +use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext}; use settings2::Settings; use theme2::{ActiveTheme, ThemeSettings}; @@ -68,30 +68,35 @@ impl Tooltip { } impl Render for Tooltip { - type Element = Div; + type Element = Overlay; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - v_stack() - .elevation_2(cx) - .font(ui_font) - .text_ui_sm() - .text_color(cx.theme().colors().text) - .py_1() - .px_2() - .child( - h_stack() - .child(self.title.clone()) - .when_some(self.key_binding.clone(), |this, key_binding| { - this.justify_between().child(key_binding) + overlay().child( + // padding to avoid mouse cursor + div().pl_2().pt_2p5().child( + v_stack() + .elevation_2(cx) + .font(ui_font) + .text_ui_sm() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() + .child( + h_stack() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) + }), + ) + .when_some(self.meta.clone(), |this, meta| { + this.child( + Label::new(meta) + .size(LabelSize::Small) + .color(TextColor::Muted), + ) }), - ) - .when_some(self.meta.clone(), |this, meta| { - this.child( - Label::new(meta) - .size(LabelSize::Small) - .color(TextColor::Muted), - ) - }) + ), + ) } }