feat: revamp the chat panel ui #7
@@ -82,10 +82,20 @@ impl ChatRegistry {
|
|||||||
/// Create a new chat registry instance
|
/// Create a new chat registry instance
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let nostr = NostrRegistry::global(cx);
|
let nostr = NostrRegistry::global(cx);
|
||||||
|
let nip65 = nostr.read(cx).nip65_state();
|
||||||
let nip17 = nostr.read(cx).nip17_state();
|
let nip17 = nostr.read(cx).nip17_state();
|
||||||
|
|
||||||
let mut subscriptions = smallvec![];
|
let mut subscriptions = smallvec![];
|
||||||
|
|
||||||
|
subscriptions.push(
|
||||||
|
// Observe the nip65 state and load chat rooms on every state change
|
||||||
|
cx.observe(&nip65, |this, state, cx| {
|
||||||
|
if state.read(cx).idle() {
|
||||||
|
this.reset(cx);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
subscriptions.push(
|
subscriptions.push(
|
||||||
// Observe the nip17 state and load chat rooms on every state change
|
// Observe the nip17 state and load chat rooms on every state change
|
||||||
cx.observe(&nip17, |this, _state, cx| {
|
cx.observe(&nip17, |this, _state, cx| {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ impl SendReport {
|
|||||||
/// Returns true if the send was successful.
|
/// Returns true if the send was successful.
|
||||||
pub fn success(&self) -> bool {
|
pub fn success(&self) -> bool {
|
||||||
if let Some(output) = self.output.as_ref() {
|
if let Some(output) = self.output.as_ref() {
|
||||||
!output.success.is_empty()
|
!output.failed.is_empty()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,10 @@ use state::NostrRegistry;
|
|||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::context_menu::ContextMenuExt;
|
|
||||||
use ui::indicator::Indicator;
|
use ui::indicator::Indicator;
|
||||||
use ui::input::{InputEvent, InputState, TextInput};
|
use ui::input::{InputEvent, InputState, TextInput};
|
||||||
|
use ui::menu::{ContextMenuExt, DropdownMenu};
|
||||||
use ui::notification::Notification;
|
use ui::notification::Notification;
|
||||||
use ui::popup_menu::PopupMenuExt;
|
|
||||||
use ui::{
|
use ui::{
|
||||||
h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
|
h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
|
||||||
WindowExtension,
|
WindowExtension,
|
||||||
@@ -392,7 +391,15 @@ impl ChatPanel {
|
|||||||
self.reports_by_id
|
self.reports_by_id
|
||||||
.read_blocking()
|
.read_blocking()
|
||||||
.get(id)
|
.get(id)
|
||||||
.is_some_and(|reports| reports.iter().all(|r| r.success()))
|
.is_some_and(|reports| reports.iter().any(|r| r.success()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a message failed to send by its ID
|
||||||
|
fn sent_failed(&self, id: &EventId) -> bool {
|
||||||
|
self.reports_by_id
|
||||||
|
.read_blocking()
|
||||||
|
.get(id)
|
||||||
|
.is_some_and(|reports| reports.iter().all(|r| !r.success()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get all sent reports for a message by its ID
|
/// Get all sent reports for a message by its ID
|
||||||
@@ -622,6 +629,9 @@ impl ChatPanel {
|
|||||||
// Check if message is sent successfully
|
// Check if message is sent successfully
|
||||||
let sent_success = self.sent_success(&id);
|
let sent_success = self.sent_success(&id);
|
||||||
|
|
||||||
|
// Check if message is sent failed
|
||||||
|
let sent_failed = self.sent_failed(&id);
|
||||||
|
|
||||||
// Hide avatar setting
|
// Hide avatar setting
|
||||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||||
|
|
||||||
@@ -679,7 +689,7 @@ impl ChatPanel {
|
|||||||
this.children(self.render_message_replies(replies, cx))
|
this.children(self.render_message_replies(replies, cx))
|
||||||
})
|
})
|
||||||
.child(rendered_text)
|
.child(rendered_text)
|
||||||
.when(!sent_success, |this| {
|
.when(sent_failed, |this| {
|
||||||
this.child(deferred(self.render_message_reports(&id, cx)))
|
this.child(deferred(self.render_message_reports(&id, cx)))
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -966,9 +976,9 @@ impl ChatPanel {
|
|||||||
.icon(IconName::Ellipsis)
|
.icon(IconName::Ellipsis)
|
||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.popup_menu({
|
.dropdown_menu({
|
||||||
let id = id.to_owned();
|
let id = id.to_owned();
|
||||||
move |this, _, _| this.menu("Seen on", Box::new(SeenOn(id)))
|
move |this, _window, _cx| this.menu("Seen on", Box::new(SeenOn(id)))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.group_hover("", |this| this.visible())
|
.group_hover("", |this| this.visible())
|
||||||
@@ -1152,7 +1162,8 @@ impl Render for ChatPanel {
|
|||||||
this.render_message(ix, window, cx)
|
this.render_message(ix, window, cx)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.flex_1(),
|
.flex_1()
|
||||||
|
.size_full(),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
@@ -1192,10 +1203,10 @@ impl Render for ChatPanel {
|
|||||||
.icon(IconName::Emoji)
|
.icon(IconName::Emoji)
|
||||||
.ghost()
|
.ghost()
|
||||||
.large()
|
.large()
|
||||||
.popup_menu_with_anchor(
|
.dropdown_menu_with_anchor(
|
||||||
gpui::Corner::BottomLeft,
|
gpui::Corner::BottomLeft,
|
||||||
move |this, _window, _cx| {
|
move |this, _window, _cx| {
|
||||||
this.axis(gpui::Axis::Horizontal)
|
this//.axis(gpui::Axis::Horizontal)
|
||||||
.menu("👍", Box::new(Command::Insert("👍")))
|
.menu("👍", Box::new(Command::Insert("👍")))
|
||||||
.menu("👎", Box::new(Command::Insert("👎")))
|
.menu("👎", Box::new(Command::Insert("👎")))
|
||||||
.menu("😄", Box::new(Command::Insert("😄")))
|
.menu("😄", Box::new(Command::Insert("😄")))
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use chat::RoomKind;
|
use chat::RoomKind;
|
||||||
use chat_ui::{CopyPublicKey, OpenPublicKey};
|
|
||||||
use dock::ClosePanel;
|
use dock::ClosePanel;
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@@ -12,7 +11,6 @@ use nostr_sdk::prelude::*;
|
|||||||
use settings::AppSettings;
|
use settings::AppSettings;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::context_menu::ContextMenuExt;
|
|
||||||
use ui::modal::ModalButtonProps;
|
use ui::modal::ModalButtonProps;
|
||||||
use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
use ui::{h_flex, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension};
|
||||||
|
|
||||||
@@ -153,12 +151,6 @@ impl RenderOnce for RoomEntry {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||||
.when_some(public_key, |this, public_key| {
|
|
||||||
this.context_menu(move |this, _window, _cx| {
|
|
||||||
this.menu("View Profile", Box::new(OpenPublicKey(public_key)))
|
|
||||||
.menu("Copy Public Key", Box::new(CopyPublicKey(public_key)))
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.when_some(self.handler, |this, handler| {
|
.when_some(self.handler, |this, handler| {
|
||||||
this.on_click(move |event, window, cx| {
|
this.on_click(move |event, window, cx| {
|
||||||
handler(event, window, cx);
|
handler(event, window, cx);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
|
|||||||
use titlebar::TitleBar;
|
use titlebar::TitleBar;
|
||||||
use ui::avatar::Avatar;
|
use ui::avatar::Avatar;
|
||||||
use ui::button::{Button, ButtonVariants};
|
use ui::button::{Button, ButtonVariants};
|
||||||
use ui::popup_menu::PopupMenuExt;
|
use ui::menu::DropdownMenu;
|
||||||
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
||||||
|
|
||||||
use crate::panels::greeter;
|
use crate::panels::greeter;
|
||||||
@@ -184,7 +184,7 @@ impl Workspace {
|
|||||||
.caret()
|
.caret()
|
||||||
.compact()
|
.compact()
|
||||||
.transparent()
|
.transparent()
|
||||||
.popup_menu(move |this, _window, _cx| {
|
.dropdown_menu(move |this, _window, _cx| {
|
||||||
this.label(profile.name())
|
this.label(profile.name())
|
||||||
.separator()
|
.separator()
|
||||||
.menu("Profile", Box::new(ClosePanel))
|
.menu("Profile", Box::new(ClosePanel))
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use gpui::{
|
|||||||
SharedString, Window,
|
SharedString, Window,
|
||||||
};
|
};
|
||||||
use ui::button::Button;
|
use ui::button::Button;
|
||||||
use ui::popup_menu::PopupMenu;
|
use ui::menu::PopupMenu;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum PanelEvent {
|
pub enum PanelEvent {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
||||||
use ui::button::{Button, ButtonVariants as _};
|
use ui::button::{Button, ButtonVariants as _};
|
||||||
use ui::popup_menu::{PopupMenu, PopupMenuExt};
|
use ui::menu::{DropdownMenu, PopupMenu};
|
||||||
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
use ui::{h_flex, v_flex, AxisExt, IconName, Placement, Selectable, Sizable, StyledExt};
|
||||||
|
|
||||||
use crate::dock::DockPlacement;
|
use crate::dock::DockPlacement;
|
||||||
@@ -454,7 +454,7 @@ impl TabPanel {
|
|||||||
.small()
|
.small()
|
||||||
.ghost()
|
.ghost()
|
||||||
.rounded()
|
.rounded()
|
||||||
.popup_menu({
|
.dropdown_menu({
|
||||||
let zoomable = state.zoomable;
|
let zoomable = state.zoomable;
|
||||||
let closable = state.closable;
|
let closable = state.closable;
|
||||||
|
|
||||||
|
|||||||
333
crates/ui/src/anchored.rs
Normal file
333
crates/ui/src/anchored.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,811 +0,0 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
|
||||||
use gpui::{
|
|
||||||
anchored, canvas, deferred, div, px, rems, AnyElement, App, AppContext, Bounds, ClickEvent,
|
|
||||||
Context, DismissEvent, ElementId, Empty, Entity, EventEmitter, FocusHandle, Focusable,
|
|
||||||
InteractiveElement, IntoElement, KeyBinding, Length, ParentElement, Pixels, Render, RenderOnce,
|
|
||||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window,
|
|
||||||
};
|
|
||||||
use theme::ActiveTheme;
|
|
||||||
|
|
||||||
use crate::actions::{Cancel, Confirm, SelectDown, SelectUp};
|
|
||||||
use crate::input::clear_button::clear_button;
|
|
||||||
use crate::list::{List, ListDelegate, ListItem};
|
|
||||||
use crate::{h_flex, v_flex, Disableable as _, Icon, IconName, Sizable, Size, StyleSized};
|
|
||||||
|
|
||||||
const CONTEXT: &str = "Dropdown";
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub enum ListEvent {
|
|
||||||
/// Single click or move to selected row.
|
|
||||||
SelectItem(usize),
|
|
||||||
/// Double click on the row.
|
|
||||||
ConfirmItem(usize),
|
|
||||||
// Cancel the selection.
|
|
||||||
Cancel,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
|
||||||
cx.bind_keys([
|
|
||||||
KeyBinding::new("up", SelectUp, Some(CONTEXT)),
|
|
||||||
KeyBinding::new("down", SelectDown, Some(CONTEXT)),
|
|
||||||
KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
|
|
||||||
KeyBinding::new(
|
|
||||||
"secondary-enter",
|
|
||||||
Confirm { secondary: true },
|
|
||||||
Some(CONTEXT),
|
|
||||||
),
|
|
||||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait for items that can be displayed in a dropdown.
|
|
||||||
pub trait DropdownItem {
|
|
||||||
type Value: Clone;
|
|
||||||
fn title(&self) -> SharedString;
|
|
||||||
/// Customize the display title used to selected item in Dropdown Input.
|
|
||||||
///
|
|
||||||
/// If return None, the title will be used.
|
|
||||||
fn display_title(&self) -> Option<AnyElement> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
fn value(&self) -> &Self::Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DropdownItem for String {
|
|
||||||
type Value = Self;
|
|
||||||
|
|
||||||
fn title(&self) -> SharedString {
|
|
||||||
SharedString::from(self.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn value(&self) -> &Self::Value {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DropdownItem for SharedString {
|
|
||||||
type Value = Self;
|
|
||||||
|
|
||||||
fn title(&self) -> SharedString {
|
|
||||||
SharedString::from(self.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn value(&self) -> &Self::Value {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait DropdownDelegate: Sized {
|
|
||||||
type Item: DropdownItem;
|
|
||||||
|
|
||||||
fn len(&self) -> usize;
|
|
||||||
|
|
||||||
fn is_empty(&self) -> bool {
|
|
||||||
self.len() == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, ix: usize) -> Option<&Self::Item>;
|
|
||||||
|
|
||||||
fn position<V>(&self, value: &V) -> Option<usize>
|
|
||||||
where
|
|
||||||
Self::Item: DropdownItem<Value = V>,
|
|
||||||
V: PartialEq,
|
|
||||||
{
|
|
||||||
(0..self.len()).find(|&i| self.get(i).is_some_and(|item| item.value() == value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_search(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform_search(&mut self, _query: &str, _window: &mut Window, _: &mut App) -> Task<()> {
|
|
||||||
Task::ready(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: DropdownItem> DropdownDelegate for Vec<T> {
|
|
||||||
type Item = T;
|
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
|
||||||
self.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, ix: usize) -> Option<&Self::Item> {
|
|
||||||
self.as_slice().get(ix)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position<V>(&self, value: &V) -> Option<usize>
|
|
||||||
where
|
|
||||||
Self::Item: DropdownItem<Value = V>,
|
|
||||||
V: PartialEq,
|
|
||||||
{
|
|
||||||
self.iter().position(|v| v.value() == value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DropdownListDelegate<D: DropdownDelegate + 'static> {
|
|
||||||
delegate: D,
|
|
||||||
dropdown: WeakEntity<DropdownState<D>>,
|
|
||||||
selected_index: Option<usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> ListDelegate for DropdownListDelegate<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
type Item = ListItem;
|
|
||||||
|
|
||||||
fn items_count(&self, _: &App) -> usize {
|
|
||||||
self.delegate.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_item(
|
|
||||||
&self,
|
|
||||||
ix: usize,
|
|
||||||
_: &mut gpui::Window,
|
|
||||||
cx: &mut gpui::Context<List<Self>>,
|
|
||||||
) -> Option<Self::Item> {
|
|
||||||
let selected = self.selected_index == Some(ix);
|
|
||||||
let size = self
|
|
||||||
.dropdown
|
|
||||||
.upgrade()
|
|
||||||
.map_or(Size::Medium, |dropdown| dropdown.read(cx).size);
|
|
||||||
|
|
||||||
if let Some(item) = self.delegate.get(ix) {
|
|
||||||
let list_item = ListItem::new(("list-item", ix))
|
|
||||||
.check_icon(IconName::Check)
|
|
||||||
.selected(selected)
|
|
||||||
.input_font_size(size)
|
|
||||||
.list_size(size)
|
|
||||||
.child(div().whitespace_nowrap().child(item.title().to_string()));
|
|
||||||
Some(list_item)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn cancel(&mut self, window: &mut Window, cx: &mut Context<List<Self>>) {
|
|
||||||
let dropdown = self.dropdown.clone();
|
|
||||||
cx.defer_in(window, move |_, window, cx| {
|
|
||||||
_ = dropdown.update(cx, |this, cx| {
|
|
||||||
this.open = false;
|
|
||||||
this.focus(window, cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<List<Self>>) {
|
|
||||||
let selected_value = self
|
|
||||||
.selected_index
|
|
||||||
.and_then(|ix| self.delegate.get(ix))
|
|
||||||
.map(|item| item.value().clone());
|
|
||||||
let dropdown = self.dropdown.clone();
|
|
||||||
|
|
||||||
cx.defer_in(window, move |_, window, cx| {
|
|
||||||
_ = dropdown.update(cx, |this, cx| {
|
|
||||||
cx.emit(DropdownEvent::Confirm(selected_value.clone()));
|
|
||||||
this.selected_value = selected_value;
|
|
||||||
this.open = false;
|
|
||||||
this.focus(window, cx);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform_search(
|
|
||||||
&mut self,
|
|
||||||
query: &str,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<List<Self>>,
|
|
||||||
) -> Task<()> {
|
|
||||||
self.dropdown.upgrade().map_or(Task::ready(()), |dropdown| {
|
|
||||||
dropdown.update(cx, |_, cx| self.delegate.perform_search(query, window, cx))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
ix: Option<usize>,
|
|
||||||
_: &mut Window,
|
|
||||||
_: &mut Context<List<Self>>,
|
|
||||||
) {
|
|
||||||
self.selected_index = ix;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_empty(&self, window: &mut Window, cx: &mut Context<List<Self>>) -> impl IntoElement {
|
|
||||||
if let Some(empty) = self
|
|
||||||
.dropdown
|
|
||||||
.upgrade()
|
|
||||||
.and_then(|dropdown| dropdown.read(cx).empty.as_ref())
|
|
||||||
{
|
|
||||||
empty(window, cx).into_any_element()
|
|
||||||
} else {
|
|
||||||
h_flex()
|
|
||||||
.justify_center()
|
|
||||||
.py_6()
|
|
||||||
.text_color(cx.theme().text_muted)
|
|
||||||
.child(Icon::new(IconName::Loader).size(px(28.)))
|
|
||||||
.into_any_element()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum DropdownEvent<D: DropdownDelegate + 'static> {
|
|
||||||
Confirm(Option<<D::Item as DropdownItem>::Value>),
|
|
||||||
}
|
|
||||||
|
|
||||||
type DropdownStateEmpty = Option<Box<dyn Fn(&Window, &App) -> AnyElement>>;
|
|
||||||
|
|
||||||
/// State of the [`Dropdown`].
|
|
||||||
pub struct DropdownState<D: DropdownDelegate + 'static> {
|
|
||||||
focus_handle: FocusHandle,
|
|
||||||
list: Entity<List<DropdownListDelegate<D>>>,
|
|
||||||
size: Size,
|
|
||||||
empty: DropdownStateEmpty,
|
|
||||||
/// Store the bounds of the input
|
|
||||||
bounds: Bounds<Pixels>,
|
|
||||||
open: bool,
|
|
||||||
selected_value: Option<<D::Item as DropdownItem>::Value>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A Dropdown element.
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Dropdown<D: DropdownDelegate + 'static> {
|
|
||||||
id: ElementId,
|
|
||||||
state: Entity<DropdownState<D>>,
|
|
||||||
size: Size,
|
|
||||||
icon: Option<Icon>,
|
|
||||||
cleanable: bool,
|
|
||||||
placeholder: Option<SharedString>,
|
|
||||||
title_prefix: Option<SharedString>,
|
|
||||||
empty: Option<AnyElement>,
|
|
||||||
width: Length,
|
|
||||||
menu_width: Length,
|
|
||||||
disabled: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SearchableVec<T> {
|
|
||||||
items: Vec<T>,
|
|
||||||
matched_items: Vec<T>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: DropdownItem + Clone> SearchableVec<T> {
|
|
||||||
pub fn new(items: impl Into<Vec<T>>) -> Self {
|
|
||||||
let items = items.into();
|
|
||||||
Self {
|
|
||||||
items: items.clone(),
|
|
||||||
matched_items: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: DropdownItem + Clone> DropdownDelegate for SearchableVec<T> {
|
|
||||||
type Item = T;
|
|
||||||
|
|
||||||
fn len(&self) -> usize {
|
|
||||||
self.matched_items.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, ix: usize) -> Option<&Self::Item> {
|
|
||||||
self.matched_items.get(ix)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn position<V>(&self, value: &V) -> Option<usize>
|
|
||||||
where
|
|
||||||
Self::Item: DropdownItem<Value = V>,
|
|
||||||
V: PartialEq,
|
|
||||||
{
|
|
||||||
for (ix, item) in self.matched_items.iter().enumerate() {
|
|
||||||
if item.value() == value {
|
|
||||||
return Some(ix);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn can_search(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn perform_search(&mut self, query: &str, _window: &mut Window, _: &mut App) -> Task<()> {
|
|
||||||
self.matched_items = self
|
|
||||||
.items
|
|
||||||
.iter()
|
|
||||||
.filter(|item| item.title().to_lowercase().contains(&query.to_lowercase()))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Task::ready(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<SharedString>> for SearchableVec<SharedString> {
|
|
||||||
fn from(items: Vec<SharedString>) -> Self {
|
|
||||||
Self {
|
|
||||||
items: items.clone(),
|
|
||||||
matched_items: items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> DropdownState<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
pub fn new(
|
|
||||||
delegate: D,
|
|
||||||
selected_index: Option<usize>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let focus_handle = cx.focus_handle();
|
|
||||||
let delegate = DropdownListDelegate {
|
|
||||||
delegate,
|
|
||||||
dropdown: cx.entity().downgrade(),
|
|
||||||
selected_index,
|
|
||||||
};
|
|
||||||
|
|
||||||
let searchable = delegate.delegate.can_search();
|
|
||||||
|
|
||||||
let list = cx.new(|cx| {
|
|
||||||
let mut list = List::new(delegate, window, cx)
|
|
||||||
.max_h(rems(20.))
|
|
||||||
.reset_on_cancel(false);
|
|
||||||
if !searchable {
|
|
||||||
list = list.no_query();
|
|
||||||
}
|
|
||||||
list
|
|
||||||
});
|
|
||||||
|
|
||||||
let _subscriptions = vec![
|
|
||||||
cx.on_blur(&list.focus_handle(cx), window, Self::on_blur),
|
|
||||||
cx.on_blur(&focus_handle, window, Self::on_blur),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut this = Self {
|
|
||||||
focus_handle,
|
|
||||||
list,
|
|
||||||
size: Size::Medium,
|
|
||||||
selected_value: None,
|
|
||||||
open: false,
|
|
||||||
bounds: Bounds::default(),
|
|
||||||
empty: None,
|
|
||||||
_subscriptions,
|
|
||||||
};
|
|
||||||
this.set_selected_index(selected_index, window, cx);
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn empty<E, F>(mut self, f: F) -> Self
|
|
||||||
where
|
|
||||||
E: IntoElement,
|
|
||||||
F: Fn(&Window, &App) -> E + 'static,
|
|
||||||
{
|
|
||||||
self.empty = Some(Box::new(move |window, cx| f(window, cx).into_any_element()));
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_selected_index(
|
|
||||||
&mut self,
|
|
||||||
selected_index: Option<usize>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
self.list.update(cx, |list, cx| {
|
|
||||||
list.set_selected_index(selected_index, window, cx);
|
|
||||||
});
|
|
||||||
self.update_selected_value(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_selected_value(
|
|
||||||
&mut self,
|
|
||||||
selected_value: &<D::Item as DropdownItem>::Value,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) where
|
|
||||||
<<D as DropdownDelegate>::Item as DropdownItem>::Value: PartialEq,
|
|
||||||
{
|
|
||||||
let delegate = self.list.read(cx).delegate();
|
|
||||||
let selected_index = delegate.delegate.position(selected_value);
|
|
||||||
self.set_selected_index(selected_index, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_index(&self, cx: &App) -> Option<usize> {
|
|
||||||
self.list.read(cx).selected_index()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_selected_value(&mut self, _: &Window, cx: &App) {
|
|
||||||
self.selected_value = self
|
|
||||||
.selected_index(cx)
|
|
||||||
.and_then(|ix| self.list.read(cx).delegate().delegate.get(ix))
|
|
||||||
.map(|item| item.value().clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_value(&self) -> Option<&<D::Item as DropdownItem>::Value> {
|
|
||||||
self.selected_value.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn focus(&self, window: &mut Window, cx: &mut App) {
|
|
||||||
self.focus_handle.focus(window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_blur(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// When the dropdown and dropdown menu are both not focused, close the dropdown menu.
|
|
||||||
if self.list.focus_handle(cx).is_focused(window) || self.focus_handle.is_focused(window) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.open = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.open {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list.focus_handle(cx).focus(window, cx);
|
|
||||||
cx.propagate();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.open {
|
|
||||||
self.open = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.list.focus_handle(cx).focus(window, cx);
|
|
||||||
cx.propagate();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn enter(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
// Propagate the event to the parent view, for example to the Modal to support ENTER to confirm.
|
|
||||||
cx.propagate();
|
|
||||||
|
|
||||||
if !self.open {
|
|
||||||
self.open = true;
|
|
||||||
cx.notify();
|
|
||||||
} else {
|
|
||||||
self.list.focus_handle(cx).focus(window, cx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn toggle_menu(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
cx.stop_propagation();
|
|
||||||
|
|
||||||
self.open = !self.open;
|
|
||||||
if self.open {
|
|
||||||
self.list.focus_handle(cx).focus(window, cx);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escape(&mut self, _: &Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
if !self.open {
|
|
||||||
cx.propagate();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.open = false;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clean(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
|
||||||
self.set_selected_index(None, window, cx);
|
|
||||||
cx.emit(DropdownEvent::Confirm(None));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the items for the dropdown.
|
|
||||||
pub fn set_items(&mut self, items: D, _: &mut Window, cx: &mut Context<Self>)
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
self.list.update(cx, |list, _| {
|
|
||||||
list.delegate_mut().delegate = items;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Render for DropdownState<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
Empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Dropdown<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
pub fn new(state: &Entity<DropdownState<D>>) -> Self {
|
|
||||||
Self {
|
|
||||||
id: ("dropdown", state.entity_id()).into(),
|
|
||||||
state: state.clone(),
|
|
||||||
placeholder: None,
|
|
||||||
size: Size::Medium,
|
|
||||||
icon: None,
|
|
||||||
cleanable: false,
|
|
||||||
title_prefix: None,
|
|
||||||
empty: None,
|
|
||||||
width: Length::Auto,
|
|
||||||
menu_width: Length::Auto,
|
|
||||||
disabled: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the width of the dropdown input, default: Length::Auto
|
|
||||||
pub fn width(mut self, width: impl Into<Length>) -> Self {
|
|
||||||
self.width = width.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the width of the dropdown menu, default: Length::Auto
|
|
||||||
pub fn menu_width(mut self, width: impl Into<Length>) -> Self {
|
|
||||||
self.menu_width = width.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the placeholder for display when dropdown value is empty.
|
|
||||||
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
|
||||||
self.placeholder = Some(placeholder.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the right icon for the dropdown input, instead of the default arrow icon.
|
|
||||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
|
||||||
self.icon = Some(icon.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set title prefix for the dropdown.
|
|
||||||
///
|
|
||||||
/// e.g.: Country: United States
|
|
||||||
///
|
|
||||||
/// You should set the label is `Country: `
|
|
||||||
pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
|
|
||||||
self.title_prefix = Some(prefix.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set true to show the clear button when the input field is not empty.
|
|
||||||
pub fn cleanable(mut self) -> Self {
|
|
||||||
self.cleanable = true;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the disable state for the dropdown.
|
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn empty(mut self, el: impl IntoElement) -> Self {
|
|
||||||
self.empty = Some(el.into_any_element());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the title element for the dropdown input.
|
|
||||||
fn display_title(&self, _: &Window, cx: &App) -> impl IntoElement {
|
|
||||||
let default_title = div()
|
|
||||||
.text_color(cx.theme().text_accent)
|
|
||||||
.child(
|
|
||||||
self.placeholder
|
|
||||||
.clone()
|
|
||||||
.unwrap_or_else(|| "Please select".into()),
|
|
||||||
)
|
|
||||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted));
|
|
||||||
|
|
||||||
let Some(selected_index) = &self.state.read(cx).selected_index(cx) else {
|
|
||||||
return default_title;
|
|
||||||
};
|
|
||||||
|
|
||||||
let Some(title) = self
|
|
||||||
.state
|
|
||||||
.read(cx)
|
|
||||||
.list
|
|
||||||
.read(cx)
|
|
||||||
.delegate()
|
|
||||||
.delegate
|
|
||||||
.get(*selected_index)
|
|
||||||
.map(|item| {
|
|
||||||
if let Some(el) = item.display_title() {
|
|
||||||
el
|
|
||||||
} else if let Some(prefix) = self.title_prefix.as_ref() {
|
|
||||||
format!("{}{}", prefix, item.title()).into_any_element()
|
|
||||||
} else {
|
|
||||||
item.title().into_any_element()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
else {
|
|
||||||
return default_title;
|
|
||||||
};
|
|
||||||
|
|
||||||
div()
|
|
||||||
.when(self.disabled, |this| this.text_color(cx.theme().text_muted))
|
|
||||||
.child(title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> Sizable for Dropdown<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
|
||||||
self.size = size.into();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> EventEmitter<DropdownEvent<D>> for DropdownState<D> where D: DropdownDelegate + 'static {}
|
|
||||||
impl<D> EventEmitter<DismissEvent> for DropdownState<D> where D: DropdownDelegate + 'static {}
|
|
||||||
impl<D> Focusable for DropdownState<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate,
|
|
||||||
{
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
if self.open {
|
|
||||||
self.list.focus_handle(cx)
|
|
||||||
} else {
|
|
||||||
self.focus_handle.clone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<D> Focusable for Dropdown<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate,
|
|
||||||
{
|
|
||||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
|
||||||
self.state.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<D> RenderOnce for Dropdown<D>
|
|
||||||
where
|
|
||||||
D: DropdownDelegate + 'static,
|
|
||||||
{
|
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
||||||
let is_focused = self.focus_handle(cx).is_focused(window);
|
|
||||||
// If the size has change, set size to self.list, to change the QueryInput size.
|
|
||||||
let old_size = self.state.read(cx).list.read(cx).size;
|
|
||||||
if old_size != self.size {
|
|
||||||
self.state
|
|
||||||
.read(cx)
|
|
||||||
.list
|
|
||||||
.clone()
|
|
||||||
.update(cx, |this, cx| this.set_size(self.size, window, cx));
|
|
||||||
self.state.update(cx, |this, _| {
|
|
||||||
this.size = self.size;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let state = self.state.read(cx);
|
|
||||||
let show_clean = self.cleanable && state.selected_index(cx).is_some();
|
|
||||||
let bounds = state.bounds;
|
|
||||||
let allow_open = !(state.open || self.disabled);
|
|
||||||
let outline_visible = state.open || is_focused && !self.disabled;
|
|
||||||
let popup_radius = cx.theme().radius.min(px(8.));
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id(self.id.clone())
|
|
||||||
.key_context(CONTEXT)
|
|
||||||
.track_focus(&self.focus_handle(cx))
|
|
||||||
.on_action(window.listener_for(&self.state, DropdownState::up))
|
|
||||||
.on_action(window.listener_for(&self.state, DropdownState::down))
|
|
||||||
.on_action(window.listener_for(&self.state, DropdownState::enter))
|
|
||||||
.on_action(window.listener_for(&self.state, DropdownState::escape))
|
|
||||||
.size_full()
|
|
||||||
.relative()
|
|
||||||
.input_font_size(self.size)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.id(ElementId::Name(format!("{}-input", self.id).into()))
|
|
||||||
.relative()
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.bg(cx.theme().background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.rounded(cx.theme().radius)
|
|
||||||
.when(cx.theme().shadow, |this| this.shadow_sm())
|
|
||||||
.overflow_hidden()
|
|
||||||
.input_font_size(self.size)
|
|
||||||
.map(|this| match self.width {
|
|
||||||
Length::Definite(l) => this.flex_none().w(l),
|
|
||||||
Length::Auto => this.w_full(),
|
|
||||||
})
|
|
||||||
.when(outline_visible, |this| this.border_color(cx.theme().ring))
|
|
||||||
.input_size(self.size)
|
|
||||||
.when(allow_open, |this| {
|
|
||||||
this.on_click(window.listener_for(&self.state, DropdownState::toggle_menu))
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.items_center()
|
|
||||||
.justify_between()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.w_full()
|
|
||||||
.overflow_hidden()
|
|
||||||
.whitespace_nowrap()
|
|
||||||
.truncate()
|
|
||||||
.child(self.display_title(window, cx)),
|
|
||||||
)
|
|
||||||
.when(show_clean, |this| {
|
|
||||||
this.child(clear_button(cx).map(|this| {
|
|
||||||
if self.disabled {
|
|
||||||
this.disabled(true)
|
|
||||||
} else {
|
|
||||||
this.on_click(
|
|
||||||
window.listener_for(&self.state, DropdownState::clean),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.when(!show_clean, |this| {
|
|
||||||
let icon = match self.icon.clone() {
|
|
||||||
Some(icon) => icon,
|
|
||||||
None => {
|
|
||||||
if state.open {
|
|
||||||
Icon::new(IconName::CaretUp)
|
|
||||||
} else {
|
|
||||||
Icon::new(IconName::CaretDown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.child(icon.xsmall().text_color(match self.disabled {
|
|
||||||
true => cx.theme().text_placeholder,
|
|
||||||
false => cx.theme().text_muted,
|
|
||||||
}))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
canvas(
|
|
||||||
{
|
|
||||||
let state = self.state.clone();
|
|
||||||
move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds)
|
|
||||||
},
|
|
||||||
|_, _, _, _| {},
|
|
||||||
)
|
|
||||||
.absolute()
|
|
||||||
.size_full(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when(state.open, |this| {
|
|
||||||
this.child(
|
|
||||||
deferred(
|
|
||||||
anchored().snap_to_window_with_margin(px(8.)).child(
|
|
||||||
div()
|
|
||||||
.occlude()
|
|
||||||
.map(|this| match self.menu_width {
|
|
||||||
Length::Auto => this.w(bounds.size.width),
|
|
||||||
Length::Definite(w) => this.w(w),
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
v_flex()
|
|
||||||
.occlude()
|
|
||||||
.mt_1p5()
|
|
||||||
.bg(cx.theme().background)
|
|
||||||
.border_1()
|
|
||||||
.border_color(cx.theme().border)
|
|
||||||
.rounded(popup_radius)
|
|
||||||
.when(cx.theme().shadow, |this| this.shadow_md())
|
|
||||||
.child(state.list.clone()),
|
|
||||||
)
|
|
||||||
.on_mouse_down_out(window.listener_for(
|
|
||||||
&self.state,
|
|
||||||
|this, _, window, cx| {
|
|
||||||
this.escape(&Cancel, window, cx);
|
|
||||||
},
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with_priority(1),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
297
crates/ui/src/geometry.rs
Normal file
297
crates/ui/src/geometry.rs
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
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 {
|
||||||
|
match self {
|
||||||
|
Placement::Left | Placement::Right => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn is_vertical(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Placement::Top | Placement::Bottom => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
fn is_horizontal(self) -> bool;
|
||||||
|
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;
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
69
crates/ui/src/index_path.rs
Normal file
69
crates/ui/src/index_path.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
pub use anchored::*;
|
||||||
pub use element_ext::ElementExt;
|
pub use element_ext::ElementExt;
|
||||||
pub use event::InteractiveElementExt;
|
pub use event::InteractiveElementExt;
|
||||||
pub use focusable::FocusableCycle;
|
pub use focusable::FocusableCycle;
|
||||||
|
pub use geometry::*;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
|
pub use index_path::IndexPath;
|
||||||
pub use kbd::*;
|
pub use kbd::*;
|
||||||
pub use menu::{context_menu, popup_menu};
|
|
||||||
pub use root::{window_paddings, Root};
|
pub use root::{window_paddings, Root};
|
||||||
pub use styled::*;
|
pub use styled::*;
|
||||||
pub use window_ext::*;
|
pub use window_ext::*;
|
||||||
@@ -16,7 +18,6 @@ pub mod avatar;
|
|||||||
pub mod button;
|
pub mod button;
|
||||||
pub mod checkbox;
|
pub mod checkbox;
|
||||||
pub mod divider;
|
pub mod divider;
|
||||||
pub mod dropdown;
|
|
||||||
pub mod history;
|
pub mod history;
|
||||||
pub mod indicator;
|
pub mod indicator;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
@@ -30,10 +31,13 @@ pub mod skeleton;
|
|||||||
pub mod switch;
|
pub mod switch;
|
||||||
pub mod tooltip;
|
pub mod tooltip;
|
||||||
|
|
||||||
|
mod anchored;
|
||||||
mod element_ext;
|
mod element_ext;
|
||||||
mod event;
|
mod event;
|
||||||
mod focusable;
|
mod focusable;
|
||||||
|
mod geometry;
|
||||||
mod icon;
|
mod icon;
|
||||||
|
mod index_path;
|
||||||
mod kbd;
|
mod kbd;
|
||||||
mod root;
|
mod root;
|
||||||
mod styled;
|
mod styled;
|
||||||
@@ -44,7 +48,6 @@ mod window_ext;
|
|||||||
/// This must be called before using any of the UI components.
|
/// This must be called before using any of the UI components.
|
||||||
/// You can initialize the UI module at your application's entry point.
|
/// You can initialize the UI module at your application's entry point.
|
||||||
pub fn init(cx: &mut gpui::App) {
|
pub fn init(cx: &mut gpui::App) {
|
||||||
dropdown::init(cx);
|
|
||||||
input::init(cx);
|
input::init(cx);
|
||||||
list::init(cx);
|
list::init(cx);
|
||||||
modal::init(cx);
|
modal::init(cx);
|
||||||
|
|||||||
221
crates/ui/src/list/cache.rs
Normal file
221
crates/ui/src/list/cache.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
171
crates/ui/src/list/delegate.rs
Normal file
171
crates/ui/src/list/delegate.rs
Normal 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
@@ -1,39 +1,54 @@
|
|||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
|
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
||||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, Styled,
|
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _,
|
||||||
Window,
|
StyleRefinement, Styled, Window,
|
||||||
};
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use theme::ActiveTheme;
|
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>>;
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
type OnMouseEnter = Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>;
|
enum ListItemMode {
|
||||||
type Suffix = Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>;
|
#[default]
|
||||||
|
Entry,
|
||||||
|
Separator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ListItemMode {
|
||||||
|
#[inline]
|
||||||
|
fn is_separator(&self) -> bool {
|
||||||
|
matches!(self, ListItemMode::Separator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ListItem {
|
pub struct ListItem {
|
||||||
base: Stateful<Div>,
|
base: Stateful<Div>,
|
||||||
|
mode: ListItemMode,
|
||||||
|
style: StyleRefinement,
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
|
secondary_selected: bool,
|
||||||
confirmed: bool,
|
confirmed: bool,
|
||||||
check_icon: Option<Icon>,
|
check_icon: Option<Icon>,
|
||||||
on_click: OnClick,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||||
on_mouse_enter: OnMouseEnter,
|
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
|
||||||
suffix: Suffix,
|
suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem {
|
impl ListItem {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
let id: ElementId = id.into();
|
let id: ElementId = id.into();
|
||||||
|
|
||||||
Self {
|
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,
|
disabled: false,
|
||||||
selected: false,
|
selected: false,
|
||||||
|
secondary_selected: false,
|
||||||
confirmed: false,
|
confirmed: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
on_mouse_enter: None,
|
on_mouse_enter: None,
|
||||||
@@ -43,9 +58,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.
|
/// Set to show check icon, default is None.
|
||||||
pub fn check_icon(mut self, icon: IconName) -> Self {
|
pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||||
self.check_icon = Some(Icon::new(icon));
|
self.check_icon = Some(icon.into());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +132,16 @@ impl Selectable for ListItem {
|
|||||||
fn is_selected(&self) -> bool {
|
fn is_selected(&self) -> bool {
|
||||||
self.selected
|
self.selected
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn secondary_selected(mut self, selected: bool) -> Self {
|
||||||
|
self.secondary_selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Styled for ListItem {
|
impl Styled for ListItem {
|
||||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||||
self.base.style()
|
&mut self.style
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,35 +153,37 @@ impl ParentElement for ListItem {
|
|||||||
|
|
||||||
impl RenderOnce for ListItem {
|
impl RenderOnce for ListItem {
|
||||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
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 mut selected_style = StyleRefinement::default();
|
||||||
|
selected_style.corner_radii = corner_radii;
|
||||||
|
|
||||||
|
let is_selectable = !(self.disabled || self.mode.is_separator());
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
|
.relative()
|
||||||
|
.gap_x_1()
|
||||||
|
.py_1()
|
||||||
|
.px_3()
|
||||||
|
.text_base()
|
||||||
.text_color(cx.theme().text)
|
.text_color(cx.theme().text)
|
||||||
.relative()
|
.relative()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.when_some(self.on_click, |this, on_click| {
|
.refine_style(&self.style)
|
||||||
if !self.disabled {
|
.when(is_selectable, |this| {
|
||||||
this.cursor_pointer()
|
this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
|
||||||
.on_mouse_down(MouseButton::Left, move |_, _window, cx| {
|
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||||
cx.stop_propagation();
|
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
|
||||||
})
|
})
|
||||||
.on_click(on_click)
|
.when(!is_active, |this| {
|
||||||
} else {
|
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||||
this
|
})
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.when(is_active, |this| this.bg(cx.theme().element_active))
|
.when(!is_selectable, |this| {
|
||||||
.when(!is_active && !self.disabled, |this| {
|
this.text_color(cx.theme().text_muted)
|
||||||
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
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
@@ -177,5 +205,17 @@ impl RenderOnce for ListItem {
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
|
.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
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ impl RenderOnce for LoadingItem {
|
|||||||
.gap_1p5()
|
.gap_1p5()
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(Skeleton::new().h_5().w_48().max_w_full())
|
.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()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,27 @@
|
|||||||
#[allow(clippy::module_inception)]
|
pub(crate) mod cache;
|
||||||
|
mod delegate;
|
||||||
mod list;
|
mod list;
|
||||||
mod list_item;
|
mod list_item;
|
||||||
mod loading;
|
mod loading;
|
||||||
|
mod separator_item;
|
||||||
|
|
||||||
|
pub use delegate::*;
|
||||||
pub use list::*;
|
pub use list::*;
|
||||||
pub use list_item::*;
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
44
crates/ui/src/list/separator_item.rs
Normal file
44
crates/ui/src/list/separator_item.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, px, App, AppContext as _, ClickEvent, Context, DismissEvent, Entity,
|
anchored, deferred, div, px, App, AppContext as _, ClickEvent, Context, DismissEvent, Entity,
|
||||||
Focusable, InteractiveElement as _, IntoElement, KeyBinding, OwnedMenu, ParentElement, Render,
|
Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu,
|
||||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::actions::{Cancel, SelectLeft, SelectRight};
|
use crate::actions::{Cancel, SelectLeft, SelectRight};
|
||||||
use crate::button::{Button, ButtonVariants};
|
use crate::button::{Button, ButtonVariants};
|
||||||
use crate::popup_menu::PopupMenu;
|
use crate::menu::PopupMenu;
|
||||||
use crate::{h_flex, Selectable, Sizable};
|
use crate::{h_flex, Selectable, Sizable};
|
||||||
|
|
||||||
const CONTEXT: &str = "AppMenuBar";
|
const CONTEXT: &str = "AppMenuBar";
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
cx.bind_keys([
|
cx.bind_keys([
|
||||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||||
@@ -22,67 +23,74 @@ pub fn init(cx: &mut App) {
|
|||||||
/// The application menu bar, for Windows and Linux.
|
/// The application menu bar, for Windows and Linux.
|
||||||
pub struct AppMenuBar {
|
pub struct AppMenuBar {
|
||||||
menus: Vec<Entity<AppMenu>>,
|
menus: Vec<Entity<AppMenu>>,
|
||||||
selected_ix: Option<usize>,
|
selected_index: Option<usize>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppMenuBar {
|
impl AppMenuBar {
|
||||||
/// Create a new app menu bar.
|
/// 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| {
|
cx.new(|cx| {
|
||||||
let menu_bar = cx.entity();
|
let mut this = Self {
|
||||||
let menus = cx
|
selected_index: None,
|
||||||
.get_menus()
|
menus: Vec::new(),
|
||||||
.unwrap_or_default()
|
};
|
||||||
.iter()
|
this.reload(cx);
|
||||||
.enumerate()
|
this
|
||||||
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), window, cx))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
selected_ix: None,
|
|
||||||
menus,
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
|
/// Reload the menus from the app.
|
||||||
let Some(selected_ix) = self.selected_ix else {
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_ix = if selected_ix == 0 {
|
let new_ix = if selected_index == 0 {
|
||||||
self.menus.len().saturating_sub(1)
|
self.menus.len().saturating_sub(1)
|
||||||
} else {
|
} 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>) {
|
fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(selected_ix) = self.selected_ix else {
|
let Some(selected_index) = self.selected_index else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_ix = if selected_ix + 1 >= self.menus.len() {
|
let new_ix = if selected_index + 1 >= self.menus.len() {
|
||||||
0
|
0
|
||||||
} else {
|
} 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>) {
|
fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.set_selected_ix(None, window, cx);
|
self.set_selected_index(None, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_selected_ix(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
|
fn set_selected_index(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.selected_ix = ix;
|
self.selected_index = ix;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn has_activated_menu(&self) -> bool {
|
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()
|
h_flex()
|
||||||
.id("app-menu-bar")
|
.id("app-menu-bar")
|
||||||
.key_context(CONTEXT)
|
.key_context(CONTEXT)
|
||||||
.on_action(cx.listener(Self::move_left))
|
.on_action(cx.listener(Self::on_move_left))
|
||||||
.on_action(cx.listener(Self::move_right))
|
.on_action(cx.listener(Self::on_move_right))
|
||||||
.on_action(cx.listener(Self::cancel))
|
.on_action(cx.listener(Self::on_cancel))
|
||||||
.size_full()
|
.size_full()
|
||||||
.gap_x_1()
|
.gap_x_1()
|
||||||
.overflow_x_scroll()
|
.overflow_x_scroll()
|
||||||
@@ -117,7 +125,6 @@ impl AppMenu {
|
|||||||
ix: usize,
|
ix: usize,
|
||||||
menu: &OwnedMenu,
|
menu: &OwnedMenu,
|
||||||
menu_bar: Entity<AppMenuBar>,
|
menu_bar: Entity<AppMenuBar>,
|
||||||
_: &mut Window,
|
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Entity<Self> {
|
) -> Entity<Self> {
|
||||||
let name = menu.name.clone();
|
let name = menu.name.clone();
|
||||||
@@ -173,7 +180,7 @@ impl AppMenu {
|
|||||||
self._subscription.take();
|
self._subscription.take();
|
||||||
self.popup_menu.take();
|
self.popup_menu.take();
|
||||||
self.menu_bar.update(cx, |state, cx| {
|
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,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
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| {
|
_ = self.menu_bar.update(cx, |state, cx| {
|
||||||
let new_ix = if is_selected { None } else { Some(self.ix) };
|
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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +208,8 @@ impl AppMenu {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.menu_bar.update(cx, |state, cx| {
|
_ = 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 {
|
impl Render for AppMenu {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let menu_bar = self.menu_bar.read(cx);
|
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()
|
div()
|
||||||
.id(self.ix)
|
.id(self.ix)
|
||||||
@@ -219,10 +226,15 @@ impl Render for AppMenu {
|
|||||||
Button::new("menu")
|
Button::new("menu")
|
||||||
.small()
|
.small()
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.xsmall()
|
.compact()
|
||||||
.ghost()
|
.ghost()
|
||||||
.label(self.name.clone())
|
.label(self.name.clone())
|
||||||
.selected(is_selected)
|
.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_click(cx.listener(Self::handle_trigger_click)),
|
||||||
)
|
)
|
||||||
.on_hover(cx.listener(Self::handle_hover))
|
.on_hover(cx.listener(Self::handle_hover))
|
||||||
|
|||||||
@@ -3,49 +3,65 @@ use std::rc::Rc;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
anchored, deferred, div, px, relative, AnyElement, App, Context, Corner, DismissEvent, Element,
|
anchored, deferred, div, px, AnyElement, App, Context, Corner, DismissEvent, Element,
|
||||||
ElementId, Entity, Focusable, GlobalElementId, InspectorElementId, InteractiveElement,
|
ElementId, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
|
||||||
IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Position, Stateful,
|
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||||
Style, Subscription, Window,
|
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(
|
fn context_menu(
|
||||||
self,
|
self,
|
||||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||||
) -> Self {
|
) -> ContextMenu<Self>
|
||||||
self.child(ContextMenu::new("context-menu").menu(f))
|
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.
|
/// A context menu that can be shown on right-click.
|
||||||
#[allow(clippy::type_complexity)]
|
pub struct ContextMenu<E: ParentElement + Styled + Sized> {
|
||||||
pub struct ContextMenu {
|
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
menu:
|
element: Option<E>,
|
||||||
Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
|
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,
|
anchor: Corner,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextMenu {
|
impl<E: ParentElement + Styled> ContextMenu<E> {
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
/// Create a new context menu with the given ID.
|
||||||
|
pub fn new(id: impl Into<ElementId>, element: E) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
|
element: Some(element),
|
||||||
menu: None,
|
menu: None,
|
||||||
anchor: Corner::TopLeft,
|
anchor: Corner::TopLeft,
|
||||||
|
_ignore_style: StyleRefinement::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the context menu using the given builder function.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn menu<F>(mut self, builder: F) -> Self
|
fn menu<F>(mut self, builder: F) -> Self
|
||||||
where
|
where
|
||||||
F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||||
{
|
{
|
||||||
self.menu = Some(Box::new(builder));
|
self.menu = Some(Rc::new(builder));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,7 +83,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;
|
type Element = Self;
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
fn into_element(self) -> Self::Element {
|
||||||
@@ -83,14 +117,14 @@ struct ContextMenuSharedState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct ContextMenuState {
|
pub struct ContextMenuState {
|
||||||
menu_element: Option<AnyElement>,
|
element: Option<AnyElement>,
|
||||||
shared_state: Rc<RefCell<ContextMenuSharedState>>,
|
shared_state: Rc<RefCell<ContextMenuSharedState>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ContextMenuState {
|
impl Default for ContextMenuState {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
menu_element: None,
|
element: None,
|
||||||
shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
|
shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
|
||||||
menu_view: None,
|
menu_view: None,
|
||||||
open: false,
|
open: false,
|
||||||
@@ -101,8 +135,8 @@ impl Default for ContextMenuState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for ContextMenu {
|
impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<E> {
|
||||||
type PrepaintState = ();
|
type PrepaintState = Hitbox;
|
||||||
type RequestLayoutState = ContextMenuState;
|
type RequestLayoutState = ContextMenuState;
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
fn id(&self) -> Option<ElementId> {
|
||||||
@@ -113,7 +147,6 @@ impl Element for ContextMenu {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::field_reassign_with_default)]
|
|
||||||
fn request_layout(
|
fn request_layout(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: Option<&gpui::GlobalElementId>,
|
id: Option<&gpui::GlobalElementId>,
|
||||||
@@ -121,71 +154,73 @@ impl Element for ContextMenu {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
) -> (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;
|
let anchor = self.anchor;
|
||||||
|
|
||||||
self.with_element_state(
|
self.with_element_state(
|
||||||
id.unwrap(),
|
id.unwrap(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|_, state: &mut ContextMenuState, window, cx| {
|
|this, state: &mut ContextMenuState, window, cx| {
|
||||||
let (position, open) = {
|
let (position, open) = {
|
||||||
let shared_state = state.shared_state.borrow();
|
let shared_state = state.shared_state.borrow();
|
||||||
(shared_state.position, shared_state.open)
|
(shared_state.position, shared_state.open)
|
||||||
};
|
};
|
||||||
let menu_view = state.shared_state.borrow().menu_view.clone();
|
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
|
let has_menu_item = menu_view
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|menu| !menu.read(cx).is_empty())
|
.map(|menu| !menu.read(cx).is_empty())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|
||||||
if has_menu_item {
|
if has_menu_item {
|
||||||
let mut menu_element = deferred(
|
menu_element = Some(
|
||||||
anchored()
|
deferred(
|
||||||
.position(position)
|
anchored().child(
|
||||||
.snap_to_window_with_margin(px(8.))
|
div()
|
||||||
.anchor(anchor)
|
.w(window.bounds().size.width)
|
||||||
.when_some(menu_view, |this, menu| {
|
.h(window.bounds().size.height)
|
||||||
// Focus the menu, so that can be handle the action.
|
.on_scroll_wheel(|_, _, cx| {
|
||||||
if !menu.focus_handle(cx).contains_focused(window, cx) {
|
cx.stop_propagation();
|
||||||
menu.focus_handle(cx).focus(window, cx);
|
})
|
||||||
}
|
.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()))
|
this.child(menu.clone())
|
||||||
}),
|
}),
|
||||||
)
|
),
|
||||||
.with_priority(1)
|
),
|
||||||
.into_any();
|
)
|
||||||
|
.with_priority(1)
|
||||||
let menu_layout_id = menu_element.request_layout(window, cx);
|
.into_any(),
|
||||||
(Some(menu_element), Some(menu_layout_id))
|
);
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
}
|
}
|
||||||
} 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,
|
layout_id,
|
||||||
ContextMenuState {
|
ContextMenuState {
|
||||||
menu_element,
|
element: Some(element),
|
||||||
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -197,33 +232,33 @@ impl Element for ContextMenu {
|
|||||||
&mut self,
|
&mut self,
|
||||||
_: Option<&gpui::GlobalElementId>,
|
_: Option<&gpui::GlobalElementId>,
|
||||||
_: Option<&InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
_: gpui::Bounds<gpui::Pixels>,
|
bounds: gpui::Bounds<gpui::Pixels>,
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self::PrepaintState {
|
) -> Self::PrepaintState {
|
||||||
if let Some(menu_element) = &mut request_layout.menu_element {
|
if let Some(element) = &mut request_layout.element {
|
||||||
menu_element.prepaint(window, cx);
|
element.prepaint(window, cx);
|
||||||
}
|
}
|
||||||
|
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: Option<&gpui::GlobalElementId>,
|
id: Option<&gpui::GlobalElementId>,
|
||||||
_: Option<&InspectorElementId>,
|
_: Option<&InspectorElementId>,
|
||||||
bounds: gpui::Bounds<gpui::Pixels>,
|
_: gpui::Bounds<gpui::Pixels>,
|
||||||
request_layout: &mut Self::RequestLayoutState,
|
request_layout: &mut Self::RequestLayoutState,
|
||||||
_: &mut Self::PrepaintState,
|
hitbox: &mut Self::PrepaintState,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
if let Some(menu_element) = &mut request_layout.menu_element {
|
if let Some(element) = &mut request_layout.element {
|
||||||
menu_element.paint(window, cx);
|
element.paint(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(builder) = self.menu.take() else {
|
// Take the builder before setting up element state to avoid borrow issues
|
||||||
return;
|
let builder = self.menu.clone();
|
||||||
};
|
|
||||||
|
|
||||||
self.with_element_state(
|
self.with_element_state(
|
||||||
id.unwrap(),
|
id.unwrap(),
|
||||||
@@ -232,33 +267,53 @@ impl Element for ContextMenu {
|
|||||||
|_view, state: &mut ContextMenuState, window, _| {
|
|_view, state: &mut ContextMenuState, window, _| {
|
||||||
let shared_state = state.shared_state.clone();
|
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.
|
// 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| {
|
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||||
if phase.bubble()
|
if phase.bubble()
|
||||||
&& event.button == MouseButton::Right
|
&& event.button == MouseButton::Right
|
||||||
&& bounds.contains(&event.position)
|
&& hitbox.is_hovered(window)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
let mut shared_state = shared_state.borrow_mut();
|
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.position = event.position;
|
||||||
shared_state.open = true;
|
shared_state.open = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let menu = PopupMenu::build(window, cx, |menu, window, cx| {
|
// Use defer to build the menu in the next frame, avoiding race conditions
|
||||||
(builder)(menu, window, cx)
|
window.defer(cx, {
|
||||||
});
|
|
||||||
|
|
||||||
let _subscription = window.subscribe(&menu, cx, {
|
|
||||||
let shared_state = shared_state.clone();
|
let shared_state = shared_state.clone();
|
||||||
move |_, _: &DismissEvent, window, _| {
|
let builder = builder.clone();
|
||||||
shared_state.borrow_mut().open = false;
|
move |window, cx| {
|
||||||
window.refresh();
|
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();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
141
crates/ui/src/menu/dropdown_menu.rs
Normal file
141
crates/ui/src/menu/dropdown_menu.rs
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
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,
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,6 @@ use theme::ActiveTheme;
|
|||||||
use crate::{h_flex, Disableable, StyledExt};
|
use crate::{h_flex, Disableable, StyledExt};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
#[allow(clippy::type_complexity)]
|
|
||||||
pub(crate) struct MenuItemElement {
|
pub(crate) struct MenuItemElement {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
group_name: SharedString,
|
group_name: SharedString,
|
||||||
@@ -23,7 +22,8 @@ pub(crate) struct MenuItemElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MenuItemElement {
|
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();
|
let id: ElementId = id.into();
|
||||||
Self {
|
Self {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
@@ -38,17 +38,19 @@ impl MenuItemElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Set ListItem as the selected item style.
|
/// 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.selected = selected;
|
||||||
self
|
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.disabled = disabled;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(
|
/// Set a handler for when the MenuItem is clicked.
|
||||||
|
pub(crate) fn on_click(
|
||||||
mut self,
|
mut self,
|
||||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -88,7 +90,7 @@ impl RenderOnce for MenuItemElement {
|
|||||||
h_flex()
|
h_flex()
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
.group(&self.group_name)
|
.group(&self.group_name)
|
||||||
.gap_x_2()
|
.gap_x_1()
|
||||||
.py_1()
|
.py_1()
|
||||||
.px_2()
|
.px_2()
|
||||||
.text_base()
|
.text_base()
|
||||||
@@ -102,12 +104,12 @@ impl RenderOnce for MenuItemElement {
|
|||||||
})
|
})
|
||||||
.when(!self.disabled, |this| {
|
.when(!self.disabled, |this| {
|
||||||
this.group_hover(self.group_name, |this| {
|
this.group_hover(self.group_name, |this| {
|
||||||
this.bg(cx.theme().elevated_surface_background)
|
this.bg(cx.theme().element_background)
|
||||||
.text_color(cx.theme().text)
|
.text_color(cx.theme().element_foreground)
|
||||||
})
|
})
|
||||||
.when(self.selected, |this| {
|
.when(self.selected, |this| {
|
||||||
this.bg(cx.theme().elevated_surface_background)
|
this.bg(cx.theme().element_background)
|
||||||
.text_color(cx.theme().text)
|
.text_color(cx.theme().element_foreground)
|
||||||
})
|
})
|
||||||
.when_some(self.on_click, |this, on_click| {
|
.when_some(self.on_click, |this, on_click| {
|
||||||
this.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
this.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
use gpui::App;
|
use gpui::App;
|
||||||
|
|
||||||
mod app_menu_bar;
|
mod app_menu_bar;
|
||||||
|
mod context_menu;
|
||||||
|
mod dropdown_menu;
|
||||||
mod menu_item;
|
mod menu_item;
|
||||||
|
mod popup_menu;
|
||||||
pub mod context_menu;
|
|
||||||
pub mod popup_menu;
|
|
||||||
|
|
||||||
pub use app_menu_bar::AppMenuBar;
|
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) {
|
pub(crate) fn init(cx: &mut App) {
|
||||||
app_menu_bar::init(cx);
|
app_menu_bar::init(cx);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use gpui::prelude::FluentBuilder;
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
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,
|
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
||||||
Window,
|
Window,
|
||||||
@@ -13,6 +13,7 @@ use theme::ActiveTheme;
|
|||||||
use crate::actions::{Cancel, Confirm};
|
use crate::actions::{Cancel, Confirm};
|
||||||
use crate::animation::cubic_bezier;
|
use crate::animation::cubic_bezier;
|
||||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||||
|
use crate::scroll::ScrollableElement;
|
||||||
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
|
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
|
||||||
|
|
||||||
const CONTEXT: &str = "Modal";
|
const CONTEXT: &str = "Modal";
|
||||||
@@ -489,13 +490,13 @@ impl RenderOnce for Modal {
|
|||||||
.w_full()
|
.w_full()
|
||||||
.h_auto()
|
.h_auto()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.relative()
|
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.pr(padding_right)
|
.pr(padding_right)
|
||||||
.pl(padding_left)
|
.pl(padding_left)
|
||||||
.scrollable(Axis::Vertical)
|
.size_full()
|
||||||
|
.overflow_y_scrollbar()
|
||||||
.child(self.content),
|
.child(self.content),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,129 +1,75 @@
|
|||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::prelude::FluentBuilder as _;
|
use gpui::prelude::FluentBuilder as _;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, anchored, deferred, div, px, AnyElement, App, Bounds, Context, Corner, DismissEvent,
|
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
|
||||||
DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
|
||||||
GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding,
|
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
|
||||||
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
|
Styled, Subscription, Window,
|
||||||
ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{Selectable, StyledExt as _};
|
use crate::actions::Cancel;
|
||||||
|
use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _};
|
||||||
|
|
||||||
const CONTEXT: &str = "Popover";
|
const CONTEXT: &str = "Popover";
|
||||||
|
|
||||||
actions!(popover, [Escape]);
|
pub(crate) fn init(cx: &mut App) {
|
||||||
|
cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
|
||||||
pub fn init(cx: &mut App) {
|
|
||||||
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PopoverChild<T> = Rc<dyn Fn(&mut Window, &mut Context<T>) -> AnyElement>;
|
/// A popover element that can be triggered by a button or any other element.
|
||||||
|
#[derive(IntoElement)]
|
||||||
pub struct PopoverContent {
|
pub struct Popover {
|
||||||
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> {
|
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
anchor: Corner,
|
style: StyleRefinement,
|
||||||
trigger: Trigger,
|
anchor: Anchor,
|
||||||
content: Content<M>,
|
default_open: bool,
|
||||||
|
open: Option<bool>,
|
||||||
|
tracked_focus_handle: Option<FocusHandle>,
|
||||||
|
trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
|
||||||
|
content: Option<
|
||||||
|
Rc<
|
||||||
|
dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
|
||||||
|
+ 'static,
|
||||||
|
>,
|
||||||
|
>,
|
||||||
|
children: Vec<AnyElement>,
|
||||||
/// Style for trigger element.
|
/// Style for trigger element.
|
||||||
/// This is used for hotfix the trigger element style to support w_full.
|
/// This is used for hotfix the trigger element style to support w_full.
|
||||||
trigger_style: Option<StyleRefinement>,
|
trigger_style: Option<StyleRefinement>,
|
||||||
mouse_button: MouseButton,
|
mouse_button: MouseButton,
|
||||||
no_style: bool,
|
appearance: bool,
|
||||||
|
overlay_closable: bool,
|
||||||
|
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> Popover<M>
|
impl Popover {
|
||||||
where
|
|
||||||
M: ManagedView,
|
|
||||||
{
|
|
||||||
/// Create a new Popover with `view` mode.
|
/// Create a new Popover with `view` mode.
|
||||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
anchor: Corner::TopLeft,
|
style: StyleRefinement::default(),
|
||||||
|
anchor: Anchor::TopLeft,
|
||||||
trigger: None,
|
trigger: None,
|
||||||
trigger_style: None,
|
trigger_style: None,
|
||||||
content: None,
|
content: None,
|
||||||
|
tracked_focus_handle: None,
|
||||||
|
children: vec![],
|
||||||
mouse_button: MouseButton::Left,
|
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 {
|
/// Set the anchor corner of the popover, default is `Corner::TopLeft`.
|
||||||
self.anchor = anchor;
|
///
|
||||||
|
/// 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,29 +79,75 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the trigger element of the popover.
|
||||||
pub fn trigger<T>(mut self, trigger: T) -> Self
|
pub fn trigger<T>(mut self, trigger: T) -> Self
|
||||||
where
|
where
|
||||||
T: Selectable + IntoElement + 'static,
|
T: Selectable + IntoElement + 'static,
|
||||||
{
|
{
|
||||||
self.trigger = Some(Box::new(|is_open, _, _| {
|
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
|
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 {
|
pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||||
self.trigger_style = Some(style);
|
self.trigger_style = Some(style);
|
||||||
self
|
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`.
|
/// This callback will called every time on render the popover.
|
||||||
pub fn content<C>(mut self, content: C) -> Self
|
/// So, you should avoid creating new elements or entities in the content closure.
|
||||||
|
pub fn content<F, E>(mut self, content: F) -> Self
|
||||||
where
|
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,302 +157,264 @@ where
|
|||||||
///
|
///
|
||||||
/// - The popover will not have a bg, border, shadow, or padding.
|
/// - The popover will not have a bg, border, shadow, or padding.
|
||||||
/// - The click out of the popover will not dismiss it.
|
/// - The click out of the popover will not dismiss it.
|
||||||
pub fn no_style(mut self) -> Self {
|
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||||
self.no_style = true;
|
self.appearance = appearance;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_trigger(&mut self, is_open: bool, window: &mut Window, cx: &mut App) -> AnyElement {
|
/// Bind the focus handle to receive focus when the popover is opened.
|
||||||
let Some(trigger) = self.trigger.take() else {
|
/// If you not set this, a new focus handle will be created for the popover to
|
||||||
return div().into_any_element();
|
///
|
||||||
|
/// 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)
|
trigger_bounds.corner(anchor.swap_vertical().into())
|
||||||
}
|
+ offset
|
||||||
|
+ Point {
|
||||||
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
x: px(0.),
|
||||||
bounds.corner(match self.anchor {
|
y: -trigger_bounds.size.height,
|
||||||
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))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<M> IntoElement for Popover<M>
|
impl ParentElement for Popover {
|
||||||
where
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||||
M: ManagedView,
|
self.children.extend(elements);
|
||||||
{
|
|
||||||
type Element = Self;
|
|
||||||
|
|
||||||
fn into_element(self) -> Self::Element {
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PopoverElementState<M> {
|
impl Styled for Popover {
|
||||||
trigger_layout_id: Option<LayoutId>,
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
popover_layout_id: Option<LayoutId>,
|
&mut self.style
|
||||||
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<M> Default for PopoverElementState<M> {
|
pub struct PopoverState {
|
||||||
fn default() -> Self {
|
focus_handle: FocusHandle,
|
||||||
|
pub(crate) tracked_focus_handle: Option<FocusHandle>,
|
||||||
|
trigger_bounds: Bounds<Pixels>,
|
||||||
|
open: bool,
|
||||||
|
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 {
|
Self {
|
||||||
trigger_layout_id: None,
|
focus_handle: cx.focus_handle(),
|
||||||
popover_layout_id: None,
|
tracked_focus_handle: None,
|
||||||
popover_element: None,
|
trigger_bounds: Bounds::default(),
|
||||||
trigger_element: None,
|
open: default_open,
|
||||||
content_view: Rc::new(RefCell::new(None)),
|
on_open_change: None,
|
||||||
trigger_bounds: None,
|
_dismiss_subscription: 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,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
/// Check if the popover is open.
|
||||||
&mut self,
|
pub fn is_open(&self) -> bool {
|
||||||
id: Option<&GlobalElementId>,
|
self.open
|
||||||
_: 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;
|
|
||||||
|
|
||||||
if let Some(mut element) = request_layout.trigger_element.take() {
|
/// Dismiss the popover if it is open.
|
||||||
element.paint(window, cx);
|
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() {
|
/// Open the popover if it is closed.
|
||||||
element.paint(window, cx);
|
pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
return;
|
if !self.open {
|
||||||
}
|
self.toggle_open(window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When mouse click down in the trigger bounds, open the popover.
|
fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
let Some(content_build) = this.content.take() else {
|
self.open = !self.open;
|
||||||
return;
|
if self.open {
|
||||||
};
|
let state = cx.entity();
|
||||||
let old_content_view = element_state.content_view.clone();
|
let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
|
||||||
let hitbox_id = prepaint.hitbox.id;
|
{
|
||||||
let mouse_button = this.mouse_button;
|
tracked_focus_handle
|
||||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
} else {
|
||||||
if phase == DispatchPhase::Bubble
|
self.focus_handle.clone()
|
||||||
&& event.button == mouse_button
|
};
|
||||||
&& hitbox_id.is_hovered(window)
|
focus_handle.focus(window, cx);
|
||||||
{
|
|
||||||
cx.stop_propagation();
|
|
||||||
window.prevent_default();
|
|
||||||
|
|
||||||
let new_content_view = (content_build)(window, cx);
|
self._dismiss_subscription =
|
||||||
let old_content_view1 = old_content_view.clone();
|
Some(
|
||||||
|
window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
|
||||||
let previous_focus_handle = window.focused(cx);
|
state.update(cx, |state, cx| {
|
||||||
|
state.dismiss(window, 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);
|
|
||||||
window.refresh();
|
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,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,232 +1,209 @@
|
|||||||
|
use std::panic::Location;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::prelude::FluentBuilder;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, relative, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId,
|
div, App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||||
InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement,
|
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||||
Pixels, Position, ScrollHandle, SharedString, Size, Stateful, StatefulInteractiveElement,
|
|
||||||
Style, 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.
|
/// A trait for elements that can be made scrollable with scrollbars.
|
||||||
pub struct Scrollable<E> {
|
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,
|
id: ElementId,
|
||||||
element: Option<E>,
|
element: E,
|
||||||
axis: ScrollbarAxis,
|
axis: ScrollbarAxis,
|
||||||
/// This is a fake element to handle Styled, InteractiveElement, not used.
|
|
||||||
_element: Stateful<Div>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> Scrollable<E>
|
impl<E> Scrollable<E>
|
||||||
where
|
where
|
||||||
E: Element,
|
E: InteractiveElement + Styled + ParentElement + Element,
|
||||||
{
|
{
|
||||||
pub(crate) fn new(axis: impl Into<ScrollbarAxis>, element: E) -> Self {
|
#[track_caller]
|
||||||
let id = ElementId::Name(SharedString::from(
|
fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
|
||||||
format!("scrollable-{:?}", element.id(),),
|
let caller = Location::caller();
|
||||||
));
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
element: Some(element),
|
id: ElementId::CodeLocation(*caller),
|
||||||
_element: div().id("fake"),
|
element,
|
||||||
id,
|
|
||||||
axis: axis.into(),
|
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>
|
impl<E> Styled for Scrollable<E>
|
||||||
where
|
where
|
||||||
E: Element + Styled,
|
E: InteractiveElement + Styled + ParentElement + Element,
|
||||||
{
|
{
|
||||||
fn style(&mut self) -> &mut StyleRefinement {
|
fn style(&mut self) -> &mut StyleRefinement {
|
||||||
if let Some(element) = &mut self.element {
|
self.element.style()
|
||||||
element.style()
|
|
||||||
} else {
|
|
||||||
self._element.style()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<E> InteractiveElement for Scrollable<E>
|
impl<E> ParentElement for Scrollable<E>
|
||||||
where
|
where
|
||||||
E: Element + InteractiveElement,
|
E: InteractiveElement + Styled + ParentElement + Element,
|
||||||
{
|
{
|
||||||
fn interactivity(&mut self) -> &mut Interactivity {
|
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||||
if let Some(element) = &mut self.element {
|
self.element.extend(elements)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
where
|
||||||
E: Element,
|
E: InteractiveElement + Styled + ParentElement + Element + 'static,
|
||||||
{
|
{
|
||||||
type PrepaintState = ScrollViewState;
|
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||||
type RequestLayoutState = AnyElement;
|
let scroll_handle = window
|
||||||
|
.use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
|
||||||
|
.read(cx)
|
||||||
|
.clone();
|
||||||
|
|
||||||
fn id(&self) -> Option<ElementId> {
|
// Inherit the size from the element style.
|
||||||
Some(self.id.clone())
|
let style = StyleRefinement {
|
||||||
}
|
size: self.element.style().size.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(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let axis = self.axis;
|
div()
|
||||||
let scroll_id = self.id.clone();
|
.id(self.id)
|
||||||
let content = self.element.take().map(|c| c.into_any_element());
|
.size_full()
|
||||||
|
.refine_style(&style)
|
||||||
self.with_element_state(id.unwrap(), window, cx, |_, element_state, window, cx| {
|
.relative()
|
||||||
let mut element = div()
|
.child(
|
||||||
.relative()
|
div()
|
||||||
.size_full()
|
.id("scroll-area")
|
||||||
.overflow_hidden()
|
.flex()
|
||||||
.child(
|
.size_full()
|
||||||
div()
|
.track_scroll(&scroll_handle)
|
||||||
.id(scroll_id)
|
.map(|this| match self.axis {
|
||||||
.track_scroll(&element_state.handle)
|
ScrollbarAxis::Vertical => this.flex_col().overflow_y_scroll(),
|
||||||
.overflow_scroll()
|
ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
|
||||||
.relative()
|
ScrollbarAxis::Both => this.overflow_scroll(),
|
||||||
.size_full()
|
})
|
||||||
.child(div().children(content)),
|
.child(
|
||||||
)
|
self.element
|
||||||
.child(
|
// Refine element size to `flex_1`.
|
||||||
div()
|
.size_auto()
|
||||||
.absolute()
|
.flex_1(),
|
||||||
.top_0()
|
),
|
||||||
.left_0()
|
)
|
||||||
.right_0()
|
.child(render_scrollbar(
|
||||||
.bottom_0()
|
"scrollbar",
|
||||||
.child(
|
&scroll_handle,
|
||||||
Scrollbar::both(&element_state.state, &element_state.handle).axis(axis),
|
self.axis,
|
||||||
),
|
window,
|
||||||
)
|
cx,
|
||||||
.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,43 +1,50 @@
|
|||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
|
use std::panic::Location;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
||||||
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
|
CursorStyle, Edges, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
|
||||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
InspectorElementId, IntoElement, IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent,
|
||||||
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
MouseUpEvent, PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style,
|
||||||
|
UniformListScrollHandle, Window,
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::{ActiveTheme, ScrollbarMode};
|
||||||
|
|
||||||
use crate::AxisExt;
|
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(4. * 2. + 8.);
|
||||||
const MIN_THUMB_SIZE: f32 = 48.;
|
const MIN_THUMB_SIZE: f32 = 48.;
|
||||||
|
|
||||||
const THUMB_WIDTH: Pixels = px(6.);
|
const THUMB_WIDTH: Pixels = px(6.);
|
||||||
const THUMB_RADIUS: Pixels = px(6. / 2.);
|
const THUMB_RADIUS: Pixels = px(6. / 2.);
|
||||||
const THUMB_INSET: Pixels = px(2.);
|
const THUMB_INSET: Pixels = px(4.);
|
||||||
|
|
||||||
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
|
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
|
||||||
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
|
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
|
||||||
const THUMB_ACTIVE_INSET: Pixels = px(2.);
|
const THUMB_ACTIVE_INSET: Pixels = px(4.);
|
||||||
|
|
||||||
const FADE_OUT_DURATION: f32 = 3.0;
|
const FADE_OUT_DURATION: f32 = 3.0;
|
||||||
const FADE_OUT_DELAY: f32 = 2.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>;
|
fn offset(&self) -> Point<Pixels>;
|
||||||
|
/// Set the offset of the scroll handle.
|
||||||
fn set_offset(&self, offset: Point<Pixels>);
|
fn set_offset(&self, offset: Point<Pixels>);
|
||||||
fn is_uniform_list(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
/// The full size of the content, including padding.
|
/// The full size of the content, including padding.
|
||||||
fn content_size(&self) -> Size<Pixels>;
|
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> {
|
fn offset(&self) -> Point<Pixels> {
|
||||||
self.offset()
|
self.offset()
|
||||||
}
|
}
|
||||||
@@ -51,7 +58,7 @@ impl ScrollHandleOffsetable for ScrollHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScrollHandleOffsetable for UniformListScrollHandle {
|
impl ScrollbarHandle for UniformListScrollHandle {
|
||||||
fn offset(&self) -> Point<Pixels> {
|
fn offset(&self) -> Point<Pixels> {
|
||||||
self.0.borrow().base_handle.offset()
|
self.0.borrow().base_handle.offset()
|
||||||
}
|
}
|
||||||
@@ -60,21 +67,41 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
|
|||||||
self.0.borrow_mut().base_handle.set_offset(offset)
|
self.0.borrow_mut().base_handle.set_offset(offset)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_uniform_list(&self) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn content_size(&self) -> Size<Pixels> {
|
fn content_size(&self) -> Size<Pixels> {
|
||||||
let base_handle = &self.0.borrow().base_handle;
|
let base_handle = &self.0.borrow().base_handle;
|
||||||
base_handle.max_offset() + base_handle.bounds().size
|
base_handle.max_offset() + base_handle.bounds().size
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
impl ScrollbarHandle for ListState {
|
||||||
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
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)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub struct ScrollbarStateInner {
|
struct ScrollbarStateInner {
|
||||||
hovered_axis: Option<Axis>,
|
hovered_axis: Option<Axis>,
|
||||||
hovered_on_thumb: Option<Axis>,
|
hovered_on_thumb: Option<Axis>,
|
||||||
dragged_axis: Option<Axis>,
|
dragged_axis: Option<Axis>,
|
||||||
@@ -83,6 +110,7 @@ pub struct ScrollbarStateInner {
|
|||||||
last_scroll_time: Option<Instant>,
|
last_scroll_time: Option<Instant>,
|
||||||
// Last update offset
|
// Last update offset
|
||||||
last_update: Instant,
|
last_update: Instant,
|
||||||
|
idle_timer_scheduled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ScrollbarState {
|
impl Default for ScrollbarState {
|
||||||
@@ -95,6 +123,7 @@ impl Default for ScrollbarState {
|
|||||||
last_scroll_offset: point(px(0.), px(0.)),
|
last_scroll_offset: point(px(0.), px(0.)),
|
||||||
last_scroll_time: None,
|
last_scroll_time: None,
|
||||||
last_update: Instant::now(),
|
last_update: Instant::now(),
|
||||||
|
idle_timer_scheduled: false,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,8 +167,10 @@ impl ScrollbarStateInner {
|
|||||||
fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
|
fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
|
||||||
let mut state = *self;
|
let mut state = *self;
|
||||||
state.hovered_on_thumb = axis;
|
state.hovered_on_thumb = axis;
|
||||||
if self.is_scrollbar_visible() && axis.is_some() {
|
if self.is_scrollbar_visible() {
|
||||||
state.last_scroll_time = Some(std::time::Instant::now());
|
if axis.is_some() {
|
||||||
|
state.last_scroll_time = Some(std::time::Instant::now());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
@@ -167,6 +198,12 @@ impl ScrollbarStateInner {
|
|||||||
state
|
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 {
|
fn is_scrollbar_visible(&self) -> bool {
|
||||||
// On drag
|
// On drag
|
||||||
if self.dragged_axis.is_some() {
|
if self.dragged_axis.is_some() {
|
||||||
@@ -182,10 +219,14 @@ impl ScrollbarStateInner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scrollbar axis.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ScrollbarAxis {
|
pub enum ScrollbarAxis {
|
||||||
|
/// Vertical scrollbar.
|
||||||
Vertical,
|
Vertical,
|
||||||
|
/// Horizontal scrollbar.
|
||||||
Horizontal,
|
Horizontal,
|
||||||
|
/// Show both vertical and horizontal scrollbars.
|
||||||
Both,
|
Both,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,25 +241,30 @@ impl From<Axis> for ScrollbarAxis {
|
|||||||
|
|
||||||
impl ScrollbarAxis {
|
impl ScrollbarAxis {
|
||||||
/// Return true if the scrollbar axis is vertical.
|
/// Return true if the scrollbar axis is vertical.
|
||||||
|
#[inline]
|
||||||
pub fn is_vertical(&self) -> bool {
|
pub fn is_vertical(&self) -> bool {
|
||||||
matches!(self, Self::Vertical)
|
matches!(self, Self::Vertical)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return true if the scrollbar axis is horizontal.
|
/// Return true if the scrollbar axis is horizontal.
|
||||||
|
#[inline]
|
||||||
pub fn is_horizontal(&self) -> bool {
|
pub fn is_horizontal(&self) -> bool {
|
||||||
matches!(self, Self::Horizontal)
|
matches!(self, Self::Horizontal)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return true if the scrollbar axis is both vertical and horizontal.
|
/// Return true if the scrollbar axis is both vertical and horizontal.
|
||||||
|
#[inline]
|
||||||
pub fn is_both(&self) -> bool {
|
pub fn is_both(&self) -> bool {
|
||||||
matches!(self, Self::Both)
|
matches!(self, Self::Both)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return true if the scrollbar has vertical axis.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_vertical(&self) -> bool {
|
pub fn has_vertical(&self) -> bool {
|
||||||
matches!(self, Self::Vertical | Self::Both)
|
matches!(self, Self::Vertical | Self::Both)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return true if the scrollbar has horizontal axis.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn has_horizontal(&self) -> bool {
|
pub fn has_horizontal(&self) -> bool {
|
||||||
matches!(self, Self::Horizontal | Self::Both)
|
matches!(self, Self::Horizontal | Self::Both)
|
||||||
@@ -238,9 +284,10 @@ impl ScrollbarAxis {
|
|||||||
|
|
||||||
/// Scrollbar control for scroll-area or a uniform-list.
|
/// Scrollbar control for scroll-area or a uniform-list.
|
||||||
pub struct Scrollbar {
|
pub struct Scrollbar {
|
||||||
|
pub(crate) id: ElementId,
|
||||||
axis: ScrollbarAxis,
|
axis: ScrollbarAxis,
|
||||||
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
|
scrollbar_mode: Option<ScrollbarMode>,
|
||||||
state: ScrollbarState,
|
scroll_handle: Rc<dyn ScrollbarHandle>,
|
||||||
scroll_size: Option<Size<Pixels>>,
|
scroll_size: Option<Size<Pixels>>,
|
||||||
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
||||||
///
|
///
|
||||||
@@ -250,50 +297,46 @@ pub struct Scrollbar {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scrollbar {
|
impl Scrollbar {
|
||||||
fn new(
|
/// Create a new scrollbar.
|
||||||
axis: impl Into<ScrollbarAxis>,
|
///
|
||||||
state: &ScrollbarState,
|
/// This will have both vertical and horizontal scrollbars.
|
||||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
#[track_caller]
|
||||||
) -> Self {
|
pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||||
|
let caller = Location::caller();
|
||||||
Self {
|
Self {
|
||||||
state: state.clone(),
|
id: ElementId::CodeLocation(*caller),
|
||||||
axis: axis.into(),
|
axis: ScrollbarAxis::Both,
|
||||||
scroll_handle: Rc::new(Box::new(scroll_handle.clone())),
|
scrollbar_mode: None,
|
||||||
|
scroll_handle: Rc::new(scroll_handle.clone()),
|
||||||
max_fps: 120,
|
max_fps: 120,
|
||||||
scroll_size: None,
|
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.
|
/// Create with horizontal scrollbar.
|
||||||
pub fn horizontal(
|
#[track_caller]
|
||||||
state: &ScrollbarState,
|
pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
|
||||||
) -> Self {
|
|
||||||
Self::new(ScrollbarAxis::Horizontal, state, scroll_handle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create with vertical scrollbar.
|
/// Create with vertical scrollbar.
|
||||||
pub fn vertical(
|
#[track_caller]
|
||||||
state: &ScrollbarState,
|
pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
|
||||||
) -> Self {
|
|
||||||
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create vertical scrollbar for uniform list.
|
/// Set a specific element id, default is the [`Location::caller`].
|
||||||
pub fn uniform_scroll(
|
///
|
||||||
state: &ScrollbarState,
|
/// NOTE: In most cases, you don't need to set a specific id for scrollbar.
|
||||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
||||||
) -> Self {
|
self.id = id.into();
|
||||||
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
|
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.
|
/// Set a special scroll size of the content area, default is None.
|
||||||
@@ -315,11 +358,16 @@ impl Scrollbar {
|
|||||||
/// If you have very high CPU usage, consider reducing this value to improve performance.
|
/// If you have very high CPU usage, consider reducing this value to improve performance.
|
||||||
///
|
///
|
||||||
/// Available values: 30..120
|
/// Available values: 30..120
|
||||||
pub fn max_fps(mut self, max_fps: usize) -> Self {
|
pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
|
||||||
self.max_fps = max_fps.clamp(30, 120);
|
self.max_fps = max_fps.clamp(30, 120);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the width of the scrollbar.
|
||||||
|
pub(crate) const fn width() -> Pixels {
|
||||||
|
WIDTH
|
||||||
|
}
|
||||||
|
|
||||||
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||||
(
|
(
|
||||||
cx.theme().scrollbar_thumb_hover_background,
|
cx.theme().scrollbar_thumb_hover_background,
|
||||||
@@ -353,11 +401,28 @@ impl Scrollbar {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn style_for_idle(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||||
let (width, inset, radius) = if cx.theme().scrollbar_mode.is_scrolling() {
|
let scrollbar_mode = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||||
(THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS)
|
let (width, inset, radius) = match scrollbar_mode {
|
||||||
} else {
|
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||||
(THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_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 {
|
pub struct PrepaintState {
|
||||||
hitbox: Hitbox,
|
hitbox: Hitbox,
|
||||||
|
scrollbar_state: ScrollbarState,
|
||||||
states: Vec<AxisPrepaintState>,
|
states: Vec<AxisPrepaintState>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
pub struct AxisPrepaintState {
|
pub struct AxisPrepaintState {
|
||||||
axis: Axis,
|
axis: Axis,
|
||||||
bar_hitbox: Hitbox,
|
bar_hitbox: Hitbox,
|
||||||
@@ -406,7 +474,7 @@ impl Element for Scrollbar {
|
|||||||
type RequestLayoutState = ();
|
type RequestLayoutState = ();
|
||||||
|
|
||||||
fn id(&self) -> Option<gpui::ElementId> {
|
fn id(&self) -> Option<gpui::ElementId> {
|
||||||
None
|
Some(self.id.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||||
@@ -420,16 +488,12 @@ impl Element for Scrollbar {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (LayoutId, Self::RequestLayoutState) {
|
) -> (LayoutId, Self::RequestLayoutState) {
|
||||||
let style = gpui::Style {
|
let mut style = Style::default();
|
||||||
position: Position::Absolute,
|
style.position = Position::Absolute;
|
||||||
flex_grow: 1.0,
|
style.flex_grow = 1.0;
|
||||||
flex_shrink: 1.0,
|
style.flex_shrink = 1.0;
|
||||||
size: gpui::Size {
|
style.size.width = relative(1.).into();
|
||||||
width: relative(1.).into(),
|
style.size.height = relative(1.).into();
|
||||||
height: relative(1.).into(),
|
|
||||||
},
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
(window.request_layout(style, None, cx), ())
|
(window.request_layout(style, None, cx), ())
|
||||||
}
|
}
|
||||||
@@ -447,6 +511,11 @@ impl Element for Scrollbar {
|
|||||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let state = window
|
||||||
|
.use_state(cx, |_, _| ScrollbarState::default())
|
||||||
|
.read(cx)
|
||||||
|
.clone();
|
||||||
|
|
||||||
let mut states = vec![];
|
let mut states = vec![];
|
||||||
let mut has_both = self.axis.is_both();
|
let mut has_both = self.axis.is_both();
|
||||||
let scroll_size = self
|
let scroll_size = self
|
||||||
@@ -470,9 +539,8 @@ impl Element for Scrollbar {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
|
// 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 {
|
let margin_end = if has_both && !is_vertical {
|
||||||
THUMB_ACTIVE_WIDTH
|
WIDTH
|
||||||
} else {
|
} else {
|
||||||
px(0.)
|
px(0.)
|
||||||
};
|
};
|
||||||
@@ -512,11 +580,12 @@ impl Element for Scrollbar {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let state = self.state.clone();
|
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||||
let is_always_to_show = cx.theme().scrollbar_mode.is_always();
|
let is_always_to_show = scrollbar_show.is_always();
|
||||||
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
|
let is_hover_to_show = scrollbar_show.is_hover();
|
||||||
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
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_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) =
|
let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
|
||||||
if state.get().dragged_axis == Some(axis) {
|
if state.get().dragged_axis == Some(axis) {
|
||||||
@@ -527,38 +596,47 @@ impl Element for Scrollbar {
|
|||||||
} else {
|
} else {
|
||||||
Self::style_for_hovered_bar(cx)
|
Self::style_for_hovered_bar(cx)
|
||||||
}
|
}
|
||||||
|
} else if is_offset_changed {
|
||||||
|
self.style_for_normal(cx)
|
||||||
} else if is_always_to_show {
|
} else if is_always_to_show {
|
||||||
#[allow(clippy::if_same_then_else)]
|
|
||||||
if is_hovered_on_thumb {
|
if is_hovered_on_thumb {
|
||||||
Self::style_for_hovered_thumb(cx)
|
Self::style_for_hovered_thumb(cx)
|
||||||
} else {
|
} else {
|
||||||
Self::style_for_hovered_bar(cx)
|
Self::style_for_hovered_bar(cx)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
// Delay 2s to fade out the scrollbar thumb (in 1s)
|
||||||
if let Some(last_time) = state.get().last_scroll_time {
|
if let Some(last_time) = state.get().last_scroll_time {
|
||||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
||||||
if elapsed < FADE_OUT_DURATION {
|
if is_hovered_on_bar {
|
||||||
if is_hovered_on_bar {
|
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
|
||||||
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
|
idle_state = if is_hovered_on_thumb {
|
||||||
idle_state = if is_hovered_on_thumb {
|
Self::style_for_hovered_thumb(cx)
|
||||||
Self::style_for_hovered_thumb(cx)
|
|
||||||
} else {
|
|
||||||
Self::style_for_hovered_bar(cx)
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
if elapsed < FADE_OUT_DELAY {
|
Self::style_for_hovered_bar(cx)
|
||||||
idle_state.0 = cx.theme().scrollbar_thumb_background;
|
};
|
||||||
} else {
|
} else if elapsed < FADE_OUT_DELAY {
|
||||||
// opacity = 1 - (x - 2)^10
|
idle_state.0 = cx.theme().scrollbar_thumb_background;
|
||||||
let opacity = 1.0 - (elapsed - FADE_OUT_DELAY).powi(10);
|
|
||||||
idle_state.0 =
|
|
||||||
cx.theme().scrollbar_thumb_background.opacity(opacity);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 +695,11 @@ impl Element for Scrollbar {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
PrepaintState { hitbox, states }
|
PrepaintState {
|
||||||
|
hitbox,
|
||||||
|
states,
|
||||||
|
scrollbar_state: state,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn paint(
|
fn paint(
|
||||||
@@ -630,19 +712,21 @@ impl Element for Scrollbar {
|
|||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
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 view_id = window.current_view();
|
||||||
let hitbox_bounds = prepaint.hitbox.bounds;
|
let hitbox_bounds = prepaint.hitbox.bounds;
|
||||||
let is_visible =
|
let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
|
||||||
self.state.get().is_scrollbar_visible() || cx.theme().scrollbar_mode.is_always();
|
let is_hover_to_show = scrollbar_show.is_hover();
|
||||||
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
|
|
||||||
|
|
||||||
// Update last_scroll_time when offset is changed.
|
// Update last_scroll_time when offset is changed.
|
||||||
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
|
if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
|
||||||
self.state.set(
|
scrollbar_state.set(
|
||||||
self.state
|
scrollbar_state
|
||||||
.get()
|
.get()
|
||||||
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
|
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
|
||||||
);
|
);
|
||||||
|
cx.notify(view_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.with_content_mask(
|
window.with_content_mask(
|
||||||
@@ -652,7 +736,10 @@ impl Element for Scrollbar {
|
|||||||
|window| {
|
|window| {
|
||||||
for state in prepaint.states.iter() {
|
for state in prepaint.states.iter() {
|
||||||
let axis = state.axis;
|
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 bounds = state.bounds;
|
||||||
let thumb_bounds = state.thumb_bounds;
|
let thumb_bounds = state.thumb_bounds;
|
||||||
let scroll_area_size = state.scroll_size;
|
let scroll_area_size = state.scroll_size;
|
||||||
@@ -670,11 +757,20 @@ impl Element for Scrollbar {
|
|||||||
bounds,
|
bounds,
|
||||||
corner_radii: (0.).into(),
|
corner_radii: (0.).into(),
|
||||||
background: gpui::transparent_black().into(),
|
background: gpui::transparent_black().into(),
|
||||||
border_widths: Edges {
|
border_widths: if is_vertical {
|
||||||
top: px(0.),
|
Edges {
|
||||||
right: px(0.),
|
top: px(0.),
|
||||||
bottom: px(0.),
|
right: px(0.),
|
||||||
left: px(0.),
|
bottom: px(0.),
|
||||||
|
left: px(0.),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Edges {
|
||||||
|
top: px(0.),
|
||||||
|
right: px(0.),
|
||||||
|
bottom: px(0.),
|
||||||
|
left: px(0.),
|
||||||
|
}
|
||||||
},
|
},
|
||||||
border_color: state.border,
|
border_color: state.border,
|
||||||
border_style: BorderStyle::default(),
|
border_style: BorderStyle::default(),
|
||||||
@@ -686,19 +782,18 @@ impl Element for Scrollbar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let state = self.state.clone();
|
let state = scrollbar_state.clone();
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
|
|
||||||
move |event: &ScrollWheelEvent, phase, _, cx| {
|
move |event: &ScrollWheelEvent, phase, _, cx| {
|
||||||
if phase.bubble()
|
if phase.bubble() && hitbox_bounds.contains(&event.position) {
|
||||||
&& hitbox_bounds.contains(&event.position)
|
if scroll_handle.offset() != state.get().last_scroll_offset {
|
||||||
&& scroll_handle.offset() != state.get().last_scroll_offset
|
state.set(state.get().with_last_scroll(
|
||||||
{
|
scroll_handle.offset(),
|
||||||
state.set(state.get().with_last_scroll(
|
Some(Instant::now()),
|
||||||
scroll_handle.offset(),
|
));
|
||||||
Some(Instant::now()),
|
cx.notify(view_id);
|
||||||
));
|
}
|
||||||
cx.notify(view_id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -707,7 +802,7 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
if is_hover_to_show || is_visible {
|
if is_hover_to_show || is_visible {
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let state = self.state.clone();
|
let state = scrollbar_state.clone();
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
let scroll_handle = self.scroll_handle.clone();
|
||||||
|
|
||||||
move |event: &MouseDownEvent, phase, _, cx| {
|
move |event: &MouseDownEvent, phase, _, cx| {
|
||||||
@@ -718,6 +813,7 @@ impl Element for Scrollbar {
|
|||||||
// click on the thumb bar, set the drag position
|
// click on the thumb bar, set the drag position
|
||||||
let pos = event.position - thumb_bounds.origin;
|
let pos = event.position - thumb_bounds.origin;
|
||||||
|
|
||||||
|
scroll_handle.start_drag();
|
||||||
state.set(state.get().with_drag_pos(axis, pos));
|
state.set(state.get().with_drag_pos(axis, pos));
|
||||||
|
|
||||||
cx.notify(view_id);
|
cx.notify(view_id);
|
||||||
@@ -755,7 +851,7 @@ impl Element for Scrollbar {
|
|||||||
|
|
||||||
window.on_mouse_event({
|
window.on_mouse_event({
|
||||||
let scroll_handle = self.scroll_handle.clone();
|
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);
|
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
|
||||||
|
|
||||||
move |event: &MouseMoveEvent, _, _, cx| {
|
move |event: &MouseMoveEvent, _, _, cx| {
|
||||||
@@ -770,11 +866,13 @@ impl Element for Scrollbar {
|
|||||||
if state.get().hovered_axis != Some(axis) {
|
if state.get().hovered_axis != Some(axis) {
|
||||||
notify = true;
|
notify = true;
|
||||||
}
|
}
|
||||||
} else if state.get().hovered_axis == Some(axis)
|
} else {
|
||||||
&& state.get().hovered_axis.is_some()
|
if state.get().hovered_axis == Some(axis) {
|
||||||
{
|
if state.get().hovered_axis.is_some() {
|
||||||
state.set(state.get().with_hovered(None));
|
state.set(state.get().with_hovered(None));
|
||||||
notify = true;
|
notify = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update hovered state for scrollbar thumb
|
// Update hovered state for scrollbar thumb
|
||||||
@@ -783,13 +881,18 @@ impl Element for Scrollbar {
|
|||||||
state.set(state.get().with_hovered_on_thumb(Some(axis)));
|
state.set(state.get().with_hovered_on_thumb(Some(axis)));
|
||||||
notify = true;
|
notify = true;
|
||||||
}
|
}
|
||||||
} else if state.get().hovered_on_thumb == Some(axis) {
|
} else {
|
||||||
state.set(state.get().with_hovered_on_thumb(None));
|
if state.get().hovered_on_thumb == Some(axis) {
|
||||||
notify = true;
|
state.set(state.get().with_hovered_on_thumb(None));
|
||||||
|
notify = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move thumb position on dragging
|
// Move thumb position on dragging
|
||||||
if state.get().dragged_axis == Some(axis) && event.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
|
// drag_pos is the position of the mouse down event
|
||||||
// We need to keep the thumb bar still at the origin down position
|
// We need to keep the thumb bar still at the origin down position
|
||||||
let drag_pos = state.get().drag_pos;
|
let drag_pos = state.get().drag_pos;
|
||||||
@@ -836,10 +939,12 @@ impl Element for Scrollbar {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.on_mouse_event({
|
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| {
|
move |_event: &MouseUpEvent, phase, _, cx| {
|
||||||
if phase.bubble() {
|
if phase.bubble() {
|
||||||
|
scroll_handle.end_drag();
|
||||||
state.set(state.get().with_unset_drag_pos());
|
state.set(state.get().with_unset_drag_pos());
|
||||||
cx.notify(view_id);
|
cx.notify(view_id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ impl Skeleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn secondary(mut self, secondary: bool) -> Self {
|
pub fn secondary(mut self) -> Self {
|
||||||
self.secondary = secondary;
|
self.secondary = true;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,7 @@
|
|||||||
use std::fmt::{self, Display, Formatter};
|
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
|
||||||
|
|
||||||
use gpui::{div, px, App, Axis, Div, Element, Pixels, Refineable, StyleRefinement, Styled};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
use crate::scroll::{Scrollable, ScrollbarAxis};
|
|
||||||
|
|
||||||
/// Returns a `Div` as horizontal flex layout.
|
/// Returns a `Div` as horizontal flex layout.
|
||||||
pub fn h_flex() -> Div {
|
pub fn h_flex() -> Div {
|
||||||
div().h_flex()
|
div().h_flex()
|
||||||
@@ -50,17 +46,6 @@ pub trait StyledExt: Styled + Sized {
|
|||||||
self.flex().flex_col()
|
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_thin, THIN);
|
||||||
font_weight!(font_extralight, EXTRA_LIGHT);
|
font_weight!(font_extralight, EXTRA_LIGHT);
|
||||||
font_weight!(font_light, LIGHT);
|
font_weight!(font_light, LIGHT);
|
||||||
@@ -259,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.
|
/// A trait for defining element that can be collapsed.
|
||||||
pub trait Collapsible {
|
pub trait Collapsible {
|
||||||
fn collapsed(self, collapsed: bool) -> Self;
|
fn collapsed(self, collapsed: bool) -> Self;
|
||||||
|
|||||||
Reference in New Issue
Block a user