merged previous stuffs on master

This commit is contained in:
2026-02-20 19:48:03 +07:00
parent 014757cfc9
commit b88955e62c
176 changed files with 11152 additions and 11212 deletions

333
crates/ui/src/anchored.rs Normal file
View File

@@ -0,0 +1,333 @@
//! This is a fork of gpui's anchored element that adds support for offsetting
//! https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/elements/anchored.rs
use gpui::{
point, px, AnyElement, App, Axis, Bounds, Display, Edges, Element, GlobalElementId, Half,
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Point, Position, Size, Style,
Window,
};
use smallvec::SmallVec;
use crate::Anchor;
/// The state that the anchored element element uses to track its children.
pub struct AnchoredState {
child_layout_ids: SmallVec<[LayoutId; 4]>,
}
/// An anchored element that can be used to display UI that
/// will avoid overflowing the window bounds.
pub(crate) struct Anchored {
children: SmallVec<[AnyElement; 2]>,
anchor_corner: Anchor,
fit_mode: AnchoredFitMode,
anchor_position: Option<Point<Pixels>>,
position_mode: AnchoredPositionMode,
offset: Option<Point<Pixels>>,
}
/// anchored gives you an element that will avoid overflowing the window bounds.
/// Its children should have no margin to avoid measurement issues.
pub(crate) fn anchored() -> Anchored {
Anchored {
children: SmallVec::new(),
anchor_corner: Anchor::TopLeft,
fit_mode: AnchoredFitMode::SwitchAnchor,
anchor_position: None,
position_mode: AnchoredPositionMode::Window,
offset: None,
}
}
#[allow(dead_code)]
impl Anchored {
/// Sets which corner of the anchored element should be anchored to the current position.
pub fn anchor(mut self, anchor: Anchor) -> Self {
self.anchor_corner = anchor;
self
}
/// Sets the position in window coordinates
/// (otherwise the location the anchored element is rendered is used)
pub fn position(mut self, anchor: Point<Pixels>) -> Self {
self.anchor_position = Some(anchor);
self
}
/// Offset the final position by this amount.
/// Useful when you want to anchor to an element but offset from it, such as in PopoverMenu.
pub fn offset(mut self, offset: Point<Pixels>) -> Self {
self.offset = Some(offset);
self
}
/// Sets the position mode for this anchored element. Local will have this
/// interpret its [`Anchored::position`] as relative to the parent element.
/// While Window will have it interpret the position as relative to the window.
pub fn position_mode(mut self, mode: AnchoredPositionMode) -> Self {
self.position_mode = mode;
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 = AnchoredFitMode::SnapToWindow;
self
}
/// Snap to window edge and leave some margins.
pub fn snap_to_window_with_margin(mut self, edges: impl Into<Edges<Pixels>>) -> Self {
self.fit_mode = AnchoredFitMode::SnapToWindowWithMargin(edges.into());
self
}
}
impl ParentElement for Anchored {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements)
}
}
impl Element for Anchored {
type PrepaintState = ();
type RequestLayoutState = AnchoredState;
fn id(&self) -> Option<gpui::ElementId> {
None
}
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let child_layout_ids = self
.children
.iter_mut()
.map(|child| child.request_layout(window, cx))
.collect::<SmallVec<_>>();
let anchored_style = Style {
position: Position::Absolute,
display: Display::Flex,
..Style::default()
};
let layout_id = window.request_layout(anchored_style, child_layout_ids.iter().copied(), cx);
(layout_id, AnchoredState { child_layout_ids })
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
if request_layout.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 &request_layout.child_layout_ids {
let child_bounds = window.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.bottom_right());
}
let size: Size<Pixels> = (child_max - child_min).into();
let (origin, mut desired) = self.position_mode.get_position_and_bounds(
self.anchor_position,
self.anchor_corner,
size,
bounds,
self.offset,
);
let limits = Bounds {
origin: Point::default(),
size: window.viewport_size(),
};
if self.fit_mode == AnchoredFitMode::SwitchAnchor {
let mut anchor_corner = self.anchor_corner;
if desired.left() < limits.left() || desired.right() > limits.right() {
let switched = Bounds::from_corner_and_size(
anchor_corner
.other_side_corner_along(Axis::Horizontal)
.into(),
origin,
size,
);
if !(switched.left() < limits.left() || switched.right() > limits.right()) {
anchor_corner = anchor_corner.other_side_corner_along(Axis::Horizontal);
desired = switched
}
}
if desired.top() < limits.top() || desired.bottom() > limits.bottom() {
let switched = Bounds::from_corner_and_size(
anchor_corner.other_side_corner_along(Axis::Vertical).into(),
origin,
size,
);
if !(switched.top() < limits.top() || switched.bottom() > limits.bottom()) {
desired = switched;
}
}
}
let client_inset = window.client_inset().unwrap_or(px(0.));
let edges = match self.fit_mode {
AnchoredFitMode::SnapToWindowWithMargin(edges) => edges,
_ => Edges::default(),
}
.map(|edge| *edge + client_inset);
// Snap the horizontal edges of the anchored element to the horizontal edges of the window if
// its horizontal bounds overflow, aligning to the left if it is wider than the limits.
if desired.right() > limits.right() {
desired.origin.x -= desired.right() - limits.right() + edges.right;
}
if desired.left() < limits.left() {
desired.origin.x = limits.origin.x + edges.left;
}
// Snap the vertical edges of the anchored element to the vertical edges of the window if
// its vertical bounds overflow, aligning to the top if it is taller than the limits.
if desired.bottom() > limits.bottom() {
desired.origin.y -= desired.bottom() - limits.bottom() + edges.bottom;
}
if desired.top() < limits.top() {
desired.origin.y = limits.origin.y + edges.top;
}
let offset = desired.origin - bounds.origin;
let offset = point(offset.x.round(), offset.y.round());
window.with_element_offset(offset, |window| {
for child in &mut self.children {
child.prepaint(window, cx);
}
})
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
_prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
for child in &mut self.children {
child.paint(window, cx);
}
}
}
impl IntoElement for Anchored {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
/// Which algorithm to use when fitting the anchored element to be inside the window.
#[allow(dead_code)]
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredFitMode {
/// Snap the anchored element to the window edge.
SnapToWindow,
/// Snap to window edge and leave some margins.
SnapToWindowWithMargin(Edges<Pixels>),
/// Switch which corner anchor this anchored element is attached to.
SwitchAnchor,
}
/// Which algorithm to use when positioning the anchored element.
#[allow(dead_code)]
#[derive(Copy, Clone, PartialEq)]
pub enum AnchoredPositionMode {
/// Position the anchored element relative to the window.
Window,
/// Position the anchored element relative to its parent.
Local,
}
impl AnchoredPositionMode {
fn get_position_and_bounds(
&self,
anchor_position: Option<Point<Pixels>>,
anchor_corner: Anchor,
size: Size<Pixels>,
bounds: Bounds<Pixels>,
offset: Option<Point<Pixels>>,
) -> (Point<Pixels>, Bounds<Pixels>) {
let offset = offset.unwrap_or_default();
match self {
AnchoredPositionMode::Window => {
let anchor_position = anchor_position.unwrap_or(bounds.origin);
let bounds =
Self::from_corner_and_size(anchor_corner, anchor_position + offset, size);
(anchor_position, bounds)
}
AnchoredPositionMode::Local => {
let anchor_position = anchor_position.unwrap_or_default();
let bounds = Self::from_corner_and_size(
anchor_corner,
bounds.origin + anchor_position + offset,
size,
);
(anchor_position, bounds)
}
}
}
// Ref https://github.com/zed-industries/zed/blob/b06f4088a3565c5e30663106ff79c1ced645d87a/crates/gpui/src/geometry.rs#L863
fn from_corner_and_size(
anchor: Anchor,
origin: Point<Pixels>,
size: Size<Pixels>,
) -> Bounds<Pixels> {
let origin = match anchor {
Anchor::TopLeft => origin,
Anchor::TopCenter => Point {
x: origin.x - size.width.half(),
y: origin.y,
},
Anchor::TopRight => Point {
x: origin.x - size.width,
y: origin.y,
},
Anchor::BottomLeft => Point {
x: origin.x,
y: origin.y - size.height,
},
Anchor::BottomCenter => Point {
x: origin.x - size.width.half(),
y: origin.y - size.height,
},
Anchor::BottomRight => Point {
x: origin.x - size.width,
y: origin.y - size.height,
},
};
Bounds { origin, size }
}
}

View File

@@ -10,7 +10,7 @@ use theme::ActiveTheme;
use crate::indicator::Indicator;
use crate::tooltip::Tooltip;
use crate::{h_flex, Disableable, Icon, Selectable, Sizable, Size, StyledExt};
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable, Size, StyledExt};
#[derive(Clone, Copy, PartialEq, Eq)]
pub struct ButtonCustomVariant {
@@ -20,50 +20,6 @@ pub struct ButtonCustomVariant {
active: Hsla,
}
pub trait ButtonVariants: Sized {
fn with_variant(self, variant: ButtonVariant) -> Self;
/// With the primary style for the Button.
fn primary(self) -> Self {
self.with_variant(ButtonVariant::Primary)
}
/// With the secondary style for the Button.
fn secondary(self) -> Self {
self.with_variant(ButtonVariant::Secondary)
}
/// With the danger style for the Button.
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
/// With the warning style for the Button.
fn warning(self) -> Self {
self.with_variant(ButtonVariant::Warning)
}
/// With the ghost style for the Button.
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: false })
}
/// With the ghost style for the Button.
fn ghost_alt(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: true })
}
/// With the transparent style for the Button.
fn transparent(self) -> Self {
self.with_variant(ButtonVariant::Transparent)
}
/// With the custom style for the Button.
fn custom(self, style: ButtonCustomVariant) -> Self {
self.with_variant(ButtonVariant::Custom(style))
}
}
impl ButtonCustomVariant {
pub fn new(_window: &Window, cx: &App) -> Self {
Self {
@@ -110,6 +66,50 @@ pub enum ButtonVariant {
Custom(ButtonCustomVariant),
}
pub trait ButtonVariants: Sized {
fn with_variant(self, variant: ButtonVariant) -> Self;
/// With the primary style for the Button.
fn primary(self) -> Self {
self.with_variant(ButtonVariant::Primary)
}
/// With the secondary style for the Button.
fn secondary(self) -> Self {
self.with_variant(ButtonVariant::Secondary)
}
/// With the danger style for the Button.
fn danger(self) -> Self {
self.with_variant(ButtonVariant::Danger)
}
/// With the warning style for the Button.
fn warning(self) -> Self {
self.with_variant(ButtonVariant::Warning)
}
/// With the ghost style for the Button.
fn ghost(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: false })
}
/// With the ghost style for the Button.
fn ghost_alt(self) -> Self {
self.with_variant(ButtonVariant::Ghost { alt: true })
}
/// With the transparent style for the Button.
fn transparent(self) -> Self {
self.with_variant(ButtonVariant::Transparent)
}
/// With the custom style for the Button.
fn custom(self, style: ButtonCustomVariant) -> Self {
self.with_variant(ButtonVariant::Custom(style))
}
}
/// A Button element.
#[derive(IntoElement)]
#[allow(clippy::type_complexity)]
@@ -124,16 +124,15 @@ pub struct Button {
children: Vec<AnyElement>,
variant: ButtonVariant,
rounded: bool,
size: Size,
disabled: bool,
reverse: bool,
bold: bool,
cta: bool,
loading: bool,
loading_icon: Option<Icon>,
rounded: bool,
compact: bool,
underline: bool,
caret: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
@@ -160,20 +159,19 @@ impl Button {
style: StyleRefinement::default(),
icon: None,
label: None,
variant: ButtonVariant::default(),
disabled: false,
selected: false,
variant: ButtonVariant::default(),
underline: false,
compact: false,
caret: false,
rounded: false,
size: Size::Medium,
tooltip: None,
on_click: None,
on_hover: None,
loading: false,
reverse: false,
bold: false,
cta: false,
children: Vec::new(),
loading_icon: None,
tab_index: 0,
tab_stop: true,
}
@@ -209,27 +207,21 @@ impl Button {
self
}
/// Set reverse the position between icon and label.
pub fn reverse(mut self) -> Self {
self.reverse = true;
/// Set true to make the button compact (no padding).
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
/// Set bold the button (label will be use the semi-bold font).
pub fn bold(mut self) -> Self {
self.bold = true;
/// Set true to show the caret indicator.
pub fn caret(mut self) -> Self {
self.caret = true;
self
}
/// Set the cta style of the button.
pub fn cta(mut self) -> Self {
self.cta = true;
self
}
/// Set the loading icon of the button.
pub fn loading_icon(mut self, icon: impl Into<Icon>) -> Self {
self.loading_icon = Some(icon.into());
/// Set true to show the underline indicator.
pub fn underline(mut self) -> Self {
self.underline = true;
self
}
@@ -338,7 +330,7 @@ impl RenderOnce for Button {
};
let focus_handle = window
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
.use_keyed_state(self.id.clone(), cx, |_window, cx| cx.focus_handle())
.read(cx)
.clone();
@@ -350,6 +342,7 @@ impl RenderOnce for Button {
.tab_stop(self.tab_stop),
)
})
.relative()
.flex_shrink_0()
.flex()
.items_center()
@@ -361,39 +354,15 @@ impl RenderOnce for Button {
false => this.rounded(cx.theme().radius),
true => this.rounded_full(),
})
.map(|this| {
.when(!self.compact, |this| {
if self.label.is_none() && self.children.is_empty() {
// Icon Button
match self.size {
Size::Size(px) => this.size(px),
Size::XSmall => {
if self.cta {
this.w_10().h_5()
} else {
this.size_5()
}
}
Size::Small => {
if self.cta {
this.w_12().h_6()
} else {
this.size_6()
}
}
Size::Medium => {
if self.cta {
this.w_12().h_7()
} else {
this.size_7()
}
}
_ => {
if self.cta {
this.w_16().h_9()
} else {
this.size_9()
}
}
Size::XSmall => this.size_5(),
Size::Small => this.size_6(),
Size::Medium => this.size_7(),
_ => this.size_9(),
}
} else {
// Normal Button
@@ -402,8 +371,6 @@ impl RenderOnce for Button {
Size::XSmall => {
if self.icon.is_some() {
this.h_6().pl_2().pr_2p5()
} else if self.cta {
this.h_6().px_4()
} else {
this.h_6().px_2()
}
@@ -411,8 +378,6 @@ impl RenderOnce for Button {
Size::Small => {
if self.icon.is_some() {
this.h_7().pl_2().pr_2p5()
} else if self.cta {
this.h_7().px_4()
} else {
this.h_7().px_2()
}
@@ -434,13 +399,27 @@ impl RenderOnce for Button {
}
}
})
.on_mouse_down(gpui::MouseButton::Left, |_, window, _| {
.refine_style(&self.style)
.on_mouse_down(gpui::MouseButton::Left, move |_, window, cx| {
// Stop handle any click event when disabled.
// To avoid handle dropdown menu open when button is disabled.
if self.disabled {
cx.stop_propagation();
return;
}
// Avoid focus on mouse down.
window.prevent_default();
})
.when_some(self.on_click.filter(|_| clickable), |this, on_click| {
.when_some(self.on_click, |this, on_click| {
this.on_click(move |event, window, cx| {
(on_click)(event, window, cx);
// Stop handle any click event when disabled.
// To avoid handle dropdown menu open when button is disabled.
if !clickable {
cx.stop_propagation();
return;
}
on_click(event, window, cx);
})
})
.when_some(self.on_hover.filter(|_| hoverable), |this, on_hover| {
@@ -451,7 +430,6 @@ impl RenderOnce for Button {
.child({
h_flex()
.id("label")
.when(self.reverse, |this| this.flex_row_reverse())
.justify_center()
.map(|this| match self.size {
Size::XSmall => this.text_xs().gap_1(),
@@ -463,22 +441,18 @@ impl RenderOnce for Button {
this.child(icon.with_size(icon_size))
})
})
.when(self.loading, |this| {
this.child(
Indicator::new()
.when_some(self.loading_icon, |this, icon| this.icon(icon)),
)
})
.when(self.loading, |this| this.child(Indicator::new()))
.when_some(self.label, |this, label| {
this.child(
div()
.flex_none()
.line_height(relative(1.))
.child(label)
.when(self.bold, |this| this.font_semibold()),
)
this.child(div().flex_none().line_height(relative(1.)).child(label))
})
.children(self.children)
.when(self.caret, |this| {
this.justify_between().gap_0p5().child(
Icon::new(IconName::ChevronDown)
.small()
.text_color(cx.theme().text_muted),
)
})
})
.text_color(normal_style.fg)
.when(!self.disabled && !self.selected, |this| {
@@ -496,6 +470,17 @@ impl RenderOnce for Button {
let selected_style = style.selected(cx);
this.bg(selected_style.bg).text_color(selected_style.fg)
})
.when(self.selected && self.underline, |this| {
this.child(
div()
.absolute()
.bottom_0()
.left_0()
.h_px()
.w_full()
.bg(cx.theme().element_background),
)
})
.when(self.disabled, |this| {
let disabled_style = style.disabled(cx);
this.cursor_not_allowed()

View File

@@ -61,8 +61,8 @@ impl RenderOnce for Divider {
.absolute()
.rounded_full()
.map(|this| match self.axis {
Axis::Vertical => this.w(px(2.)).h_full(),
Axis::Horizontal => this.h(px(2.)).w_full(),
Axis::Vertical => this.w(px(1.)).h_full(),
Axis::Horizontal => this.h(px(1.)).w_full(),
})
.bg(self.color.unwrap_or(cx.theme().border_variant)),
)

View File

@@ -4,7 +4,7 @@ use gpui::{
};
use crate::button::Button;
use crate::popup_menu::PopupMenu;
use crate::menu::PopupMenu;
pub enum PanelEvent {
ZoomIn,

View File

@@ -14,7 +14,7 @@ use super::stack_panel::StackPanel;
use super::{ClosePanel, DockArea, PanelEvent, PanelStyle, ToggleZoom};
use crate::button::{Button, ButtonVariants as _};
use crate::dock_area::panel::Panel;
use crate::popup_menu::{PopupMenu, PopupMenuExt};
use crate::menu::{DropdownMenu, PopupMenu};
use crate::tab::tab_bar::TabBar;
use crate::tab::Tab;
use crate::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
@@ -423,7 +423,7 @@ impl TabPanel {
.when(self.is_zoomed, |this| {
this.child(
Button::new("zoom")
.icon(IconName::ArrowIn)
.icon(IconName::Zoom)
.small()
.ghost()
.tooltip("Zoom Out")
@@ -442,7 +442,7 @@ impl TabPanel {
.small()
.ghost()
.rounded()
.popup_menu({
.dropdown_menu({
let zoomable = state.zoomable;
let closable = state.closable;

View File

@@ -0,0 +1,27 @@
use gpui::{canvas, App, Bounds, ParentElement, Pixels, Styled as _, Window};
/// A trait to extend [`gpui::Element`] with additional functionality.
pub trait ElementExt: ParentElement + Sized {
/// Add a prepaint callback to the element.
///
/// This is a helper method to get the bounds of the element after paint.
///
/// The first argument is the bounds of the element in pixels.
///
/// See also [`gpui::canvas`].
fn on_prepaint<F>(self, f: F) -> Self
where
F: FnOnce(Bounds<Pixels>, &mut Window, &mut App) + 'static,
{
self.child(
canvas(
move |bounds, window, cx| f(bounds, window, cx),
|_, _, _, _| {},
)
.absolute()
.size_full(),
)
}
}
impl<T: ParentElement> ElementExt for T {}

294
crates/ui/src/geometry.rs Normal file
View File

@@ -0,0 +1,294 @@
use std::fmt::{self, Debug, Display, Formatter};
use gpui::{AbsoluteLength, Axis, Corner, Length, Pixels};
use serde::{Deserialize, Serialize};
/// A enum for defining the placement of the element.
///
/// See also: [`Side`] if you need to define the left, right side.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Placement {
#[serde(rename = "top")]
Top,
#[serde(rename = "bottom")]
Bottom,
#[serde(rename = "left")]
Left,
#[serde(rename = "right")]
Right,
}
impl Display for Placement {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Placement::Top => write!(f, "Top"),
Placement::Bottom => write!(f, "Bottom"),
Placement::Left => write!(f, "Left"),
Placement::Right => write!(f, "Right"),
}
}
}
impl Placement {
#[inline]
pub fn is_horizontal(&self) -> bool {
matches!(self, Placement::Left | Placement::Right)
}
#[inline]
pub fn is_vertical(&self) -> bool {
matches!(self, Placement::Top | Placement::Bottom)
}
#[inline]
pub fn axis(&self) -> Axis {
match self {
Placement::Top | Placement::Bottom => Axis::Vertical,
Placement::Left | Placement::Right => Axis::Horizontal,
}
}
}
/// The anchor position of an element.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Anchor {
#[default]
#[serde(rename = "top-left")]
TopLeft,
#[serde(rename = "top-center")]
TopCenter,
#[serde(rename = "top-right")]
TopRight,
#[serde(rename = "bottom-left")]
BottomLeft,
#[serde(rename = "bottom-center")]
BottomCenter,
#[serde(rename = "bottom-right")]
BottomRight,
}
impl Display for Anchor {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Anchor::TopLeft => write!(f, "TopLeft"),
Anchor::TopCenter => write!(f, "TopCenter"),
Anchor::TopRight => write!(f, "TopRight"),
Anchor::BottomLeft => write!(f, "BottomLeft"),
Anchor::BottomCenter => write!(f, "BottomCenter"),
Anchor::BottomRight => write!(f, "BottomRight"),
}
}
}
impl Anchor {
/// Returns true if the anchor is at the top.
#[inline]
pub fn is_top(&self) -> bool {
matches!(self, Self::TopLeft | Self::TopCenter | Self::TopRight)
}
/// Returns true if the anchor is at the bottom.
#[inline]
pub fn is_bottom(&self) -> bool {
matches!(
self,
Self::BottomLeft | Self::BottomCenter | Self::BottomRight
)
}
/// Returns true if the anchor is at the left.
#[inline]
pub fn is_left(&self) -> bool {
matches!(self, Self::TopLeft | Self::BottomLeft)
}
/// Returns true if the anchor is at the right.
#[inline]
pub fn is_right(&self) -> bool {
matches!(self, Self::TopRight | Self::BottomRight)
}
/// Returns true if the anchor is at the center.
#[inline]
pub fn is_center(&self) -> bool {
matches!(self, Self::TopCenter | Self::BottomCenter)
}
/// Swaps the vertical position of the anchor.
pub fn swap_vertical(&self) -> Self {
match self {
Anchor::TopLeft => Anchor::BottomLeft,
Anchor::TopCenter => Anchor::BottomCenter,
Anchor::TopRight => Anchor::BottomRight,
Anchor::BottomLeft => Anchor::TopLeft,
Anchor::BottomCenter => Anchor::TopCenter,
Anchor::BottomRight => Anchor::TopRight,
}
}
/// Swaps the horizontal position of the anchor.
pub fn swap_horizontal(&self) -> Self {
match self {
Anchor::TopLeft => Anchor::TopRight,
Anchor::TopCenter => Anchor::TopCenter,
Anchor::TopRight => Anchor::TopLeft,
Anchor::BottomLeft => Anchor::BottomRight,
Anchor::BottomCenter => Anchor::BottomCenter,
Anchor::BottomRight => Anchor::BottomLeft,
}
}
pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor {
match axis {
Axis::Vertical => match self {
Self::TopLeft => Self::BottomLeft,
Self::TopCenter => Self::BottomCenter,
Self::TopRight => Self::BottomRight,
Self::BottomLeft => Self::TopLeft,
Self::BottomCenter => Self::TopCenter,
Self::BottomRight => Self::TopRight,
},
Axis::Horizontal => match self {
Self::TopLeft => Self::TopRight,
Self::TopCenter => Self::TopCenter,
Self::TopRight => Self::TopLeft,
Self::BottomLeft => Self::BottomRight,
Self::BottomCenter => Self::BottomCenter,
Self::BottomRight => Self::BottomLeft,
},
}
}
}
impl From<Corner> for Anchor {
fn from(corner: Corner) -> Self {
match corner {
Corner::TopLeft => Anchor::TopLeft,
Corner::TopRight => Anchor::TopRight,
Corner::BottomLeft => Anchor::BottomLeft,
Corner::BottomRight => Anchor::BottomRight,
}
}
}
impl From<Anchor> for Corner {
fn from(anchor: Anchor) -> Self {
match anchor {
Anchor::TopLeft => Corner::TopLeft,
Anchor::TopRight => Corner::TopRight,
Anchor::BottomLeft => Corner::BottomLeft,
Anchor::BottomRight => Corner::BottomRight,
Anchor::TopCenter => Corner::TopLeft,
Anchor::BottomCenter => Corner::BottomLeft,
}
}
}
/// A enum for defining the side of the element.
///
/// See also: [`Placement`] if you need to define the 4 edges.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Side {
#[serde(rename = "left")]
Left,
#[serde(rename = "right")]
Right,
}
impl Side {
/// Returns true if the side is left.
#[inline]
pub fn is_left(&self) -> bool {
matches!(self, Self::Left)
}
/// Returns true if the side is right.
#[inline]
pub fn is_right(&self) -> bool {
matches!(self, Self::Right)
}
}
/// A trait to extend the [`Axis`] enum with utility methods.
pub trait AxisExt {
#[allow(clippy::wrong_self_convention)]
fn is_horizontal(self) -> bool;
#[allow(clippy::wrong_self_convention)]
fn is_vertical(self) -> bool;
}
impl AxisExt for Axis {
#[inline]
fn is_horizontal(self) -> bool {
self == Axis::Horizontal
}
#[inline]
fn is_vertical(self) -> bool {
self == Axis::Vertical
}
}
/// A trait for converting [`Pixels`] to `f32` and `f64`.
pub trait PixelsExt {
fn as_f32(&self) -> f32;
#[allow(clippy::wrong_self_convention)]
fn as_f64(self) -> f64;
}
impl PixelsExt for Pixels {
fn as_f32(&self) -> f32 {
f32::from(self)
}
fn as_f64(self) -> f64 {
f64::from(self)
}
}
/// A trait to extend the [`Length`] enum with utility methods.
pub trait LengthExt {
/// Converts the [`Length`] to [`Pixels`] based on a given `base_size` and `rem_size`.
///
/// If the [`Length`] is [`Length::Auto`], it returns `None`.
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels>;
}
impl LengthExt for Length {
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels> {
match self {
Length::Auto => None,
Length::Definite(len) => Some(len.to_pixels(base_size, rem_size)),
}
}
}
/// A struct for defining the edges of an element.
///
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
#[repr(C)]
pub struct Edges<T: Clone + Debug + Default + PartialEq> {
/// The size of the top edge.
pub top: T,
/// The size of the right edge.
pub right: T,
/// The size of the bottom edge.
pub bottom: T,
/// The size of the left edge.
pub left: T,
}
impl<T> Edges<T>
where
T: Clone + Debug + Default + PartialEq,
{
/// Creates a new `Edges` instance with all edges set to the same value.
pub fn all(value: T) -> Self {
Self {
top: value.clone(),
right: value.clone(),
bottom: value.clone(),
left: value,
}
}
}

View File

@@ -9,127 +9,113 @@ use crate::{Sizable, Size};
#[derive(IntoElement, Clone)]
pub enum IconName {
ArrowIn,
ArrowDown,
ArrowLeft,
ArrowRight,
ArrowUp,
CaretUp,
Boom,
ChevronDown,
CaretDown,
CaretDownFill,
CaretRight,
CaretUp,
Check,
CheckCircle,
CheckCircleFill,
Close,
CloseCircle,
CloseCircleFill,
Copy,
Edit,
Door,
Ellipsis,
Encryption,
Emoji,
Eye,
EyeOff,
EmojiFill,
Info,
Invite,
Inbox,
InboxFill,
Link,
Loader,
Logout,
Moon,
PanelBottom,
PanelBottomOpen,
PanelLeft,
PanelLeftClose,
PanelLeftOpen,
PanelRight,
PanelRightClose,
PanelRightOpen,
Plus,
PlusFill,
PlusCircleFill,
Group,
ResizeCorner,
PlusCircle,
Profile,
Relay,
Reply,
Report,
Refresh,
Signal,
Search,
Settings,
Server,
SortAscending,
SortDescending,
Sun,
ThumbsDown,
ThumbsUp,
Ship,
Shield,
Upload,
OpenUrl,
Usb,
PanelLeft,
PanelLeftOpen,
PanelRight,
PanelRightOpen,
PanelBottom,
PanelBottomOpen,
PaperPlaneFill,
Warning,
WindowClose,
WindowMaximize,
WindowMinimize,
WindowRestore,
Fistbump,
FistbumpFill,
Zoom,
}
impl IconName {
pub fn path(self) -> SharedString {
match self {
Self::ArrowIn => "icons/arrows-in.svg",
Self::ArrowDown => "icons/arrow-down.svg",
Self::ArrowLeft => "icons/arrow-left.svg",
Self::ArrowRight => "icons/arrow-right.svg",
Self::ArrowUp => "icons/arrow-up.svg",
Self::Boom => "icons/boom.svg",
Self::ChevronDown => "icons/chevron-down.svg",
Self::CaretDown => "icons/caret-down.svg",
Self::CaretRight => "icons/caret-right.svg",
Self::CaretUp => "icons/caret-up.svg",
Self::CaretDown => "icons/caret-down.svg",
Self::CaretDownFill => "icons/caret-down-fill.svg",
Self::Check => "icons/check.svg",
Self::CheckCircle => "icons/check-circle.svg",
Self::CheckCircleFill => "icons/check-circle-fill.svg",
Self::Close => "icons/close.svg",
Self::CloseCircle => "icons/close-circle.svg",
Self::CloseCircleFill => "icons/close-circle-fill.svg",
Self::Copy => "icons/copy.svg",
Self::Edit => "icons/edit.svg",
Self::Door => "icons/door.svg",
Self::Ellipsis => "icons/ellipsis.svg",
Self::Emoji => "icons/emoji.svg",
Self::Eye => "icons/eye.svg",
Self::Encryption => "icons/encryption.svg",
Self::EmojiFill => "icons/emoji-fill.svg",
Self::EyeOff => "icons/eye-off.svg",
Self::Info => "icons/info.svg",
Self::Invite => "icons/invite.svg",
Self::Inbox => "icons/inbox.svg",
Self::InboxFill => "icons/inbox-fill.svg",
Self::Link => "icons/link.svg",
Self::Loader => "icons/loader.svg",
Self::Logout => "icons/logout.svg",
Self::Moon => "icons/moon.svg",
Self::PanelBottom => "icons/panel-bottom.svg",
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
Self::PanelLeft => "icons/panel-left.svg",
Self::PanelLeftClose => "icons/panel-left-close.svg",
Self::PanelLeftOpen => "icons/panel-left-open.svg",
Self::PanelRight => "icons/panel-right.svg",
Self::PanelRightClose => "icons/panel-right-close.svg",
Self::PanelRightOpen => "icons/panel-right-open.svg",
Self::Plus => "icons/plus.svg",
Self::PlusFill => "icons/plus-fill.svg",
Self::PlusCircleFill => "icons/plus-circle-fill.svg",
Self::Group => "icons/group.svg",
Self::ResizeCorner => "icons/resize-corner.svg",
Self::PlusCircle => "icons/plus-circle.svg",
Self::Profile => "icons/profile.svg",
Self::Relay => "icons/relay.svg",
Self::Reply => "icons/reply.svg",
Self::Report => "icons/report.svg",
Self::Refresh => "icons/refresh.svg",
Self::Signal => "icons/signal.svg",
Self::Search => "icons/search.svg",
Self::Settings => "icons/settings.svg",
Self::Server => "icons/server.svg",
Self::SortAscending => "icons/sort-ascending.svg",
Self::SortDescending => "icons/sort-descending.svg",
Self::Sun => "icons/sun.svg",
Self::ThumbsDown => "icons/thumbs-down.svg",
Self::ThumbsUp => "icons/thumbs-up.svg",
Self::Ship => "icons/ship.svg",
Self::Shield => "icons/shield.svg",
Self::Upload => "icons/upload.svg",
Self::OpenUrl => "icons/open-url.svg",
Self::Usb => "icons/usb.svg",
Self::PanelLeft => "icons/panel-left.svg",
Self::PanelLeftOpen => "icons/panel-left-open.svg",
Self::PanelRight => "icons/panel-right.svg",
Self::PanelRightOpen => "icons/panel-right-open.svg",
Self::PanelBottom => "icons/panel-bottom.svg",
Self::PanelBottomOpen => "icons/panel-bottom-open.svg",
Self::PaperPlaneFill => "icons/paper-plane-fill.svg",
Self::Warning => "icons/warning.svg",
Self::WindowClose => "icons/window-close.svg",
Self::WindowMaximize => "icons/window-maximize.svg",
Self::WindowMinimize => "icons/window-minimize.svg",
Self::WindowRestore => "icons/window-restore.svg",
Self::Fistbump => "icons/fistbump.svg",
Self::FistbumpFill => "icons/fistbump-fill.svg",
Self::Zoom => "icons/zoom.svg",
}
.into()
}

View File

@@ -0,0 +1,69 @@
use std::fmt::{Debug, Display};
use gpui::ElementId;
/// Represents an index path in a list, which consists of a section index,
///
/// The default values for section, row, and column are all set to 0.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct IndexPath {
/// The section index.
pub section: usize,
/// The item index in the section.
pub row: usize,
/// The column index.
pub column: usize,
}
impl From<IndexPath> for ElementId {
fn from(path: IndexPath) -> Self {
ElementId::Name(format!("index-path({},{},{})", path.section, path.row, path.column).into())
}
}
impl Display for IndexPath {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"IndexPath(section: {}, row: {}, column: {})",
self.section, self.row, self.column
)
}
}
impl IndexPath {
/// Create a new index path with the specified section and row.
///
/// The `section` is set to 0 by default.
/// The `column` is set to 0 by default.
pub fn new(row: usize) -> Self {
IndexPath {
section: 0,
row,
..Default::default()
}
}
/// Set the section for the index path.
pub fn section(mut self, section: usize) -> Self {
self.section = section;
self
}
/// Set the row for the index path.
pub fn row(mut self, row: usize) -> Self {
self.row = row;
self
}
/// Set the column for the index path.
pub fn column(mut self, column: usize) -> Self {
self.column = column;
self
}
/// Check if the self is equal to the given index path (Same section and row).
pub fn eq_row(&self, index: IndexPath) -> bool {
self.section == index.section && self.row == index.row
}
}

View File

@@ -1009,8 +1009,7 @@ impl InputState {
let left_part = self.text.slice(0..offset).to_string();
UnicodeSegmentation::split_word_bound_indices(left_part.as_str())
.filter(|(_, s)| !s.trim_start().is_empty())
.next_back()
.rfind(|(_, s)| !s.trim_start().is_empty())
.map(|(i, _)| i)
.unwrap_or(0)
}

View File

@@ -145,6 +145,7 @@ impl Styled for TextInput {
impl RenderOnce for TextInput {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
const LINE_HEIGHT: Rems = Rems(1.25);
let font = window.text_style().font();
let font_size = window.text_style().font_size.to_pixels(window.rem_size());
@@ -155,6 +156,7 @@ impl RenderOnce for TextInput {
});
let state = self.state.read(cx);
let focused = state.focus_handle.is_focused(window) && !state.disabled;
let gap_x = match self.size {
Size::Small => px(4.),
@@ -266,7 +268,16 @@ impl RenderOnce for TextInput {
.when_some(self.height, |this, height| this.h(height))
})
.when(self.appearance, |this| {
this.bg(bg).rounded(cx.theme().radius)
this.bg(bg)
.rounded(cx.theme().radius)
.when(self.bordered, |this| {
this.border_color(cx.theme().border)
.border_1()
.when(cx.theme().shadow, |this| this.shadow_xs())
.when(focused && self.focus_bordered, |this| {
this.border_color(cx.theme().border_focused)
})
})
})
.items_center()
.gap(gap_x)

View File

@@ -1,11 +1,14 @@
pub use anchored::*;
pub use element_ext::ElementExt;
pub use event::InteractiveElementExt;
pub use focusable::FocusableCycle;
pub use geometry::*;
pub use icon::*;
pub use index_path::IndexPath;
pub use kbd::*;
pub use menu::{context_menu, popup_menu};
pub use root::{ContextModal, Root};
pub use root::{window_paddings, Root};
pub use styled::*;
pub use window_border::{window_border, WindowBorder};
pub use window_ext::*;
pub use crate::Disableable;
@@ -16,7 +19,6 @@ pub mod button;
pub mod checkbox;
pub mod divider;
pub mod dock_area;
pub mod dropdown;
pub mod history;
pub mod indicator;
pub mod input;
@@ -32,20 +34,23 @@ pub mod switch;
pub mod tab;
pub mod tooltip;
mod anchored;
mod element_ext;
mod event;
mod focusable;
mod geometry;
mod icon;
mod index_path;
mod kbd;
mod root;
mod styled;
mod window_border;
mod window_ext;
/// Initialize the UI module.
///
/// This must be called before using any of the UI components.
/// You can initialize the UI module at your application's entry point.
pub fn init(cx: &mut gpui::App) {
dropdown::init(cx);
input::init(cx);
list::init(cx);
modal::init(cx);

221
crates/ui/src/list/cache.rs Normal file
View File

@@ -0,0 +1,221 @@
use std::rc::Rc;
use gpui::{App, Pixels, Size};
use crate::IndexPath;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum RowEntry {
Entry(IndexPath),
SectionHeader(usize),
SectionFooter(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) struct MeasuredEntrySize {
pub(crate) item_size: Size<Pixels>,
pub(crate) section_header_size: Size<Pixels>,
pub(crate) section_footer_size: Size<Pixels>,
}
impl RowEntry {
#[inline]
#[allow(unused)]
pub(crate) fn is_section_header(&self) -> bool {
matches!(self, RowEntry::SectionHeader(_))
}
pub(crate) fn eq_index_path(&self, path: &IndexPath) -> bool {
match self {
RowEntry::Entry(index_path) => index_path == path,
RowEntry::SectionHeader(_) | RowEntry::SectionFooter(_) => false,
}
}
#[allow(unused)]
pub(crate) fn index(&self) -> IndexPath {
match self {
RowEntry::Entry(index_path) => *index_path,
RowEntry::SectionHeader(ix) => IndexPath::default().section(*ix),
RowEntry::SectionFooter(ix) => IndexPath::default().section(*ix),
}
}
#[inline]
#[allow(unused)]
pub(crate) fn is_section_footer(&self) -> bool {
matches!(self, RowEntry::SectionFooter(_))
}
#[inline]
pub(crate) fn is_entry(&self) -> bool {
matches!(self, RowEntry::Entry(_))
}
#[inline]
#[allow(unused)]
pub(crate) fn section_ix(&self) -> Option<usize> {
match self {
RowEntry::SectionHeader(ix) | RowEntry::SectionFooter(ix) => Some(*ix),
_ => None,
}
}
}
#[derive(Default, Clone)]
pub(crate) struct RowsCache {
/// Only have section's that have rows.
pub(crate) entities: Rc<Vec<RowEntry>>,
pub(crate) items_count: usize,
/// The sections, the item is number of rows in each section.
pub(crate) sections: Rc<Vec<usize>>,
pub(crate) entries_sizes: Rc<Vec<Size<Pixels>>>,
measured_size: MeasuredEntrySize,
}
impl RowsCache {
pub(crate) fn get(&self, flatten_ix: usize) -> Option<RowEntry> {
self.entities.get(flatten_ix).cloned()
}
/// Returns the number of flattened rows (Includes header, item, footer).
pub(crate) fn len(&self) -> usize {
self.entities.len()
}
/// Return the number of items in the cache.
pub(crate) fn items_count(&self) -> usize {
self.items_count
}
/// Returns the index of the Entry with given path in the flattened rows.
pub(crate) fn position_of(&self, path: &IndexPath) -> Option<usize> {
self.entities
.iter()
.position(|p| p.is_entry() && p.eq_index_path(path))
}
/// Return prev row, if the row is the first in the first section, goes to the last row.
///
/// Empty rows section are skipped.
pub(crate) fn prev(&self, path: Option<IndexPath>) -> IndexPath {
let path = path.unwrap_or_default();
let Some(pos) = self.position_of(&path) else {
return self
.entities
.iter()
.rfind(|entry| entry.is_entry())
.map(|entry| entry.index())
.unwrap_or_default();
};
if let Some(path) = self
.entities
.iter()
.take(pos)
.rev()
.find(|entry| entry.is_entry())
.map(|entry| entry.index())
{
path
} else {
self.entities
.iter()
.rfind(|entry| entry.is_entry())
.map(|entry| entry.index())
.unwrap_or_default()
}
}
/// Returns the next row, if the row is the last in the last section, goes to the first row.
///
/// Empty rows section are skipped.
pub(crate) fn next(&self, path: Option<IndexPath>) -> IndexPath {
let Some(mut path) = path else {
return IndexPath::default();
};
let Some(pos) = self.position_of(&path) else {
return self
.entities
.iter()
.find(|entry| entry.is_entry())
.map(|entry| entry.index())
.unwrap_or_default();
};
if let Some(next_path) = self
.entities
.iter()
.skip(pos + 1)
.find(|entry| entry.is_entry())
.map(|entry| entry.index())
{
path = next_path;
} else {
path = self
.entities
.iter()
.find(|entry| entry.is_entry())
.map(|entry| entry.index())
.unwrap_or_default()
}
path
}
pub(crate) fn prepare_if_needed<F>(
&mut self,
sections_count: usize,
measured_size: MeasuredEntrySize,
cx: &App,
rows_count_f: F,
) where
F: Fn(usize, &App) -> usize,
{
let mut new_sections = vec![];
for section_ix in 0..sections_count {
new_sections.push(rows_count_f(section_ix, cx));
}
let need_update = new_sections != *self.sections || self.measured_size != measured_size;
if !need_update {
return;
}
let mut entries_sizes = vec![];
let mut total_items_count = 0;
self.measured_size = measured_size;
self.sections = Rc::new(new_sections);
self.entities = Rc::new(
self.sections
.iter()
.enumerate()
.flat_map(|(section, items_count)| {
total_items_count += items_count;
let mut children = vec![];
if *items_count == 0 {
return children;
}
children.push(RowEntry::SectionHeader(section));
entries_sizes.push(measured_size.section_header_size);
for row in 0..*items_count {
children.push(RowEntry::Entry(IndexPath {
section,
row,
..Default::default()
}));
entries_sizes.push(measured_size.item_size);
}
children.push(RowEntry::SectionFooter(section));
entries_sizes.push(measured_size.section_footer_size);
children
})
.collect(),
);
self.entries_sizes = Rc::new(entries_sizes);
self.items_count = total_items_count;
}
}

View File

@@ -0,0 +1,171 @@
use gpui::{AnyElement, App, Context, IntoElement, ParentElement as _, Styled as _, Task, Window};
use theme::ActiveTheme;
use crate::list::loading::Loading;
use crate::list::ListState;
use crate::{h_flex, Icon, IconName, IndexPath, Selectable};
/// A delegate for the List.
#[allow(unused)]
pub trait ListDelegate: Sized + 'static {
type Item: Selectable + IntoElement;
/// When Query Input change, this method will be called.
/// You can perform search here.
fn perform_search(
&mut self,
query: &str,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> Task<()> {
Task::ready(())
}
/// Return the number of sections in the list, default is 1.
///
/// Min value is 1.
fn sections_count(&self, cx: &App) -> usize {
1
}
/// Return the number of items in the section at the given index.
///
/// NOTE: Only the sections with items_count > 0 will be rendered. If the section has 0 items,
/// the section header and footer will also be skipped.
fn items_count(&self, section: usize, cx: &App) -> usize;
/// Render the item at the given index.
///
/// Return None will skip the item.
///
/// NOTE: Every item should have same height.
fn render_item(
&mut self,
ix: IndexPath,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> Option<Self::Item>;
/// Render the section header at the given index, default is None.
///
/// NOTE: Every header should have same height.
fn render_section_header(
&mut self,
section: usize,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> Option<impl IntoElement> {
None::<AnyElement>
}
/// Render the section footer at the given index, default is None.
///
/// NOTE: Every footer should have same height.
fn render_section_footer(
&mut self,
section: usize,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> Option<impl IntoElement> {
None::<AnyElement>
}
/// Return a Element to show when list is empty.
fn render_empty(
&mut self,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> impl IntoElement {
h_flex()
.size_full()
.justify_center()
.text_color(cx.theme().text_muted.opacity(0.6))
.child(Icon::new(IconName::Inbox).size_12())
.into_any_element()
}
/// Returns Some(AnyElement) to render the initial state of the list.
///
/// This can be used to show a view for the list before the user has
/// interacted with it.
///
/// For example: The last search results, or the last selected item.
///
/// Default is None, that means no initial state.
fn render_initial(
&mut self,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> Option<AnyElement> {
None
}
/// Returns the loading state to show the loading view.
fn loading(&self, cx: &App) -> bool {
false
}
/// Returns a Element to show when loading, default is built-in Skeleton
/// loading view.
fn render_loading(
&mut self,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) -> impl IntoElement {
Loading
}
/// Set the selected index, just store the ix, don't confirm.
fn set_selected_index(
&mut self,
ix: Option<IndexPath>,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
);
/// Set the index of the item that has been right clicked.
fn set_right_clicked_index(
&mut self,
ix: Option<IndexPath>,
window: &mut Window,
cx: &mut Context<ListState<Self>>,
) {
}
/// Set the confirm and give the selected index,
/// this is means user have clicked the item or pressed Enter.
///
/// This will always to `set_selected_index` before confirm.
fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<ListState<Self>>) {
}
/// Cancel the selection, e.g.: Pressed ESC.
fn cancel(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
/// Return true to enable load more data when scrolling to the bottom.
///
/// Default: false
fn has_more(&self, cx: &App) -> bool {
false
}
/// Returns a threshold value (n entities), of course,
/// when scrolling to the bottom, the remaining number of rows
/// triggers `load_more`.
///
/// This should smaller than the total number of first load rows.
///
/// Default: 20 entities (section header, footer and row)
fn load_more_threshold(&self) -> usize {
20
}
/// Load more data when the table is scrolled to the bottom.
///
/// This will performed in a background task.
///
/// This is always called when the table is near the bottom,
/// so you must check if there is more data to load or lock
/// the loading state.
fn load_more(&mut self, window: &mut Window, cx: &mut Context<ListState<Self>>) {}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,57 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, Styled,
Window,
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _,
StyleRefinement, Styled, Window,
};
use smallvec::SmallVec;
use theme::ActiveTheme;
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable as _};
use crate::{h_flex, Disableable, Icon, Selectable, Sizable as _, StyledExt};
type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>;
type OnMouseEnter = Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>;
type Suffix = Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum ListItemMode {
#[default]
Entry,
Separator,
}
impl ListItemMode {
#[inline]
fn is_separator(&self) -> bool {
matches!(self, ListItemMode::Separator)
}
}
#[derive(IntoElement)]
pub struct ListItem {
base: Stateful<Div>,
mode: ListItemMode,
style: StyleRefinement,
disabled: bool,
selected: bool,
secondary_selected: bool,
confirmed: bool,
check_icon: Option<Icon>,
on_click: OnClick,
on_mouse_enter: OnMouseEnter,
suffix: Suffix,
#[allow(clippy::type_complexity)]
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
#[allow(clippy::type_complexity)]
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
#[allow(clippy::type_complexity)]
suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
impl ListItem {
pub fn new(id: impl Into<ElementId>) -> Self {
let id: ElementId = id.into();
Self {
base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(),
mode: ListItemMode::Entry,
base: h_flex().id(id),
style: StyleRefinement::default(),
disabled: false,
selected: false,
secondary_selected: false,
confirmed: false,
on_click: None,
on_mouse_enter: None,
@@ -43,9 +61,15 @@ impl ListItem {
}
}
/// Set this list item to as a separator, it not able to be selected.
pub fn separator(mut self) -> Self {
self.mode = ListItemMode::Separator;
self
}
/// Set to show check icon, default is None.
pub fn check_icon(mut self, icon: IconName) -> Self {
self.check_icon = Some(Icon::new(icon));
pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
self.check_icon = Some(icon.into());
self
}
@@ -111,11 +135,16 @@ impl Selectable for ListItem {
fn is_selected(&self) -> bool {
self.selected
}
fn secondary_selected(mut self, selected: bool) -> Self {
self.secondary_selected = selected;
self
}
}
impl Styled for ListItem {
fn style(&mut self) -> &mut gpui::StyleRefinement {
self.base.style()
&mut self.style
}
}
@@ -127,35 +156,39 @@ impl ParentElement for ListItem {
impl RenderOnce for ListItem {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let is_active = self.selected || self.confirmed;
let is_active = self.confirmed || self.selected;
let corner_radii = self.style.corner_radii.clone();
let _selected_style = StyleRefinement {
corner_radii,
..Default::default()
};
let is_selectable = !(self.disabled || self.mode.is_separator());
self.base
.relative()
.gap_x_1()
.py_1()
.px_3()
.text_base()
.text_color(cx.theme().text)
.relative()
.items_center()
.justify_between()
.when_some(self.on_click, |this, on_click| {
if !self.disabled {
this.cursor_pointer()
.on_mouse_down(MouseButton::Left, move |_, _window, cx| {
cx.stop_propagation();
})
.on_click(on_click)
} else {
this
}
.refine_style(&self.style)
.when(is_selectable, |this| {
this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
})
.when(!is_active, |this| {
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
})
})
.when(is_active, |this| this.bg(cx.theme().element_active))
.when(!is_active && !self.disabled, |this| {
this.hover(|this| this.bg(cx.theme().elevated_surface_background))
})
// Mouse enter
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
if !self.disabled {
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
} else {
this
}
.when(!is_selectable, |this| {
this.text_color(cx.theme().text_muted)
})
.child(
h_flex()
@@ -177,5 +210,17 @@ impl RenderOnce for ListItem {
}),
)
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
.map(|this| {
if is_selectable && (self.selected || self.secondary_selected) {
let bg = if self.selected {
cx.theme().ghost_element_active
} else {
cx.theme().ghost_element_background
};
this.bg(bg)
} else {
this
}
})
}
}

View File

@@ -17,7 +17,7 @@ impl RenderOnce for LoadingItem {
.gap_1p5()
.overflow_hidden()
.child(Skeleton::new().h_5().w_48().max_w_full())
.child(Skeleton::new().secondary(true).h_3().w_64().max_w_full()),
.child(Skeleton::new().secondary().h_3().w_64().max_w_full()),
)
}
}

View File

@@ -1,7 +1,28 @@
pub(crate) mod cache;
mod delegate;
#[allow(clippy::module_inception)]
mod list;
mod list_item;
mod loading;
mod separator_item;
pub use delegate::*;
pub use list::*;
pub use list_item::*;
pub use separator_item::*;
use serde::{Deserialize, Serialize};
/// Settings for List.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListSettings {
/// Whether to use active highlight style on ListItem, default
pub active_highlight: bool,
}
impl Default for ListSettings {
fn default() -> Self {
Self {
active_highlight: true,
}
}
}

View File

@@ -0,0 +1,50 @@
use gpui::{AnyElement, ParentElement, RenderOnce, StyleRefinement};
use smallvec::SmallVec;
use crate::list::ListItem;
use crate::{Selectable, StyledExt};
pub struct ListSeparatorItem {
style: StyleRefinement,
children: SmallVec<[AnyElement; 2]>,
}
impl ListSeparatorItem {
pub fn new() -> Self {
Self {
style: StyleRefinement::default(),
children: SmallVec::new(),
}
}
}
impl Default for ListSeparatorItem {
fn default() -> Self {
Self::new()
}
}
impl ParentElement for ListSeparatorItem {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl Selectable for ListSeparatorItem {
fn selected(self, _: bool) -> Self {
self
}
fn is_selected(&self) -> bool {
false
}
}
impl RenderOnce for ListSeparatorItem {
fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement {
ListItem::new("separator")
.refine_style(&self.style)
.children(self.children)
.disabled(true)
}
}

View File

@@ -1,16 +1,17 @@
use gpui::prelude::FluentBuilder;
use gpui::{
anchored, deferred, div, px, App, AppContext as _, ClickEvent, Context, DismissEvent, Entity,
Focusable, InteractiveElement as _, IntoElement, KeyBinding, OwnedMenu, ParentElement, Render,
SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu,
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
};
use crate::actions::{Cancel, SelectLeft, SelectRight};
use crate::button::{Button, ButtonVariants};
use crate::popup_menu::PopupMenu;
use crate::menu::PopupMenu;
use crate::{h_flex, Selectable, Sizable};
const CONTEXT: &str = "AppMenuBar";
pub fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
@@ -22,67 +23,74 @@ pub fn init(cx: &mut App) {
/// The application menu bar, for Windows and Linux.
pub struct AppMenuBar {
menus: Vec<Entity<AppMenu>>,
selected_ix: Option<usize>,
selected_index: Option<usize>,
}
impl AppMenuBar {
/// Create a new app menu bar.
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
pub fn new(cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
let menu_bar = cx.entity();
let menus = cx
.get_menus()
.unwrap_or_default()
.iter()
.enumerate()
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), window, cx))
.collect();
Self {
selected_ix: None,
menus,
}
let mut this = Self {
selected_index: None,
menus: Vec::new(),
};
this.reload(cx);
this
})
}
fn move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_ix) = self.selected_ix else {
/// Reload the menus from the app.
pub fn reload(&mut self, cx: &mut Context<Self>) {
let menu_bar = cx.entity();
self.menus = cx
.get_menus()
.unwrap_or_default()
.iter()
.enumerate()
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), cx))
.collect();
self.selected_index = None;
cx.notify();
}
fn on_move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_index) = self.selected_index else {
return;
};
let new_ix = if selected_ix == 0 {
let new_ix = if selected_index == 0 {
self.menus.len().saturating_sub(1)
} else {
selected_ix.saturating_sub(1)
selected_index.saturating_sub(1)
};
self.set_selected_ix(Some(new_ix), window, cx);
self.set_selected_index(Some(new_ix), window, cx);
}
fn move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_ix) = self.selected_ix else {
fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
let Some(selected_index) = self.selected_index else {
return;
};
let new_ix = if selected_ix + 1 >= self.menus.len() {
let new_ix = if selected_index + 1 >= self.menus.len() {
0
} else {
selected_ix + 1
selected_index + 1
};
self.set_selected_ix(Some(new_ix), window, cx);
self.set_selected_index(Some(new_ix), window, cx);
}
fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.set_selected_ix(None, window, cx);
fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.set_selected_index(None, window, cx);
}
fn set_selected_ix(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
self.selected_ix = ix;
fn set_selected_index(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
self.selected_index = ix;
cx.notify();
}
#[inline]
fn has_activated_menu(&self) -> bool {
self.selected_ix.is_some()
self.selected_index.is_some()
}
}
@@ -91,9 +99,9 @@ impl Render for AppMenuBar {
h_flex()
.id("app-menu-bar")
.key_context(CONTEXT)
.on_action(cx.listener(Self::move_left))
.on_action(cx.listener(Self::move_right))
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::on_move_left))
.on_action(cx.listener(Self::on_move_right))
.on_action(cx.listener(Self::on_cancel))
.size_full()
.gap_x_1()
.overflow_x_scroll()
@@ -117,7 +125,6 @@ impl AppMenu {
ix: usize,
menu: &OwnedMenu,
menu_bar: Entity<AppMenuBar>,
_: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let name = menu.name.clone();
@@ -173,7 +180,7 @@ impl AppMenu {
self._subscription.take();
self.popup_menu.take();
self.menu_bar.update(cx, |state, cx| {
state.cancel(&Cancel, window, cx);
state.on_cancel(&Cancel, window, cx);
});
}
@@ -183,11 +190,11 @@ impl AppMenu {
window: &mut Window,
cx: &mut Context<Self>,
) {
let is_selected = self.menu_bar.read(cx).selected_ix == Some(self.ix);
let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix);
self.menu_bar.update(cx, |state, cx| {
let new_ix = if is_selected { None } else { Some(self.ix) };
state.set_selected_ix(new_ix, window, cx);
state.set_selected_index(new_ix, window, cx);
});
}
@@ -202,7 +209,7 @@ impl AppMenu {
}
self.menu_bar.update(cx, |state, cx| {
state.set_selected_ix(Some(self.ix), window, cx);
state.set_selected_index(Some(self.ix), window, cx);
});
}
}
@@ -210,7 +217,7 @@ impl AppMenu {
impl Render for AppMenu {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let menu_bar = self.menu_bar.read(cx);
let is_selected = menu_bar.selected_ix == Some(self.ix);
let is_selected = menu_bar.selected_index == Some(self.ix);
div()
.id(self.ix)
@@ -219,10 +226,15 @@ impl Render for AppMenu {
Button::new("menu")
.small()
.py_0p5()
.xsmall()
.compact()
.ghost()
.label(self.name.clone())
.selected(is_selected)
.on_mouse_down(MouseButton::Left, |_, window, cx| {
// Stop propagation to avoid dragging the window.
window.prevent_default();
cx.stop_propagation();
})
.on_click(cx.listener(Self::handle_trigger_click)),
)
.on_hover(cx.listener(Self::handle_hover))

View File

@@ -3,49 +3,66 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
anchored, deferred, div, px, relative, AnyElement, App, Context, Corner, DismissEvent, Element,
ElementId, Entity, Focusable, GlobalElementId, InspectorElementId, InteractiveElement,
IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Position, Stateful,
Style, Subscription, Window,
anchored, deferred, div, px, AnyElement, App, Context, Corner, DismissEvent, Element,
ElementId, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
StyleRefinement, Styled, Subscription, Window,
};
use crate::popup_menu::PopupMenu;
use crate::menu::PopupMenu;
pub trait ContextMenuExt: ParentElement + Sized {
/// A extension trait for adding a context menu to an element.
pub trait ContextMenuExt: ParentElement + Styled {
/// Add a context menu to the element.
///
/// This will changed the element to be `relative` positioned, and add a child `ContextMenu` element.
/// Because the `ContextMenu` element is positioned `absolute`, it will not affect the layout of the parent element.
fn context_menu(
self,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Self {
self.child(ContextMenu::new("context-menu").menu(f))
) -> ContextMenu<Self>
where
Self: Sized,
{
// Generate a unique ID based on the element's memory address to ensure
// each context menu has its own state and doesn't share with others
let id = format!("context-menu-{:p}", &self as *const _);
ContextMenu::new(id, self).menu(f)
}
}
impl<E> ContextMenuExt for Stateful<E> where E: ParentElement {}
impl<E: ParentElement + Styled> ContextMenuExt for E {}
/// A context menu that can be shown on right-click.
#[allow(clippy::type_complexity)]
pub struct ContextMenu {
pub struct ContextMenu<E: ParentElement + Styled + Sized> {
id: ElementId,
menu:
Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
element: Option<E>,
#[allow(clippy::type_complexity)]
menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
// This is not in use, just for style refinement forwarding.
_ignore_style: StyleRefinement,
anchor: Corner,
}
impl ContextMenu {
pub fn new(id: impl Into<ElementId>) -> Self {
impl<E: ParentElement + Styled> ContextMenu<E> {
/// Create a new context menu with the given ID.
pub fn new(id: impl Into<ElementId>, element: E) -> Self {
Self {
id: id.into(),
element: Some(element),
menu: None,
anchor: Corner::TopLeft,
_ignore_style: StyleRefinement::default(),
}
}
/// Build the context menu using the given builder function.
#[must_use]
pub fn menu<F>(mut self, builder: F) -> Self
fn menu<F>(mut self, builder: F) -> Self
where
F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
{
self.menu = Some(Box::new(builder));
self.menu = Some(Rc::new(builder));
self
}
@@ -67,7 +84,25 @@ impl ContextMenu {
}
}
impl IntoElement for ContextMenu {
impl<E: ParentElement + Styled> ParentElement for ContextMenu<E> {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
if let Some(element) = &mut self.element {
element.extend(elements);
}
}
}
impl<E: ParentElement + Styled> Styled for ContextMenu<E> {
fn style(&mut self) -> &mut StyleRefinement {
if let Some(element) = &mut self.element {
element.style()
} else {
&mut self._ignore_style
}
}
}
impl<E: ParentElement + Styled + IntoElement + 'static> IntoElement for ContextMenu<E> {
type Element = Self;
fn into_element(self) -> Self::Element {
@@ -83,14 +118,14 @@ struct ContextMenuSharedState {
}
pub struct ContextMenuState {
menu_element: Option<AnyElement>,
element: Option<AnyElement>,
shared_state: Rc<RefCell<ContextMenuSharedState>>,
}
impl Default for ContextMenuState {
fn default() -> Self {
Self {
menu_element: None,
element: None,
shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
menu_view: None,
open: false,
@@ -101,8 +136,8 @@ impl Default for ContextMenuState {
}
}
impl Element for ContextMenu {
type PrepaintState = ();
impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<E> {
type PrepaintState = Hitbox;
type RequestLayoutState = ContextMenuState;
fn id(&self) -> Option<ElementId> {
@@ -113,7 +148,6 @@ impl Element for ContextMenu {
None
}
#[allow(clippy::field_reassign_with_default)]
fn request_layout(
&mut self,
id: Option<&gpui::GlobalElementId>,
@@ -121,71 +155,73 @@ impl Element for ContextMenu {
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
// Set the layout style relative to the table view to get same size.
style.position = Position::Absolute;
style.flex_grow = 1.0;
style.flex_shrink = 1.0;
style.size.width = relative(1.).into();
style.size.height = relative(1.).into();
let anchor = self.anchor;
self.with_element_state(
id.unwrap(),
window,
cx,
|_, state: &mut ContextMenuState, window, cx| {
|this, state: &mut ContextMenuState, window, cx| {
let (position, open) = {
let shared_state = state.shared_state.borrow();
(shared_state.position, shared_state.open)
};
let menu_view = state.shared_state.borrow().menu_view.clone();
let (menu_element, menu_layout_id) = if open {
let mut menu_element = None;
if open {
let has_menu_item = menu_view
.as_ref()
.map(|menu| !menu.read(cx).is_empty())
.unwrap_or(false);
if has_menu_item {
let mut menu_element = deferred(
anchored()
.position(position)
.snap_to_window_with_margin(px(8.))
.anchor(anchor)
.when_some(menu_view, |this, menu| {
// Focus the menu, so that can be handle the action.
if !menu.focus_handle(cx).contains_focused(window, cx) {
menu.focus_handle(cx).focus(window, cx);
}
menu_element = Some(
deferred(
anchored().child(
div()
.w(window.bounds().size.width)
.h(window.bounds().size.height)
.on_scroll_wheel(|_, _, cx| {
cx.stop_propagation();
})
.child(
anchored()
.position(position)
.snap_to_window_with_margin(px(8.))
.anchor(anchor)
.when_some(menu_view, |this, menu| {
// Focus the menu, so that can be handle the action.
if !menu
.focus_handle(cx)
.contains_focused(window, cx)
{
menu.focus_handle(cx).focus(window, cx);
}
this.child(div().occlude().child(menu.clone()))
}),
)
.with_priority(1)
.into_any();
let menu_layout_id = menu_element.request_layout(window, cx);
(Some(menu_element), Some(menu_layout_id))
} else {
(None, None)
this.child(menu.clone())
}),
),
),
)
.with_priority(1)
.into_any(),
);
}
} else {
(None, None)
};
let mut layout_ids = vec![];
if let Some(menu_layout_id) = menu_layout_id {
layout_ids.push(menu_layout_id);
}
let layout_id = window.request_layout(style, layout_ids, cx);
let mut element = this
.element
.take()
.expect("Element should exists.")
.children(menu_element)
.into_any_element();
let layout_id = element.request_layout(window, cx);
(
layout_id,
ContextMenuState {
menu_element,
element: Some(element),
..Default::default()
},
)
@@ -197,33 +233,33 @@ impl Element for ContextMenu {
&mut self,
_: Option<&gpui::GlobalElementId>,
_: Option<&InspectorElementId>,
_: gpui::Bounds<gpui::Pixels>,
bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
if let Some(menu_element) = &mut request_layout.menu_element {
menu_element.prepaint(window, cx);
if let Some(element) = &mut request_layout.element {
element.prepaint(window, cx);
}
window.insert_hitbox(bounds, HitboxBehavior::Normal)
}
fn paint(
&mut self,
id: Option<&gpui::GlobalElementId>,
_: Option<&InspectorElementId>,
bounds: gpui::Bounds<gpui::Pixels>,
_: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
hitbox: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
if let Some(menu_element) = &mut request_layout.menu_element {
menu_element.paint(window, cx);
if let Some(element) = &mut request_layout.element {
element.paint(window, cx);
}
let Some(builder) = self.menu.take() else {
return;
};
// Take the builder before setting up element state to avoid borrow issues
let builder = self.menu.clone();
self.with_element_state(
id.unwrap(),
@@ -232,34 +268,53 @@ impl Element for ContextMenu {
|_view, state: &mut ContextMenuState, window, _| {
let shared_state = state.shared_state.clone();
let hitbox = hitbox.clone();
// When right mouse click, to build content menu, and show it at the mouse position.
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase.bubble()
&& event.button == MouseButton::Right
&& bounds.contains(&event.position)
&& hitbox.is_hovered(window)
{
{
let mut shared_state = shared_state.borrow_mut();
// Clear any existing menu view to allow immediate replacement
// Set the new position and open the menu
shared_state.menu_view = None;
shared_state._subscription = None;
shared_state.position = event.position;
shared_state.open = true;
}
let menu = PopupMenu::build(window, cx, |menu, window, cx| {
(builder)(menu, window, cx)
})
.into_element();
let _subscription = window.subscribe(&menu, cx, {
// Use defer to build the menu in the next frame, avoiding race conditions
window.defer(cx, {
let shared_state = shared_state.clone();
move |_, _: &DismissEvent, window, _| {
shared_state.borrow_mut().open = false;
window.refresh();
let builder = builder.clone();
move |window, cx| {
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
let Some(build) = &builder else {
return menu;
};
build(menu, window, cx)
});
// Set up the subscription for dismiss handling
let _subscription = window.subscribe(&menu, cx, {
let shared_state = shared_state.clone();
move |_, _: &DismissEvent, window, _cx| {
shared_state.borrow_mut().open = false;
window.refresh();
}
});
// Update the shared state with the built menu and subscription
{
let mut state = shared_state.borrow_mut();
state.menu_view = Some(menu.clone());
state._subscription = Some(_subscription);
window.refresh();
}
}
});
shared_state.borrow_mut().menu_view = Some(menu.clone());
shared_state.borrow_mut()._subscription = Some(_subscription);
window.refresh();
}
});
},

View File

@@ -0,0 +1,142 @@
use std::rc::Rc;
use gpui::{
Context, Corner, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement,
RenderOnce, SharedString, StyleRefinement, Styled, Window,
};
use crate::button::Button;
use crate::menu::PopupMenu;
use crate::popover::Popover;
use crate::Selectable;
/// A dropdown menu trait for buttons and other interactive elements
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
/// Create a dropdown menu with the given items, anchored to the TopLeft corner
fn dropdown_menu(
self,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> DropdownMenuPopover<Self> {
self.dropdown_menu_with_anchor(Corner::TopLeft, f)
}
/// Create a dropdown menu with the given items, anchored to the given corner
fn dropdown_menu_with_anchor(
mut self,
anchor: impl Into<Corner>,
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> DropdownMenuPopover<Self> {
let style = self.style().clone();
let id = self.interactivity().element_id.clone();
DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
}
}
impl DropdownMenu for Button {}
#[derive(IntoElement)]
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
id: ElementId,
style: StyleRefinement,
anchor: Corner,
trigger: T,
#[allow(clippy::type_complexity)]
builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
}
impl<T> DropdownMenuPopover<T>
where
T: Selectable + IntoElement + 'static,
{
fn new(
id: ElementId,
anchor: impl Into<Corner>,
trigger: T,
builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
) -> Self {
Self {
id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
style: StyleRefinement::default(),
anchor: anchor.into(),
trigger,
builder: Rc::new(builder),
}
}
/// Set the anchor corner for the dropdown menu popover.
pub fn anchor(mut self, anchor: impl Into<Corner>) -> Self {
self.anchor = anchor.into();
self
}
/// Set the style refinement for the dropdown menu trigger.
fn trigger_style(mut self, style: StyleRefinement) -> Self {
self.style = style;
self
}
}
#[derive(Default)]
struct DropdownMenuState {
menu: Option<Entity<PopupMenu>>,
}
impl<T> RenderOnce for DropdownMenuPopover<T>
where
T: Selectable + IntoElement + 'static,
{
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
let builder = self.builder.clone();
let menu_state =
window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default());
Popover::new(SharedString::from(format!("popover:{}", self.id)))
.appearance(false)
.overlay_closable(false)
.trigger(self.trigger)
.trigger_style(self.style)
.anchor(self.anchor)
.content(move |_, window, cx| {
// Here is special logic to only create the PopupMenu once and reuse it.
// Because this `content` will called in every time render, so we need to store the menu
// in state to avoid recreating at every render.
//
// And we also need to rebuild the menu when it is dismissed, to rebuild menu items
// dynamically for support `dropdown_menu` method, so we listen for DismissEvent below.
let menu = match menu_state.read(cx).menu.clone() {
Some(menu) => menu,
None => {
let builder = builder.clone();
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
builder(menu, window, cx)
});
menu_state.update(cx, |state, _| {
state.menu = Some(menu.clone());
});
menu.focus_handle(cx).focus(window, cx);
// Listen for dismiss events from the PopupMenu to close the popover.
let popover_state = cx.entity();
window
.subscribe(&menu, cx, {
let menu_state = menu_state.clone();
move |_, _: &DismissEvent, window, cx| {
popover_state.update(cx, |state, cx| {
state.dismiss(window, cx);
});
menu_state.update(cx, |state, _| {
state.menu = None;
});
}
})
.detach();
menu.clone()
}
};
menu.clone()
})
}
}

View File

@@ -10,20 +10,22 @@ use theme::ActiveTheme;
use crate::{h_flex, Disableable, StyledExt};
#[derive(IntoElement)]
#[allow(clippy::type_complexity)]
pub(crate) struct MenuItemElement {
id: ElementId,
group_name: SharedString,
style: StyleRefinement,
disabled: bool,
selected: bool,
#[allow(clippy::type_complexity)]
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
#[allow(clippy::type_complexity)]
on_hover: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
children: SmallVec<[AnyElement; 2]>,
}
impl MenuItemElement {
pub fn new(id: impl Into<ElementId>, group_name: impl Into<SharedString>) -> Self {
/// Create a new MenuItem with the given ID and group name.
pub(crate) fn new(id: impl Into<ElementId>, group_name: impl Into<SharedString>) -> Self {
let id: ElementId = id.into();
Self {
id: id.clone(),
@@ -38,17 +40,19 @@ impl MenuItemElement {
}
/// Set ListItem as the selected item style.
pub fn selected(mut self, selected: bool) -> Self {
pub(crate) fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
/// Set the disabled state of the MenuItem.
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(
/// Set a handler for when the MenuItem is clicked.
pub(crate) fn on_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
@@ -88,7 +92,7 @@ impl RenderOnce for MenuItemElement {
h_flex()
.id(self.id)
.group(&self.group_name)
.gap_x_2()
.gap_x_1()
.py_1()
.px_2()
.text_base()
@@ -102,12 +106,12 @@ impl RenderOnce for MenuItemElement {
})
.when(!self.disabled, |this| {
this.group_hover(self.group_name, |this| {
this.bg(cx.theme().elevated_surface_background)
.text_color(cx.theme().text)
this.bg(cx.theme().secondary_background)
.text_color(cx.theme().secondary_foreground)
})
.when(self.selected, |this| {
this.bg(cx.theme().elevated_surface_background)
.text_color(cx.theme().text)
this.bg(cx.theme().secondary_background)
.text_color(cx.theme().secondary_foreground)
})
.when_some(self.on_click, |this, on_click| {
this.on_mouse_down(MouseButton::Left, move |_, _, cx| {

View File

@@ -1,12 +1,15 @@
use gpui::App;
mod app_menu_bar;
mod context_menu;
mod dropdown_menu;
mod menu_item;
pub mod context_menu;
pub mod popup_menu;
mod popup_menu;
pub use app_menu_bar::AppMenuBar;
pub use context_menu::{ContextMenu, ContextMenuExt, ContextMenuState};
pub use dropdown_menu::DropdownMenu;
pub use popup_menu::{PopupMenu, PopupMenuItem};
pub(crate) fn init(cx: &mut App) {
app_menu_bar::init(cx);

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ use std::time::Duration;
use gpui::prelude::FluentBuilder;
use gpui::{
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Axis, Bounds,
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds,
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
Window,
@@ -13,7 +13,8 @@ use theme::ActiveTheme;
use crate::actions::{Cancel, Confirm};
use crate::animation::cubic_bezier;
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
use crate::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
use crate::scroll::ScrollableElement;
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
const CONTEXT: &str = "Modal";
@@ -97,9 +98,9 @@ pub struct Modal {
button_props: ModalButtonProps,
/// This will be change when open the modal, the focus handle is create when open the modal.
pub(crate) focus_handle: FocusHandle,
pub(crate) layer_ix: usize,
pub(crate) overlay_visible: bool,
pub focus_handle: FocusHandle,
pub layer_ix: usize,
pub overlay_visible: bool,
}
impl Modal {
@@ -255,7 +256,7 @@ impl Modal {
self
}
pub(crate) fn has_overlay(&self) -> bool {
pub fn has_overlay(&self) -> bool {
self.overlay
}
}
@@ -341,7 +342,7 @@ impl RenderOnce for Modal {
}
});
let window_paddings = crate::window_border::window_paddings(window, cx);
let window_paddings = crate::root::window_paddings(window, cx);
let radius = (cx.theme().radius_lg * 2.).min(px(20.));
let view_size = window.viewport_size()
@@ -489,13 +490,13 @@ impl RenderOnce for Modal {
.w_full()
.h_auto()
.flex_1()
.relative()
.overflow_hidden()
.child(
v_flex()
.pr(padding_right)
.pl(padding_left)
.scrollable(Axis::Vertical)
.size_full()
.overflow_y_scrollbar()
.child(self.content),
),
)

View File

@@ -425,7 +425,7 @@ impl NotificationList {
cx.notify();
}
pub(crate) fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
pub fn close<T>(&mut self, key: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<ElementId>,
{

View File

@@ -1,129 +1,78 @@
use std::cell::RefCell;
use std::rc::Rc;
use gpui::prelude::FluentBuilder as _;
use gpui::{
actions, anchored, deferred, div, px, AnyElement, App, Bounds, Context, Corner, DismissEvent,
DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding,
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
Styled, Subscription, Window,
};
use crate::{Selectable, StyledExt as _};
use crate::actions::Cancel;
use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _};
const CONTEXT: &str = "Popover";
actions!(popover, [Escape]);
pub fn init(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))])
pub(crate) fn init(cx: &mut App) {
cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
}
type PopoverChild<T> = Rc<dyn Fn(&mut Window, &mut Context<T>) -> AnyElement>;
pub struct PopoverContent {
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
max_width: Option<Pixels>,
max_height: Option<Pixels>,
scrollable: bool,
child: PopoverChild<Self>,
}
impl PopoverContent {
pub fn new<B>(_window: &mut Window, cx: &mut App, content: B) -> Self
where
B: Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
{
let focus_handle = cx.focus_handle();
let scroll_handle = ScrollHandle::default();
Self {
focus_handle,
scroll_handle,
child: Rc::new(content),
max_width: None,
max_height: None,
scrollable: false,
}
}
pub fn max_w(mut self, max_width: Pixels) -> Self {
self.max_width = Some(max_width);
self
}
pub fn max_h(mut self, max_height: Pixels) -> Self {
self.max_height = Some(max_height);
self
}
pub fn scrollable(mut self) -> Self {
self.scrollable = true;
self
}
}
impl EventEmitter<DismissEvent> for PopoverContent {}
impl Focusable for PopoverContent {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for PopoverContent {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.id("popup-content")
.track_focus(&self.focus_handle)
.key_context(CONTEXT)
.on_action(cx.listener(|_, _: &Escape, _, cx| cx.emit(DismissEvent)))
.p_2()
.when(self.scrollable, |this| {
this.overflow_y_scroll().track_scroll(&self.scroll_handle)
})
.when_some(self.max_width, |this, v| this.max_w(v))
.when_some(self.max_height, |this, v| this.max_h(v))
.child(self.child.clone()(window, cx))
}
}
type Trigger = Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>;
type Content<M> = Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>;
pub struct Popover<M: ManagedView> {
/// A popover element that can be triggered by a button or any other element.
#[derive(IntoElement)]
pub struct Popover {
id: ElementId,
anchor: Corner,
trigger: Trigger,
content: Content<M>,
style: StyleRefinement,
anchor: Anchor,
default_open: bool,
open: Option<bool>,
tracked_focus_handle: Option<FocusHandle>,
#[allow(clippy::type_complexity)]
trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
#[allow(clippy::type_complexity)]
content: Option<
Rc<
dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
+ 'static,
>,
>,
children: Vec<AnyElement>,
/// Style for trigger element.
/// This is used for hotfix the trigger element style to support w_full.
trigger_style: Option<StyleRefinement>,
mouse_button: MouseButton,
no_style: bool,
appearance: bool,
overlay_closable: bool,
#[allow(clippy::type_complexity)]
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
}
impl<M> Popover<M>
where
M: ManagedView,
{
impl Popover {
/// Create a new Popover with `view` mode.
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
anchor: Corner::TopLeft,
style: StyleRefinement::default(),
anchor: Anchor::TopLeft,
trigger: None,
trigger_style: None,
content: None,
tracked_focus_handle: None,
children: vec![],
mouse_button: MouseButton::Left,
no_style: false,
appearance: true,
overlay_closable: true,
default_open: false,
open: None,
on_open_change: None,
}
}
pub fn anchor(mut self, anchor: Corner) -> Self {
self.anchor = anchor;
/// Set the anchor corner of the popover, default is `Corner::TopLeft`.
///
/// This method is kept for backward compatibility with `Corner` type.
/// Internally, it converts `Corner` to `Anchor`.
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
self.anchor = anchor.into();
self
}
@@ -133,29 +82,75 @@ where
self
}
/// Set the trigger element of the popover.
pub fn trigger<T>(mut self, trigger: T) -> Self
where
T: Selectable + IntoElement + 'static,
{
self.trigger = Some(Box::new(|is_open, _, _| {
trigger.selected(is_open).into_any_element()
let selected = trigger.is_selected();
trigger.selected(selected || is_open).into_any_element()
}));
self
}
/// Set the default open state of the popover, default is `false`.
///
/// This is only used to initialize the open state of the popover.
///
/// And please note that if you use the `open` method, this value will be ignored.
pub fn default_open(mut self, open: bool) -> Self {
self.default_open = open;
self
}
/// Force set the open state of the popover.
///
/// If this is set, the popover will be controlled by this value.
///
/// NOTE: You must be used in conjunction with `on_open_change` to handle state changes.
pub fn open(mut self, open: bool) -> Self {
self.open = Some(open);
self
}
/// Add a callback to be called when the open state changes.
///
/// The first `&bool` parameter is the **new open state**.
///
/// This is useful when using the `open` method to control the popover state.
pub fn on_open_change<F>(mut self, callback: F) -> Self
where
F: Fn(&bool, &mut Window, &mut App) + 'static,
{
self.on_open_change = Some(Rc::new(callback));
self
}
/// Set the style for the trigger element.
pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
self.trigger_style = Some(style);
self
}
/// Set the content of the popover.
/// Set whether clicking outside the popover will dismiss it, default is `true`.
pub fn overlay_closable(mut self, closable: bool) -> Self {
self.overlay_closable = closable;
self
}
/// Set the content builder for content of the Popover.
///
/// The `content` is a closure that returns an `AnyElement`.
pub fn content<C>(mut self, content: C) -> Self
/// This callback will called every time on render the popover.
/// So, you should avoid creating new elements or entities in the content closure.
pub fn content<F, E>(mut self, content: F) -> Self
where
C: Fn(&mut Window, &mut App) -> Entity<M> + 'static,
E: IntoElement,
F: Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> E + 'static,
{
self.content = Some(Rc::new(content));
self.content = Some(Rc::new(move |state, window, cx| {
content(state, window, cx).into_any_element()
}));
self
}
@@ -165,302 +160,265 @@ where
///
/// - The popover will not have a bg, border, shadow, or padding.
/// - The click out of the popover will not dismiss it.
pub fn no_style(mut self) -> Self {
self.no_style = true;
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
fn render_trigger(&mut self, is_open: bool, window: &mut Window, cx: &mut App) -> AnyElement {
let Some(trigger) = self.trigger.take() else {
return div().into_any_element();
/// Bind the focus handle to receive focus when the popover is opened.
/// If you not set this, a new focus handle will be created for the popover to
///
/// If popover is opened, the focus will be moved to the focus handle.
pub fn track_focus(mut self, handle: &FocusHandle) -> Self {
self.tracked_focus_handle = Some(handle.clone());
self
}
fn resolved_corner(anchor: Anchor, trigger_bounds: Bounds<Pixels>) -> Point<Pixels> {
let offset = if anchor.is_center() {
gpui::point(trigger_bounds.size.width.half(), px(0.))
} else {
Point::default()
};
(trigger)(is_open, window, cx)
}
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
bounds.corner(match self.anchor {
Corner::TopLeft => Corner::BottomLeft,
Corner::TopRight => Corner::BottomRight,
Corner::BottomLeft => Corner::TopLeft,
Corner::BottomRight => Corner::TopRight,
})
}
fn with_element_state<R>(
&mut self,
id: &GlobalElementId,
window: &mut Window,
cx: &mut App,
f: impl FnOnce(&mut Self, &mut PopoverElementState<M>, &mut Window, &mut App) -> R,
) -> R {
window.with_optional_element_state::<PopoverElementState<M>, _>(
Some(id),
|element_state, window| {
let mut element_state = element_state.unwrap().unwrap_or_default();
let result = f(self, &mut element_state, window, cx);
(result, Some(element_state))
},
)
trigger_bounds.corner(anchor.swap_vertical().into())
+ offset
+ Point {
x: px(0.),
y: -trigger_bounds.size.height,
}
}
}
impl<M> IntoElement for Popover<M>
where
M: ManagedView,
{
type Element = Self;
fn into_element(self) -> Self::Element {
self
impl ParentElement for Popover {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
pub struct PopoverElementState<M> {
trigger_layout_id: Option<LayoutId>,
popover_layout_id: Option<LayoutId>,
popover_element: Option<AnyElement>,
trigger_element: Option<AnyElement>,
content_view: Rc<RefCell<Option<Entity<M>>>>,
/// Trigger bounds for positioning the popover.
trigger_bounds: Option<Bounds<Pixels>>,
impl Styled for Popover {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl<M> Default for PopoverElementState<M> {
fn default() -> Self {
pub struct PopoverState {
focus_handle: FocusHandle,
pub(crate) tracked_focus_handle: Option<FocusHandle>,
trigger_bounds: Bounds<Pixels>,
open: bool,
#[allow(clippy::type_complexity)]
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
_dismiss_subscription: Option<Subscription>,
}
impl PopoverState {
pub fn new(default_open: bool, cx: &mut App) -> Self {
Self {
trigger_layout_id: None,
popover_layout_id: None,
popover_element: None,
trigger_element: None,
content_view: Rc::new(RefCell::new(None)),
trigger_bounds: None,
}
}
}
pub struct PrepaintState {
hitbox: Hitbox,
/// Trigger bounds for limit a rect to handle mouse click.
trigger_bounds: Option<Bounds<Pixels>>,
}
impl<M: ManagedView> Element for Popover<M> {
type PrepaintState = PrepaintState;
type RequestLayoutState = PopoverElementState<M>;
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
id: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
let mut style = Style::default();
// FIXME: Remove this and find a better way to handle this.
// Apply trigger style, for support w_full for trigger.
//
// If remove this, the trigger will not support w_full.
if let Some(trigger_style) = self.trigger_style.clone() {
if let Some(width) = trigger_style.size.width {
style.size.width = width;
}
if let Some(display) = trigger_style.display {
style.display = display;
}
}
self.with_element_state(
id.unwrap(),
window,
cx,
|view, element_state, window, cx| {
let mut popover_layout_id = None;
let mut popover_element = None;
let mut is_open = false;
if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() {
is_open = true;
let mut anchored = anchored()
.snap_to_window_with_margin(px(8.))
.anchor(view.anchor);
if let Some(trigger_bounds) = element_state.trigger_bounds {
anchored = anchored.position(view.resolved_corner(trigger_bounds));
}
let mut element = {
let content_view_mut = element_state.content_view.clone();
let anchor = view.anchor;
let no_style = view.no_style;
deferred(
anchored.child(
div()
.size_full()
.occlude()
.when(!no_style, |this| this.popover_style(cx))
.map(|this| match anchor {
Corner::TopLeft | Corner::TopRight => this.top_1p5(),
Corner::BottomLeft | Corner::BottomRight => {
this.bottom_1p5()
}
})
.child(content_view.clone())
.when(!no_style, |this| {
this.on_mouse_down_out(move |_, window, _| {
// Update the element_state.content_view to `None`,
// so that the `paint`` method will not paint it.
*content_view_mut.borrow_mut() = None;
window.refresh();
})
}),
),
)
.with_priority(1)
.into_any()
};
popover_layout_id = Some(element.request_layout(window, cx));
popover_element = Some(element);
}
let mut trigger_element = view.render_trigger(is_open, window, cx);
let trigger_layout_id = trigger_element.request_layout(window, cx);
let layout_id = window.request_layout(
style,
Some(trigger_layout_id).into_iter().chain(popover_layout_id),
cx,
);
(
layout_id,
PopoverElementState {
trigger_layout_id: Some(trigger_layout_id),
popover_layout_id,
popover_element,
trigger_element: Some(trigger_element),
..Default::default()
},
)
},
)
}
fn prepaint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_bounds: gpui::Bounds<gpui::Pixels>,
request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
if let Some(element) = &mut request_layout.trigger_element {
element.prepaint(window, cx);
}
if let Some(element) = &mut request_layout.popover_element {
element.prepaint(window, cx);
}
let trigger_bounds = request_layout
.trigger_layout_id
.map(|id| window.layout_bounds(id));
// Prepare the popover, for get the bounds of it for open window size.
let _ = request_layout
.popover_layout_id
.map(|id| window.layout_bounds(id));
let hitbox =
window.insert_hitbox(trigger_bounds.unwrap_or_default(), HitboxBehavior::Normal);
PrepaintState {
trigger_bounds,
hitbox,
focus_handle: cx.focus_handle(),
tracked_focus_handle: None,
trigger_bounds: Bounds::default(),
open: default_open,
on_open_change: None,
_dismiss_subscription: None,
}
}
fn paint(
&mut self,
id: Option<&GlobalElementId>,
_: Option<&gpui::InspectorElementId>,
_bounds: Bounds<Pixels>,
request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
self.with_element_state(
id.unwrap(),
window,
cx,
|this, element_state, window, cx| {
element_state.trigger_bounds = prepaint.trigger_bounds;
/// Check if the popover is open.
pub fn is_open(&self) -> bool {
self.open
}
if let Some(mut element) = request_layout.trigger_element.take() {
element.paint(window, cx);
}
/// Dismiss the popover if it is open.
pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.open {
self.toggle_open(window, cx);
}
}
if let Some(mut element) = request_layout.popover_element.take() {
element.paint(window, cx);
return;
}
/// Open the popover if it is closed.
pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.open {
self.toggle_open(window, cx);
}
}
// When mouse click down in the trigger bounds, open the popover.
let Some(content_build) = this.content.take() else {
return;
};
let old_content_view = element_state.content_view.clone();
let hitbox_id = prepaint.hitbox.id;
let mouse_button = this.mouse_button;
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble
&& event.button == mouse_button
&& hitbox_id.is_hovered(window)
{
cx.stop_propagation();
window.prevent_default();
fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.open = !self.open;
if self.open {
let state = cx.entity();
let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
{
tracked_focus_handle
} else {
self.focus_handle.clone()
};
focus_handle.focus(window, cx);
let new_content_view = (content_build)(window, cx);
let old_content_view1 = old_content_view.clone();
let previous_focus_handle = window.focused(cx);
window
.subscribe(
&new_content_view,
cx,
move |modal, _: &DismissEvent, window, cx| {
if modal.focus_handle(cx).contains_focused(window, cx) {
if let Some(previous_focus_handle) =
previous_focus_handle.as_ref()
{
window.focus(previous_focus_handle, cx);
}
}
*old_content_view1.borrow_mut() = None;
window.refresh();
},
)
.detach();
window.focus(&new_content_view.focus_handle(cx), cx);
*old_content_view.borrow_mut() = Some(new_content_view);
self._dismiss_subscription =
Some(
window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
state.update(cx, |state, cx| {
state.dismiss(window, cx);
});
window.refresh();
}
});
},
);
}),
);
} else {
self._dismiss_subscription = None;
}
if let Some(callback) = self.on_open_change.as_ref() {
callback(&self.open, window, cx);
}
cx.notify();
}
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
self.dismiss(window, cx);
}
}
impl Focusable for PopoverState {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for PopoverState {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div()
}
}
impl EventEmitter<DismissEvent> for PopoverState {}
impl Popover {
pub(crate) fn render_popover<E>(
anchor: Anchor,
trigger_bounds: Bounds<Pixels>,
content: E,
_: &mut Window,
_: &mut App,
) -> Deferred
where
E: IntoElement + 'static,
{
deferred(
anchored()
.snap_to_window_with_margin(px(8.))
.anchor(anchor)
.position(Self::resolved_corner(anchor, trigger_bounds))
.child(div().relative().child(content)),
)
.with_priority(1)
}
pub(crate) fn render_popover_content(
anchor: Anchor,
appearance: bool,
_: &mut Window,
cx: &mut App,
) -> Stateful<Div> {
v_flex()
.id("content")
.occlude()
.tab_group()
.when(appearance, |this| this.popover_style(cx).p_3())
.map(|this| match anchor {
Anchor::TopLeft | Anchor::TopCenter | Anchor::TopRight => this.top_1(),
Anchor::BottomLeft | Anchor::BottomCenter | Anchor::BottomRight => this.bottom_1(),
})
}
}
impl RenderOnce for Popover {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let force_open = self.open;
let default_open = self.default_open;
let tracked_focus_handle = self.tracked_focus_handle.clone();
let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| {
PopoverState::new(default_open, cx)
});
state.update(cx, |state, _| {
if let Some(tracked_focus_handle) = tracked_focus_handle {
state.tracked_focus_handle = Some(tracked_focus_handle);
}
state.on_open_change = self.on_open_change.clone();
if let Some(force_open) = force_open {
state.open = force_open;
}
});
let open = state.read(cx).open;
let focus_handle = state.read(cx).focus_handle.clone();
let trigger_bounds = state.read(cx).trigger_bounds;
let Some(trigger) = self.trigger else {
return div().id("empty");
};
let parent_view_id = window.current_view();
let el = div()
.id(self.id)
.child((trigger)(open, window, cx))
.on_mouse_down(self.mouse_button, {
let state = state.clone();
move |_, window, cx| {
cx.stop_propagation();
state.update(cx, |state, cx| {
// We force set open to false to toggle it correctly.
// Because if the mouse down out will toggle open first.
state.open = open;
state.toggle_open(window, cx);
});
cx.notify(parent_view_id);
}
})
.on_prepaint({
let state = state.clone();
move |bounds, _, cx| {
state.update(cx, |state, _| {
state.trigger_bounds = bounds;
})
}
});
if !open {
return el;
}
let popover_content =
Self::render_popover_content(self.anchor, self.appearance, window, cx)
.track_focus(&focus_handle)
.key_context(CONTEXT)
.on_action(window.listener_for(&state, PopoverState::on_action_cancel))
.when_some(self.content, |this, content| {
this.child(state.update(cx, |state, cx| (content)(state, window, cx)))
})
.children(self.children)
.when(self.overlay_closable, |this| {
this.on_mouse_down_out({
let state = state.clone();
move |_, window, cx| {
state.update(cx, |state, cx| {
state.dismiss(window, cx);
});
cx.notify(parent_view_id);
}
})
})
.refine_style(&self.style);
el.child(Self::render_popover(
self.anchor,
trigger_bounds,
popover_content,
window,
cx,
))
}
}

View File

@@ -2,168 +2,63 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
div, AnyView, App, AppContext, Context, Decorations, Entity, FocusHandle, InteractiveElement,
IntoElement, ParentElement as _, Render, SharedString, Styled, Window,
canvas, div, point, px, size, AnyView, App, AppContext, Bounds, Context, CursorStyle,
Decorations, Edges, Entity, FocusHandle, HitboxBehavior, Hsla, InteractiveElement, IntoElement,
MouseButton, ParentElement as _, Pixels, Point, Render, ResizeEdge, SharedString, Size, Styled,
Tiling, WeakFocusHandle, Window,
};
use theme::{
ActiveTheme, CLIENT_SIDE_DECORATION_BORDER, CLIENT_SIDE_DECORATION_ROUNDING,
CLIENT_SIDE_DECORATION_SHADOW,
};
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING};
use crate::input::InputState;
use crate::modal::Modal;
use crate::notification::{Notification, NotificationList};
use crate::window_border;
/// Extension trait for [`WindowContext`] and [`ViewContext`] to add drawer functionality.
pub trait ContextModal: Sized {
/// Opens a Modal.
fn open_modal<F>(&mut self, cx: &mut App, build: F)
where
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static;
/// Return true, if there is an active Modal.
fn has_active_modal(&mut self, cx: &mut App) -> bool;
/// Closes the last active Modal.
fn close_modal(&mut self, cx: &mut App);
/// Closes all active Modals.
fn close_all_modals(&mut self, cx: &mut App);
/// Returns number of notifications.
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
/// Pushes a notification to the notification list.
fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App);
/// Clears a notification by its ID.
fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App);
/// Clear all notifications
fn clear_notifications(&mut self, cx: &mut App);
/// Return current focused Input entity.
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>>;
/// Returns true if there is a focused Input entity.
fn has_focused_input(&mut self, cx: &mut App) -> bool;
}
impl ContextModal for Window {
fn open_modal<F>(&mut self, cx: &mut App, build: F)
where
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
{
Root::update(self, cx, move |root, window, cx| {
// Only save focus handle if there are no active modals.
// This is used to restore focus when all modals are closed.
if root.active_modals.is_empty() {
root.previous_focus_handle = window.focused(cx);
}
let focus_handle = cx.focus_handle();
focus_handle.focus(window, cx);
root.active_modals.push(ActiveModal {
focus_handle,
builder: Rc::new(build),
});
cx.notify();
})
}
fn has_active_modal(&mut self, cx: &mut App) -> bool {
!Root::read(self, cx).active_modals.is_empty()
}
fn close_modal(&mut self, cx: &mut App) {
Root::update(self, cx, move |root, window, cx| {
root.active_modals.pop();
if let Some(top_modal) = root.active_modals.last() {
// Focus the next modal.
top_modal.focus_handle.focus(window, cx);
} else {
// Restore focus if there are no more modals.
root.focus_back(window, cx);
}
cx.notify();
})
}
fn close_all_modals(&mut self, cx: &mut App) {
Root::update(self, cx, |root, window, cx| {
root.active_modals.clear();
root.focus_back(window, cx);
cx.notify();
})
}
fn push_notification(&mut self, note: impl Into<Notification>, cx: &mut App) {
let note = note.into();
Root::update(self, cx, move |root, window, cx| {
root.notification
.update(cx, |view, cx| view.push(note, window, cx));
cx.notify();
})
}
fn clear_notifications(&mut self, cx: &mut App) {
Root::update(self, cx, move |root, window, cx| {
root.notification
.update(cx, |view, cx| view.clear(window, cx));
cx.notify();
})
}
fn clear_notification_by_id(&mut self, id: SharedString, cx: &mut App) {
Root::update(self, cx, move |root, window, cx| {
root.notification.update(cx, |view, cx| {
view.close(id.clone(), window, cx);
});
cx.notify();
})
}
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
let entity = Root::read(self, cx).notification.clone();
Rc::new(entity.read(cx).notifications())
}
fn has_focused_input(&mut self, cx: &mut App) -> bool {
Root::read(self, cx).focused_input.is_some()
}
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>> {
Root::read(self, cx).focused_input.clone()
}
}
type Builder = Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>;
#[derive(Clone)]
pub(crate) struct ActiveModal {
#[allow(clippy::type_complexity)]
pub struct ActiveModal {
focus_handle: FocusHandle,
builder: Builder,
/// The previous focused handle before opening the modal.
previous_focused_handle: Option<WeakFocusHandle>,
builder: Rc<dyn Fn(Modal, &mut Window, &mut App) -> Modal + 'static>,
}
impl ActiveModal {
fn new(
focus_handle: FocusHandle,
previous_focused_handle: Option<WeakFocusHandle>,
builder: impl Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
) -> Self {
Self {
focus_handle,
previous_focused_handle,
builder: Rc::new(builder),
}
}
}
/// Root is a view for the App window for as the top level view (Must be the first view in the window).
///
/// It is used to manage the Modal, and Notification.
pub struct Root {
/// All active models
pub(crate) active_modals: Vec<ActiveModal>,
pub notification: Entity<NotificationList>,
pub focused_input: Option<Entity<InputState>>,
/// Used to store the focus handle of the previous view.
///
/// When the Modal closes, we will focus back to the previous view.
previous_focus_handle: Option<FocusHandle>,
/// Notification layer
pub(crate) notification: Entity<NotificationList>,
/// Current focused input
pub(crate) focused_input: Option<Entity<InputState>>,
/// App view
view: AnyView,
}
impl Root {
pub fn new(view: AnyView, window: &mut Window, cx: &mut Context<Self>) -> Self {
Self {
previous_focus_handle: None,
focused_input: None,
active_modals: Vec::new(),
notification: cx.new(|cx| NotificationList::new(window, cx)),
@@ -188,13 +83,11 @@ impl Root {
.read(cx)
}
fn focus_back(&mut self, window: &mut Window, cx: &mut App) {
if let Some(handle) = self.previous_focus_handle.clone() {
window.focus(&handle, cx);
}
pub fn view(&self) -> &AnyView {
&self.view
}
/// Render Notification layer.
/// Render the notification layer.
pub fn render_notification_layer(
window: &mut Window,
cx: &mut App,
@@ -210,10 +103,9 @@ impl Root {
)
}
/// Render the Modal layer.
/// Render the modal layer.
pub fn render_modal_layer(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
let root = window.root::<Root>()??;
let active_modals = root.read(cx).active_modals.clone();
if active_modals.is_empty() {
@@ -255,50 +147,316 @@ impl Root {
Some(div().children(modals))
}
/// Return the root view of the Root.
pub fn view(&self) -> &AnyView {
&self.view
/// Open a modal.
pub fn open_modal<F>(&mut self, builder: F, window: &mut Window, cx: &mut Context<'_, Self>)
where
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
{
let previous_focused_handle = window.focused(cx).map(|h| h.downgrade());
let focus_handle = cx.focus_handle();
focus_handle.focus(window, cx);
self.active_modals.push(ActiveModal::new(
focus_handle,
previous_focused_handle,
builder,
));
cx.notify();
}
/// Replace the root view of the Root.
pub fn replace_view(&mut self, view: AnyView) {
self.view = view;
/// Close the topmost modal.
pub fn close_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.focused_input = None;
if let Some(handle) = self
.active_modals
.pop()
.and_then(|d| d.previous_focused_handle)
.and_then(|h| h.upgrade())
{
window.focus(&handle, cx);
}
cx.notify();
}
/// Close all modals.
pub fn close_all_modals(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.focused_input = None;
self.active_modals.clear();
let previous_focused_handle = self
.active_modals
.first()
.and_then(|d| d.previous_focused_handle.clone());
if let Some(handle) = previous_focused_handle.and_then(|h| h.upgrade()) {
window.focus(&handle, cx);
}
cx.notify();
}
/// Check if there are any active modals.
pub fn has_active_modals(&self) -> bool {
!self.active_modals.is_empty()
}
/// Push a notification to the notification layer.
pub fn push_notification<T>(&mut self, note: T, window: &mut Window, cx: &mut Context<'_, Root>)
where
T: Into<Notification>,
{
self.notification
.update(cx, |view, cx| view.push(note, window, cx));
cx.notify();
}
/// Clear a notification by its ID.
pub fn clear_notification<T>(&mut self, id: T, window: &mut Window, cx: &mut Context<Self>)
where
T: Into<SharedString>,
{
self.notification
.update(cx, |view, cx| view.close(id.into(), window, cx));
cx.notify();
}
/// Clear all notifications from the notification layer.
pub fn clear_notifications(&mut self, window: &mut Window, cx: &mut Context<'_, Root>) {
self.notification
.update(cx, |view, cx| view.clear(window, cx));
cx.notify();
}
}
impl Render for Root {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let base_font_size = cx.theme().font_size;
let rem_size = cx.theme().font_size;
let font_family = cx.theme().font_family.clone();
let decorations = window.window_decorations();
window.set_rem_size(base_font_size);
// Set the base font size
window.set_rem_size(rem_size);
window_border().child(
div()
.id("root")
.map(|this| match decorations {
Decorations::Server => this,
Decorations::Client { tiling, .. } => this
.when(!(tiling.top || tiling.right), |el| {
el.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |el| {
el.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |el| {
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |el| {
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
}),
})
.relative()
.size_full()
.font_family(font_family)
.bg(cx.theme().background)
.text_color(cx.theme().text)
.child(self.view.clone()),
)
// Set the client inset (linux only)
match decorations {
Decorations::Client { .. } => window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW),
Decorations::Server => window.set_client_inset(px(0.0)),
}
div()
.id("window")
.size_full()
.bg(gpui::transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.bg(gpui::transparent_black())
.child(
canvas(
|_bounds, window, _cx| {
window.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
window.window_bounds().get_bounds().size,
),
HitboxBehavior::Normal,
)
},
move |_bounds, hitbox, window, _cx| {
let mouse = window.mouse_position();
let size = window.window_bounds().get_bounds().size;
let Some(edge) =
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
else {
return;
};
window.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW))
.when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW))
.when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW))
.when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW))
.on_mouse_down(MouseButton::Left, move |e, window, _cx| {
let size = window.window_bounds().get_bounds().size;
let pos = e.position;
if let Some(edge) =
resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size, tiling)
{
window.start_window_resize(edge)
};
}),
})
.child(
div()
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.border_color(cx.theme().border)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| {
div.border_t(CLIENT_SIDE_DECORATION_BORDER)
})
.when(!tiling.bottom, |div| {
div.border_b(CLIENT_SIDE_DECORATION_BORDER)
})
.when(!tiling.left, |div| {
div.border_l(CLIENT_SIDE_DECORATION_BORDER)
})
.when(!tiling.right, |div| {
div.border_r(CLIENT_SIDE_DECORATION_BORDER)
})
.when(!tiling.is_tiled(), |div| {
div.shadow(vec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.4,
},
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, _, cx| {
cx.stop_propagation();
})
.size_full()
.font_family(font_family)
.bg(cx.theme().background)
.text_color(cx.theme().text)
.child(self.view.clone()),
)
}
}
/// Get the window paddings.
pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
match window.window_decorations() {
Decorations::Server => Edges::all(px(0.0)),
Decorations::Client { tiling } => {
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
if tiling.top {
paddings.top = px(0.0);
}
if tiling.bottom {
paddings.bottom = px(0.0);
}
if tiling.left {
paddings.left = px(0.0);
}
if tiling.right {
paddings.right = px(0.0);
}
paddings
}
}
}
/// Get the window resize edge.
fn resize_edge(
pos: Point<Pixels>,
shadow_size: Pixels,
window_size: Size<Pixels>,
tiling: Tiling,
) -> Option<ResizeEdge> {
let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5);
if bounds.contains(&pos) {
return None;
}
let corner_size = size(shadow_size * 1.5, shadow_size * 1.5);
let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size);
if !tiling.top && top_left_bounds.contains(&pos) {
return Some(ResizeEdge::TopLeft);
}
let top_right_bounds = Bounds::new(
Point::new(window_size.width - corner_size.width, px(0.)),
corner_size,
);
if !tiling.top && top_right_bounds.contains(&pos) {
return Some(ResizeEdge::TopRight);
}
let bottom_left_bounds = Bounds::new(
Point::new(px(0.), window_size.height - corner_size.height),
corner_size,
);
if !tiling.bottom && bottom_left_bounds.contains(&pos) {
return Some(ResizeEdge::BottomLeft);
}
let bottom_right_bounds = Bounds::new(
Point::new(
window_size.width - corner_size.width,
window_size.height - corner_size.height,
),
corner_size,
);
if !tiling.bottom && bottom_right_bounds.contains(&pos) {
return Some(ResizeEdge::BottomRight);
}
if !tiling.top && pos.y < shadow_size {
Some(ResizeEdge::Top)
} else if !tiling.bottom && pos.y > window_size.height - shadow_size {
Some(ResizeEdge::Bottom)
} else if !tiling.left && pos.x < shadow_size {
Some(ResizeEdge::Left)
} else if !tiling.right && pos.x > window_size.width - shadow_size {
Some(ResizeEdge::Right)
} else {
None
}
}

View File

@@ -1,232 +1,209 @@
use std::panic::Location;
use std::rc::Rc;
use gpui::prelude::FluentBuilder;
use gpui::{
div, relative, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId,
InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement,
Pixels, Position, ScrollHandle, SharedString, Size, Stateful, StatefulInteractiveElement,
Style, StyleRefinement, Styled, Window,
div, App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window,
};
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
use super::{Scrollbar, ScrollbarAxis};
use crate::scroll::ScrollbarHandle;
use crate::StyledExt;
/// A scroll view is a container that allows the user to scroll through a large amount of content.
pub struct Scrollable<E> {
/// A trait for elements that can be made scrollable with scrollbars.
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
/// Adds a scrollbar to the element.
#[track_caller]
fn scrollbar<H: ScrollbarHandle + Clone>(
self,
scroll_handle: &H,
axis: impl Into<ScrollbarAxis>,
) -> Self {
self.child(ScrollbarLayer {
id: "scrollbar_layer".into(),
axis: axis.into(),
scroll_handle: Rc::new(scroll_handle.clone()),
})
}
/// Adds a vertical scrollbar to the element.
#[track_caller]
fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
}
/// Adds a horizontal scrollbar to the element.
#[track_caller]
fn horizontal_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
self.scrollbar(scroll_handle, ScrollbarAxis::Horizontal)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_scroll`], but adds scrollbars.
#[track_caller]
fn overflow_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Both)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_x_scroll`], but adds Horizontal scrollbar.
#[track_caller]
fn overflow_x_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Horizontal)
}
/// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
#[track_caller]
fn overflow_y_scrollbar(self) -> Scrollable<Self> {
Scrollable::new(self, ScrollbarAxis::Vertical)
}
}
/// A scrollable element wrapper that adds scrollbars to an interactive element.
#[derive(IntoElement)]
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
id: ElementId,
element: Option<E>,
element: E,
axis: ScrollbarAxis,
/// This is a fake element to handle Styled, InteractiveElement, not used.
_element: Stateful<Div>,
}
impl<E> Scrollable<E>
where
E: Element,
E: InteractiveElement + Styled + ParentElement + Element,
{
pub(crate) fn new(axis: impl Into<ScrollbarAxis>, element: E) -> Self {
let id = ElementId::Name(SharedString::from(
format!("scrollable-{:?}", element.id(),),
));
#[track_caller]
fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
let caller = Location::caller();
Self {
element: Some(element),
_element: div().id("fake"),
id,
id: ElementId::CodeLocation(*caller),
element,
axis: axis.into(),
}
}
/// Set only a vertical scrollbar.
pub fn vertical(mut self) -> Self {
self.set_axis(ScrollbarAxis::Vertical);
self
}
/// Set only a horizontal scrollbar.
/// In current implementation, this is not supported yet.
pub fn horizontal(mut self) -> Self {
self.set_axis(ScrollbarAxis::Horizontal);
self
}
/// Set the axis of the scroll view.
pub fn set_axis(&mut self, axis: impl Into<ScrollbarAxis>) {
self.axis = axis.into();
}
fn with_element_state<R>(
&mut self,
id: &GlobalElementId,
window: &mut Window,
cx: &mut App,
f: impl FnOnce(&mut Self, &mut ScrollViewState, &mut Window, &mut App) -> R,
) -> R {
window.with_optional_element_state::<ScrollViewState, _>(
Some(id),
|element_state, window| {
let mut element_state = element_state.unwrap().unwrap_or_default();
let result = f(self, &mut element_state, window, cx);
(result, Some(element_state))
},
)
}
}
pub struct ScrollViewState {
state: ScrollbarState,
handle: ScrollHandle,
}
impl Default for ScrollViewState {
fn default() -> Self {
Self {
handle: ScrollHandle::new(),
state: ScrollbarState::default(),
}
}
}
impl<E> ParentElement for Scrollable<E>
where
E: Element + ParentElement,
{
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
if let Some(element) = &mut self.element {
element.extend(elements);
}
}
}
impl<E> Styled for Scrollable<E>
where
E: Element + Styled,
E: InteractiveElement + Styled + ParentElement + Element,
{
fn style(&mut self) -> &mut StyleRefinement {
if let Some(element) = &mut self.element {
element.style()
} else {
self._element.style()
}
self.element.style()
}
}
impl<E> InteractiveElement for Scrollable<E>
impl<E> ParentElement for Scrollable<E>
where
E: Element + InteractiveElement,
E: InteractiveElement + Styled + ParentElement + Element,
{
fn interactivity(&mut self) -> &mut Interactivity {
if let Some(element) = &mut self.element {
element.interactivity()
} else {
self._element.interactivity()
}
}
}
impl<E> StatefulInteractiveElement for Scrollable<E> where E: Element + StatefulInteractiveElement {}
impl<E> IntoElement for Scrollable<E>
where
E: Element,
{
type Element = Self;
fn into_element(self) -> Self::Element {
self
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
self.element.extend(elements)
}
}
impl<E> Element for Scrollable<E>
impl InteractiveElement for Scrollable<Div> {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.element.interactivity()
}
}
impl InteractiveElement for Scrollable<Stateful<Div>> {
fn interactivity(&mut self) -> &mut gpui::Interactivity {
self.element.interactivity()
}
}
impl<E> RenderOnce for Scrollable<E>
where
E: Element,
E: InteractiveElement + Styled + ParentElement + Element + 'static,
{
type PrepaintState = ScrollViewState;
type RequestLayoutState = AnyElement;
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let scroll_handle = window
.use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
.read(cx)
.clone();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let style = Style {
position: Position::Relative,
flex_grow: 1.0,
flex_shrink: 1.0,
size: Size {
width: relative(1.).into(),
height: relative(1.).into(),
},
// Inherit the size from the element style.
let style = StyleRefinement {
size: self.element.style().size.clone(),
..Default::default()
};
let axis = self.axis;
let scroll_id = self.id.clone();
let content = self.element.take().map(|c| c.into_any_element());
self.with_element_state(id.unwrap(), window, cx, |_, element_state, window, cx| {
let mut element = div()
.relative()
.size_full()
.overflow_hidden()
.child(
div()
.id(scroll_id)
.track_scroll(&element_state.handle)
.overflow_scroll()
.relative()
.size_full()
.child(div().children(content)),
)
.child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(
Scrollbar::both(&element_state.state, &element_state.handle).axis(axis),
),
)
.into_any_element();
let element_id = element.request_layout(window, cx);
let layout_id = window.request_layout(style, vec![element_id], cx);
(layout_id, element)
})
}
fn prepaint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) -> Self::PrepaintState {
element.prepaint(window, cx);
// do nothing
ScrollViewState::default()
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
) {
element.paint(window, cx)
div()
.id(self.id)
.size_full()
.refine_style(&style)
.relative()
.child(
div()
.id("scroll-area")
.flex()
.size_full()
.track_scroll(&scroll_handle)
.map(|this| match self.axis {
ScrollbarAxis::Vertical => this.flex_col().overflow_y_scroll(),
ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
ScrollbarAxis::Both => this.overflow_scroll(),
})
.child(
self.element
// Refine element size to `flex_1`.
.size_auto()
.flex_1(),
),
)
.child(render_scrollbar(
"scrollbar",
&scroll_handle,
self.axis,
window,
cx,
))
}
}
impl ScrollableElement for Div {}
impl<E> ScrollableElement for Stateful<E>
where
E: ParentElement + Styled + Element,
Self: InteractiveElement,
{
}
#[derive(IntoElement)]
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
id: ElementId,
axis: ScrollbarAxis,
scroll_handle: Rc<H>,
}
impl<H> RenderOnce for ScrollbarLayer<H>
where
H: ScrollbarHandle + Clone + 'static,
{
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
}
}
#[inline]
#[track_caller]
fn render_scrollbar<H: ScrollbarHandle + Clone>(
id: impl Into<ElementId>,
scroll_handle: &H,
axis: ScrollbarAxis,
window: &mut Window,
cx: &mut App,
) -> Div {
// Do not render scrollbar when inspector is picking elements,
// to allow us to pick the background elements.
let is_inspector_picking = window.is_inspector_picking(cx);
if is_inspector_picking {
return div();
}
div()
.absolute()
.top_0()
.left_0()
.right_0()
.bottom_0()
.child(Scrollbar::new(scroll_handle).id(id).axis(axis))
}

View File

@@ -1,43 +1,50 @@
use std::cell::Cell;
use std::ops::Deref;
use std::panic::Location;
use std::rc::Rc;
use std::time::{Duration, Instant};
use gpui::{
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
CursorStyle, Edges, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
InspectorElementId, IntoElement, IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent,
MouseUpEvent, PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style,
UniformListScrollHandle, Window,
};
use theme::ActiveTheme;
use theme::{ActiveTheme, ScrollbarMode};
use crate::AxisExt;
const WIDTH: Pixels = px(2. * 2. + 8.);
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
const WIDTH: Pixels = px(1. * 2. + 8.);
const MIN_THUMB_SIZE: f32 = 48.;
const THUMB_WIDTH: Pixels = px(6.);
const THUMB_RADIUS: Pixels = px(6. / 2.);
const THUMB_INSET: Pixels = px(2.);
const THUMB_INSET: Pixels = px(1.);
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
const THUMB_ACTIVE_INSET: Pixels = px(2.);
const THUMB_ACTIVE_INSET: Pixels = px(1.);
const FADE_OUT_DURATION: f32 = 3.0;
const FADE_OUT_DELAY: f32 = 2.0;
pub trait ScrollHandleOffsetable {
/// A trait for scroll handles that can get and set offset.
pub trait ScrollbarHandle: 'static {
/// Get the current offset of the scroll handle.
fn offset(&self) -> Point<Pixels>;
/// Set the offset of the scroll handle.
fn set_offset(&self, offset: Point<Pixels>);
fn is_uniform_list(&self) -> bool {
false
}
/// The full size of the content, including padding.
fn content_size(&self) -> Size<Pixels>;
/// Called when start dragging the scrollbar thumb.
fn start_drag(&self) {}
/// Called when end dragging the scrollbar thumb.
fn end_drag(&self) {}
}
impl ScrollHandleOffsetable for ScrollHandle {
impl ScrollbarHandle for ScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.offset()
}
@@ -51,7 +58,7 @@ impl ScrollHandleOffsetable for ScrollHandle {
}
}
impl ScrollHandleOffsetable for UniformListScrollHandle {
impl ScrollbarHandle for UniformListScrollHandle {
fn offset(&self) -> Point<Pixels> {
self.0.borrow().base_handle.offset()
}
@@ -60,21 +67,41 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
self.0.borrow_mut().base_handle.set_offset(offset)
}
fn is_uniform_list(&self) -> bool {
true
}
fn content_size(&self) -> Size<Pixels> {
let base_handle = &self.0.borrow().base_handle;
base_handle.max_offset() + base_handle.bounds().size
}
}
#[derive(Debug, Clone)]
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
impl ScrollbarHandle for ListState {
fn offset(&self) -> Point<Pixels> {
self.scroll_px_offset_for_scrollbar()
}
fn set_offset(&self, offset: Point<Pixels>) {
self.set_offset_from_scrollbar(offset);
}
fn content_size(&self) -> Size<Pixels> {
self.viewport_bounds().size + self.max_offset_for_scrollbar()
}
fn start_drag(&self) {
self.scrollbar_drag_started();
}
fn end_drag(&self) {
self.scrollbar_drag_ended();
}
}
#[doc(hidden)]
#[derive(Debug, Clone)]
struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
#[doc(hidden)]
#[derive(Debug, Clone, Copy)]
pub struct ScrollbarStateInner {
struct ScrollbarStateInner {
hovered_axis: Option<Axis>,
hovered_on_thumb: Option<Axis>,
dragged_axis: Option<Axis>,
@@ -83,6 +110,7 @@ pub struct ScrollbarStateInner {
last_scroll_time: Option<Instant>,
// Last update offset
last_update: Instant,
idle_timer_scheduled: bool,
}
impl Default for ScrollbarState {
@@ -95,6 +123,7 @@ impl Default for ScrollbarState {
last_scroll_offset: point(px(0.), px(0.)),
last_scroll_time: None,
last_update: Instant::now(),
idle_timer_scheduled: false,
})))
}
}
@@ -167,6 +196,12 @@ impl ScrollbarStateInner {
state
}
fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self {
let mut state = *self;
state.idle_timer_scheduled = scheduled;
state
}
fn is_scrollbar_visible(&self) -> bool {
// On drag
if self.dragged_axis.is_some() {
@@ -182,10 +217,14 @@ impl ScrollbarStateInner {
}
}
/// Scrollbar axis.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScrollbarAxis {
/// Vertical scrollbar.
Vertical,
/// Horizontal scrollbar.
Horizontal,
/// Show both vertical and horizontal scrollbars.
Both,
}
@@ -200,25 +239,30 @@ impl From<Axis> for ScrollbarAxis {
impl ScrollbarAxis {
/// Return true if the scrollbar axis is vertical.
#[inline]
pub fn is_vertical(&self) -> bool {
matches!(self, Self::Vertical)
}
/// Return true if the scrollbar axis is horizontal.
#[inline]
pub fn is_horizontal(&self) -> bool {
matches!(self, Self::Horizontal)
}
/// Return true if the scrollbar axis is both vertical and horizontal.
#[inline]
pub fn is_both(&self) -> bool {
matches!(self, Self::Both)
}
/// Return true if the scrollbar has vertical axis.
#[inline]
pub fn has_vertical(&self) -> bool {
matches!(self, Self::Vertical | Self::Both)
}
/// Return true if the scrollbar has horizontal axis.
#[inline]
pub fn has_horizontal(&self) -> bool {
matches!(self, Self::Horizontal | Self::Both)
@@ -238,9 +282,10 @@ impl ScrollbarAxis {
/// Scrollbar control for scroll-area or a uniform-list.
pub struct Scrollbar {
pub(crate) id: ElementId,
axis: ScrollbarAxis,
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
state: ScrollbarState,
scrollbar_mode: Option<ScrollbarMode>,
scroll_handle: Rc<dyn ScrollbarHandle>,
scroll_size: Option<Size<Pixels>>,
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
///
@@ -250,50 +295,46 @@ pub struct Scrollbar {
}
impl Scrollbar {
fn new(
axis: impl Into<ScrollbarAxis>,
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
/// Create a new scrollbar.
///
/// This will have both vertical and horizontal scrollbars.
#[track_caller]
pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
let caller = Location::caller();
Self {
state: state.clone(),
axis: axis.into(),
scroll_handle: Rc::new(Box::new(scroll_handle.clone())),
id: ElementId::CodeLocation(*caller),
axis: ScrollbarAxis::Both,
scrollbar_mode: None,
scroll_handle: Rc::new(scroll_handle.clone()),
max_fps: 120,
scroll_size: None,
}
}
/// Create with vertical and horizontal scrollbar.
pub fn both(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Both, state, scroll_handle)
}
/// Create with horizontal scrollbar.
pub fn horizontal(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Horizontal, state, scroll_handle)
#[track_caller]
pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
}
/// Create with vertical scrollbar.
pub fn vertical(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
#[track_caller]
pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
}
/// Create vertical scrollbar for uniform list.
pub fn uniform_scroll(
state: &ScrollbarState,
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
) -> Self {
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
/// Set a specific element id, default is the [`Location::caller`].
///
/// NOTE: In most cases, you don't need to set a specific id for scrollbar.
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
self.id = id.into();
self
}
/// Set the scrollbar show mode [`ScrollbarShow`], if not set use the `cx.theme().scrollbar_show`.
pub fn scrollbar_mode(mut self, mode: ScrollbarMode) -> Self {
self.scrollbar_mode = Some(mode);
self
}
/// Set a special scroll size of the content area, default is None.
@@ -315,11 +356,18 @@ impl Scrollbar {
/// If you have very high CPU usage, consider reducing this value to improve performance.
///
/// Available values: 30..120
pub fn max_fps(mut self, max_fps: usize) -> Self {
#[allow(dead_code)]
pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
self.max_fps = max_fps.clamp(30, 120);
self
}
// Get the width of the scrollbar.
#[allow(dead_code)]
pub(crate) const fn width() -> Pixels {
WIDTH
}
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
(
cx.theme().scrollbar_thumb_hover_background,
@@ -353,11 +401,28 @@ impl Scrollbar {
)
}
fn style_for_idle(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
let (width, inset, radius) = if cx.theme().scrollbar_mode.is_scrolling() {
(THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS)
} else {
(THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS)
fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
let scrollbar_mode = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
let (width, inset, radius) = match scrollbar_mode {
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
};
(
cx.theme().scrollbar_thumb_background,
cx.theme().scrollbar_track_background,
gpui::transparent_black(),
width,
inset,
radius,
)
}
fn style_for_idle(&self, _cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
let scrollbar_mode = self.scrollbar_mode.unwrap_or(ScrollbarMode::Always);
let (width, inset, radius) = match scrollbar_mode {
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
};
(
@@ -379,11 +444,14 @@ impl IntoElement for Scrollbar {
}
}
#[doc(hidden)]
pub struct PrepaintState {
hitbox: Hitbox,
scrollbar_state: ScrollbarState,
states: Vec<AxisPrepaintState>,
}
#[doc(hidden)]
pub struct AxisPrepaintState {
axis: Axis,
bar_hitbox: Hitbox,
@@ -406,7 +474,7 @@ impl Element for Scrollbar {
type RequestLayoutState = ();
fn id(&self) -> Option<gpui::ElementId> {
None
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
@@ -420,11 +488,11 @@ impl Element for Scrollbar {
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let style = gpui::Style {
let style = Style {
position: Position::Absolute,
flex_grow: 1.0,
flex_shrink: 1.0,
size: gpui::Size {
size: Size {
width: relative(1.).into(),
height: relative(1.).into(),
},
@@ -447,6 +515,11 @@ impl Element for Scrollbar {
window.insert_hitbox(bounds, HitboxBehavior::Normal)
});
let state = window
.use_state(cx, |_, _| ScrollbarState::default())
.read(cx)
.clone();
let mut states = vec![];
let mut has_both = self.axis.is_both();
let scroll_size = self
@@ -470,9 +543,8 @@ impl Element for Scrollbar {
};
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
let margin_end = if has_both && !is_vertical {
THUMB_ACTIVE_WIDTH
WIDTH
} else {
px(0.)
};
@@ -512,11 +584,12 @@ impl Element for Scrollbar {
},
};
let state = self.state.clone();
let is_always_to_show = cx.theme().scrollbar_mode.is_always();
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
let is_always_to_show = scrollbar_show.is_always();
let is_hover_to_show = scrollbar_show.is_hover();
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
let is_offset_changed = state.get().last_scroll_offset != self.scroll_handle.offset();
let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
if state.get().dragged_axis == Some(axis) {
@@ -527,38 +600,47 @@ impl Element for Scrollbar {
} else {
Self::style_for_hovered_bar(cx)
}
} else if is_offset_changed {
self.style_for_normal(cx)
} else if is_always_to_show {
#[allow(clippy::if_same_then_else)]
if is_hovered_on_thumb {
Self::style_for_hovered_thumb(cx)
} else {
Self::style_for_hovered_bar(cx)
}
} else {
let mut idle_state = Self::style_for_idle(cx);
let mut idle_state = self.style_for_idle(cx);
// Delay 2s to fade out the scrollbar thumb (in 1s)
if let Some(last_time) = state.get().last_scroll_time {
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
if elapsed < FADE_OUT_DURATION {
if is_hovered_on_bar {
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
idle_state = if is_hovered_on_thumb {
Self::style_for_hovered_thumb(cx)
} else {
Self::style_for_hovered_bar(cx)
};
if is_hovered_on_bar {
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
idle_state = if is_hovered_on_thumb {
Self::style_for_hovered_thumb(cx)
} else {
if elapsed < FADE_OUT_DELAY {
idle_state.0 = cx.theme().scrollbar_thumb_background;
} else {
// opacity = 1 - (x - 2)^10
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
idle_state.0 =
cx.theme().scrollbar_thumb_background.opacity(opacity);
};
Self::style_for_hovered_bar(cx)
};
} else if elapsed < FADE_OUT_DELAY {
idle_state.0 = cx.theme().scrollbar_thumb_background;
window.request_animation_frame();
if !state.get().idle_timer_scheduled {
let state = state.clone();
state.set(state.get().with_idle_timer_scheduled(true));
let current_view = window.current_view();
let next_delay = Duration::from_secs_f32(FADE_OUT_DELAY - elapsed);
window
.spawn(cx, async move |cx| {
cx.background_executor().timer(next_delay).await;
state.set(state.get().with_idle_timer_scheduled(false));
cx.update(|_, cx| cx.notify(current_view)).ok();
})
.detach();
}
} else if elapsed < FADE_OUT_DURATION {
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
idle_state.0 = cx.theme().scrollbar_thumb_background.opacity(opacity);
window.request_animation_frame();
}
}
@@ -617,7 +699,11 @@ impl Element for Scrollbar {
})
}
PrepaintState { hitbox, states }
PrepaintState {
hitbox,
states,
scrollbar_state: state,
}
}
fn paint(
@@ -630,19 +716,21 @@ impl Element for Scrollbar {
window: &mut Window,
cx: &mut App,
) {
let scrollbar_state = &prepaint.scrollbar_state;
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
let view_id = window.current_view();
let hitbox_bounds = prepaint.hitbox.bounds;
let is_visible =
self.state.get().is_scrollbar_visible() || cx.theme().scrollbar_mode.is_always();
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
let is_hover_to_show = scrollbar_show.is_hover();
// Update last_scroll_time when offset is changed.
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
self.state.set(
self.state
if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
scrollbar_state.set(
scrollbar_state
.get()
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
);
cx.notify(view_id);
}
window.with_content_mask(
@@ -652,7 +740,10 @@ impl Element for Scrollbar {
|window| {
for state in prepaint.states.iter() {
let axis = state.axis;
let radius = state.radius;
let mut radius = state.radius;
if cx.theme().radius.is_zero() {
radius = px(0.);
}
let bounds = state.bounds;
let thumb_bounds = state.thumb_bounds;
let scroll_area_size = state.scroll_size;
@@ -686,7 +777,7 @@ impl Element for Scrollbar {
});
window.on_mouse_event({
let state = self.state.clone();
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
move |event: &ScrollWheelEvent, phase, _, cx| {
@@ -707,7 +798,7 @@ impl Element for Scrollbar {
if is_hover_to_show || is_visible {
window.on_mouse_event({
let state = self.state.clone();
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
move |event: &MouseDownEvent, phase, _, cx| {
@@ -718,6 +809,7 @@ impl Element for Scrollbar {
// click on the thumb bar, set the drag position
let pos = event.position - thumb_bounds.origin;
scroll_handle.start_drag();
state.set(state.get().with_drag_pos(axis, pos));
cx.notify(view_id);
@@ -755,7 +847,7 @@ impl Element for Scrollbar {
window.on_mouse_event({
let scroll_handle = self.scroll_handle.clone();
let state = self.state.clone();
let state = scrollbar_state.clone();
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
move |event: &MouseMoveEvent, _, _, cx| {
@@ -770,9 +862,7 @@ impl Element for Scrollbar {
if state.get().hovered_axis != Some(axis) {
notify = true;
}
} else if state.get().hovered_axis == Some(axis)
&& state.get().hovered_axis.is_some()
{
} else if state.get().hovered_axis == Some(axis) {
state.set(state.get().with_hovered(None));
notify = true;
}
@@ -790,6 +880,9 @@ impl Element for Scrollbar {
// Move thumb position on dragging
if state.get().dragged_axis == Some(axis) && event.dragging() {
// Stop the event propagation to avoid selecting text or other side effects.
cx.stop_propagation();
// drag_pos is the position of the mouse down event
// We need to keep the thumb bar still at the origin down position
let drag_pos = state.get().drag_pos;
@@ -836,10 +929,12 @@ impl Element for Scrollbar {
});
window.on_mouse_event({
let state = self.state.clone();
let state = scrollbar_state.clone();
let scroll_handle = self.scroll_handle.clone();
move |_event: &MouseUpEvent, phase, _, cx| {
if phase.bubble() {
scroll_handle.end_drag();
state.set(state.get().with_unset_drag_pos());
cx.notify(view_id);
}

View File

@@ -22,8 +22,8 @@ impl Skeleton {
}
}
pub fn secondary(mut self, secondary: bool) -> Self {
self.secondary = secondary;
pub fn secondary(mut self) -> Self {
self.secondary = true;
self
}
}

View File

@@ -1,11 +1,7 @@
use std::fmt::{self, Display, Formatter};
use gpui::{div, px, App, Axis, Div, Element, Pixels, Refineable, StyleRefinement, Styled};
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
use serde::{Deserialize, Serialize};
use theme::ActiveTheme;
use crate::scroll::{Scrollable, ScrollbarAxis};
/// Returns a `Div` as horizontal flex layout.
pub fn h_flex() -> Div {
div().h_flex()
@@ -18,7 +14,7 @@ pub fn v_flex() -> Div {
/// Returns a `Div` as divider.
pub fn divider(cx: &App) -> Div {
div().my_2().w_full().h_px().bg(cx.theme().border)
div().my_2().w_full().h_px().bg(cx.theme().border_variant)
}
macro_rules! font_weight {
@@ -50,17 +46,6 @@ pub trait StyledExt: Styled + Sized {
self.flex().flex_col()
}
/// Wraps the element in a ScrollView.
///
/// Current this is only have a vertical scrollbar.
#[inline]
fn scrollable(self, axis: impl Into<ScrollbarAxis>) -> Scrollable<Self>
where
Self: Element,
{
Scrollable::new(axis, self)
}
font_weight!(font_thin, THIN);
font_weight!(font_extralight, EXTRA_LIGHT);
font_weight!(font_light, LIGHT);
@@ -183,39 +168,43 @@ impl<T: Styled> StyleSized<T> for T {
fn input_pl(self, size: Size) -> Self {
match size {
Size::Large => self.pl_5(),
Size::XSmall => self.pl_1(),
Size::Medium => self.pl_3(),
Size::Large => self.pl_5(),
_ => self.pl_2(),
}
}
fn input_pr(self, size: Size) -> Self {
match size {
Size::Large => self.pr_5(),
Size::XSmall => self.pr_1(),
Size::Medium => self.pr_3(),
Size::Large => self.pr_5(),
_ => self.pr_2(),
}
}
fn input_px(self, size: Size) -> Self {
match size {
Size::Large => self.px_5(),
Size::XSmall => self.px_1(),
Size::Medium => self.px_3(),
Size::Large => self.px_5(),
_ => self.px_2(),
}
}
fn input_py(self, size: Size) -> Self {
match size {
Size::Large => self.py_5(),
Size::XSmall => self.py_0p5(),
Size::Medium => self.py_2(),
Size::Large => self.py_5(),
_ => self.py_1(),
}
}
fn input_h(self, size: Size) -> Self {
match size {
Size::XSmall => self.h_7(),
Size::XSmall => self.h_6(),
Size::Small => self.h_8(),
Size::Medium => self.h_9(),
Size::Large => self.h_12(),
@@ -255,74 +244,6 @@ impl<T: Styled> StyleSized<T> for T {
}
}
pub trait AxisExt {
fn is_horizontal(&self) -> bool;
fn is_vertical(&self) -> bool;
}
impl AxisExt for Axis {
fn is_horizontal(&self) -> bool {
self == &Axis::Horizontal
}
fn is_vertical(&self) -> bool {
self == &Axis::Vertical
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Placement {
Top,
Bottom,
Left,
Right,
}
impl Display for Placement {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Placement::Top => write!(f, "Top"),
Placement::Bottom => write!(f, "Bottom"),
Placement::Left => write!(f, "Left"),
Placement::Right => write!(f, "Right"),
}
}
}
impl Placement {
pub fn is_horizontal(&self) -> bool {
matches!(self, Placement::Left | Placement::Right)
}
pub fn is_vertical(&self) -> bool {
matches!(self, Placement::Top | Placement::Bottom)
}
pub fn axis(&self) -> Axis {
match self {
Placement::Top | Placement::Bottom => Axis::Vertical,
Placement::Left | Placement::Right => Axis::Horizontal,
}
}
}
/// A enum for defining the side of the element.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Side {
Left,
Right,
}
impl Side {
pub(crate) fn is_left(&self) -> bool {
matches!(self, Self::Left)
}
pub(crate) fn is_right(&self) -> bool {
matches!(self, Self::Right)
}
}
/// A trait for defining element that can be collapsed.
pub trait Collapsible {
fn collapsed(self, collapsed: bool) -> Self;

View File

@@ -1,204 +0,0 @@
use gpui::prelude::FluentBuilder as _;
use gpui::{
canvas, div, point, px, AnyElement, App, Bounds, CursorStyle, Decorations, Edges,
HitboxBehavior, Hsla, InteractiveElement as _, IntoElement, MouseButton, ParentElement, Pixels,
Point, RenderOnce, ResizeEdge, Size, Styled as _, Window,
};
use theme::{CLIENT_SIDE_DECORATION_ROUNDING, CLIENT_SIDE_DECORATION_SHADOW};
const WINDOW_BORDER_WIDTH: Pixels = px(1.0);
/// Create a new window border.
pub fn window_border() -> WindowBorder {
WindowBorder::new()
}
/// Window border use to render a custom window border and shadow for Linux.
#[derive(IntoElement, Default)]
pub struct WindowBorder {
children: Vec<AnyElement>,
}
/// Get the window paddings.
pub fn window_paddings(window: &Window, _cx: &App) -> Edges<Pixels> {
match window.window_decorations() {
Decorations::Server => Edges::all(px(0.0)),
Decorations::Client { tiling } => {
let mut paddings = Edges::all(CLIENT_SIDE_DECORATION_SHADOW);
if tiling.top {
paddings.top = px(0.0);
}
if tiling.bottom {
paddings.bottom = px(0.0);
}
if tiling.left {
paddings.left = px(0.0);
}
if tiling.right {
paddings.right = px(0.0);
}
paddings
}
}
}
impl WindowBorder {
pub fn new() -> Self {
Self {
..Default::default()
}
}
}
impl ParentElement for WindowBorder {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl RenderOnce for WindowBorder {
fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let decorations = window.window_decorations();
window.set_client_inset(CLIENT_SIDE_DECORATION_SHADOW);
div()
.id("window-backdrop")
.bg(gpui::transparent_black())
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling, .. } => div
.bg(gpui::transparent_black())
.child(
canvas(
|_bounds, window, _cx| {
window.insert_hitbox(
Bounds::new(
point(px(0.0), px(0.0)),
window.window_bounds().get_bounds().size,
),
HitboxBehavior::Normal,
)
},
move |_bounds, hitbox, window, _cx| {
let mouse = window.mouse_position();
let size = window.window_bounds().get_bounds().size;
let Some(edge) =
resize_edge(mouse, CLIENT_SIDE_DECORATION_SHADOW, size)
else {
return;
};
window.set_cursor_style(
match edge {
ResizeEdge::Top | ResizeEdge::Bottom => {
CursorStyle::ResizeUpDown
}
ResizeEdge::Left | ResizeEdge::Right => {
CursorStyle::ResizeLeftRight
}
ResizeEdge::TopLeft | ResizeEdge::BottomRight => {
CursorStyle::ResizeUpLeftDownRight
}
ResizeEdge::TopRight | ResizeEdge::BottomLeft => {
CursorStyle::ResizeUpRightDownLeft
}
},
&hitbox,
);
},
)
.size_full()
.absolute(),
)
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| div.pt(CLIENT_SIDE_DECORATION_SHADOW))
.when(!tiling.bottom, |div| div.pb(CLIENT_SIDE_DECORATION_SHADOW))
.when(!tiling.left, |div| div.pl(CLIENT_SIDE_DECORATION_SHADOW))
.when(!tiling.right, |div| div.pr(CLIENT_SIDE_DECORATION_SHADOW))
.on_mouse_down(MouseButton::Left, move |_, window, _cx| {
let size = window.window_bounds().get_bounds().size;
let pos = window.mouse_position();
if let Some(edge) = resize_edge(pos, CLIENT_SIDE_DECORATION_SHADOW, size) {
window.start_window_resize(edge)
};
}),
})
.size_full()
.child(
div()
.map(|div| match decorations {
Decorations::Server => div,
Decorations::Client { tiling } => div
.when(!(tiling.top || tiling.right), |div| {
div.rounded_tr(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.top || tiling.left), |div| {
div.rounded_tl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.right), |div| {
div.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!(tiling.bottom || tiling.left), |div| {
div.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
})
.when(!tiling.top, |div| div.border_t(WINDOW_BORDER_WIDTH))
.when(!tiling.bottom, |div| div.border_b(WINDOW_BORDER_WIDTH))
.when(!tiling.left, |div| div.border_l(WINDOW_BORDER_WIDTH))
.when(!tiling.right, |div| div.border_r(WINDOW_BORDER_WIDTH))
.when(!tiling.is_tiled(), |div| {
div.shadow(vec![gpui::BoxShadow {
color: Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.3,
},
blur_radius: CLIENT_SIDE_DECORATION_SHADOW / 2.,
spread_radius: px(0.),
offset: point(px(0.0), px(0.0)),
}])
}),
})
.on_mouse_move(|_e, _window, cx| {
cx.stop_propagation();
})
.bg(gpui::transparent_black())
.size_full()
.children(self.children),
)
}
}
fn resize_edge(pos: Point<Pixels>, shadow_size: Pixels, size: Size<Pixels>) -> Option<ResizeEdge> {
let edge = if pos.y < shadow_size && pos.x < shadow_size {
ResizeEdge::TopLeft
} else if pos.y < shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::TopRight
} else if pos.y < shadow_size {
ResizeEdge::Top
} else if pos.y > size.height - shadow_size && pos.x < shadow_size {
ResizeEdge::BottomLeft
} else if pos.y > size.height - shadow_size && pos.x > size.width - shadow_size {
ResizeEdge::BottomRight
} else if pos.y > size.height - shadow_size {
ResizeEdge::Bottom
} else if pos.x < shadow_size {
ResizeEdge::Left
} else if pos.x > size.width - shadow_size {
ResizeEdge::Right
} else {
return None;
};
Some(edge)
}

120
crates/ui/src/window_ext.rs Normal file
View File

@@ -0,0 +1,120 @@
use std::rc::Rc;
use gpui::{App, Entity, SharedString, Window};
use crate::input::InputState;
use crate::modal::Modal;
use crate::notification::Notification;
use crate::Root;
/// Extension trait for [`Window`] to add modal, notification .. functionality.
pub trait WindowExtension: Sized {
/// Opens a Modal.
fn open_modal<F>(&mut self, cx: &mut App, builder: F)
where
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static;
/// Return true, if there is an active Modal.
fn has_active_modal(&mut self, cx: &mut App) -> bool;
/// Closes the last active Modal.
fn close_modal(&mut self, cx: &mut App);
/// Closes all active Modals.
fn close_all_modals(&mut self, cx: &mut App);
/// Returns number of notifications.
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>>;
/// Pushes a notification to the notification list.
fn push_notification<T>(&mut self, note: T, cx: &mut App)
where
T: Into<Notification>;
/// Clears a notification by its ID.
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
where
T: Into<SharedString>;
/// Clear all notifications
fn clear_notifications(&mut self, cx: &mut App);
/// Return current focused Input entity.
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>>;
/// Returns true if there is a focused Input entity.
fn has_focused_input(&mut self, cx: &mut App) -> bool;
}
impl WindowExtension for Window {
#[inline]
fn open_modal<F>(&mut self, cx: &mut App, builder: F)
where
F: Fn(Modal, &mut Window, &mut App) -> Modal + 'static,
{
Root::update(self, cx, move |root, window, cx| {
root.open_modal(builder, window, cx);
})
}
#[inline]
fn has_active_modal(&mut self, cx: &mut App) -> bool {
Root::read(self, cx).has_active_modals()
}
#[inline]
fn close_modal(&mut self, cx: &mut App) {
Root::update(self, cx, move |root, window, cx| {
root.close_modal(window, cx);
})
}
#[inline]
fn close_all_modals(&mut self, cx: &mut App) {
Root::update(self, cx, |root, window, cx| {
root.close_all_modals(window, cx);
})
}
#[inline]
fn push_notification<T>(&mut self, note: T, cx: &mut App)
where
T: Into<Notification>,
{
let note = note.into();
Root::update(self, cx, move |root, window, cx| {
root.push_notification(note, window, cx);
})
}
#[inline]
fn clear_notification<T>(&mut self, id: T, cx: &mut App)
where
T: Into<SharedString>,
{
let id = id.into();
Root::update(self, cx, move |root, window, cx| {
root.clear_notification(id, window, cx);
})
}
#[inline]
fn clear_notifications(&mut self, cx: &mut App) {
Root::update(self, cx, move |root, window, cx| {
root.clear_notifications(window, cx);
})
}
fn notifications(&mut self, cx: &mut App) -> Rc<Vec<Entity<Notification>>> {
let entity = Root::read(self, cx).notification.clone();
Rc::new(entity.read(cx).notifications())
}
fn has_focused_input(&mut self, cx: &mut App) -> bool {
Root::read(self, cx).focused_input.is_some()
}
fn focused_input(&mut self, cx: &mut App) -> Option<Entity<InputState>> {
Root::read(self, cx).focused_input.clone()
}
}