Compare commits
2 Commits
2c33670ba5
...
fad30a89f1
| Author | SHA1 | Date | |
|---|---|---|---|
| fad30a89f1 | |||
| 3e8efdd0ef |
@@ -231,13 +231,13 @@ impl AutoUpdater {
|
||||
|
||||
fn subscribe_to_updates(cx: &App) -> Task<()> {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let _client = nostr.read(cx).client();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let _opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
|
||||
|
||||
let filter = Filter::new()
|
||||
let _filter = Filter::new()
|
||||
.kind(Kind::ReleaseArtifactSet)
|
||||
.author(app_pubkey)
|
||||
.limit(1);
|
||||
@@ -253,7 +253,7 @@ impl AutoUpdater {
|
||||
});
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let _opts = SubscribeAutoCloseOptions::default().exit_policy(ReqExitPolicy::ExitOnEOSE);
|
||||
let app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
|
||||
|
||||
let filter = Filter::new()
|
||||
@@ -274,7 +274,7 @@ impl AutoUpdater {
|
||||
// Get all file metadata event ids
|
||||
let ids: Vec<EventId> = event.tags.event_ids().copied().collect();
|
||||
|
||||
let filter = Filter::new()
|
||||
let _filter = Filter::new()
|
||||
.kind(Kind::FileMetadata)
|
||||
.author(app_pubkey)
|
||||
.ids(ids.clone());
|
||||
|
||||
@@ -82,10 +82,20 @@ impl ChatRegistry {
|
||||
/// Create a new chat registry instance
|
||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let nip65 = nostr.read(cx).nip65_state();
|
||||
let nip17 = nostr.read(cx).nip17_state();
|
||||
|
||||
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(
|
||||
// Observe the nip17 state and load chat rooms on every state change
|
||||
cx.observe(&nip17, |this, _state, cx| {
|
||||
|
||||
@@ -16,6 +16,7 @@ use crate::{ChatRegistry, NewMessage};
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SendReport {
|
||||
pub receiver: PublicKey,
|
||||
pub gift_wrap_id: Option<EventId>,
|
||||
pub error: Option<SharedString>,
|
||||
pub output: Option<Output<EventId>>,
|
||||
}
|
||||
@@ -24,12 +25,18 @@ impl SendReport {
|
||||
pub fn new(receiver: PublicKey) -> Self {
|
||||
Self {
|
||||
receiver,
|
||||
|
||||
gift_wrap_id: None,
|
||||
error: None,
|
||||
output: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the gift wrap ID.
|
||||
pub fn gift_wrap_id(mut self, gift_wrap_id: EventId) -> Self {
|
||||
self.gift_wrap_id = Some(gift_wrap_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the output.
|
||||
pub fn output(mut self, output: Output<EventId>) -> Self {
|
||||
self.output = Some(output);
|
||||
@@ -533,7 +540,11 @@ impl Room {
|
||||
.await
|
||||
{
|
||||
Ok(output) => {
|
||||
reports.push(SendReport::new(member.public_key()).output(output));
|
||||
reports.push(
|
||||
SendReport::new(member.public_key())
|
||||
.gift_wrap_id(event.id)
|
||||
.output(output),
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
reports.push(
|
||||
|
||||
@@ -26,11 +26,11 @@ use state::NostrRegistry;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::context_menu::ContextMenuExt;
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::menu::{ContextMenuExt, DropdownMenu};
|
||||
use ui::notification::Notification;
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{
|
||||
h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
|
||||
WindowExtension,
|
||||
@@ -63,11 +63,14 @@ pub struct ChatPanel {
|
||||
rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
|
||||
|
||||
/// Mapping message (rumor event) ids to their reports
|
||||
reports_by_id: Arc<RwLock<BTreeMap<EventId, Vec<SendReport>>>>,
|
||||
reports_by_id: Entity<BTreeMap<EventId, Vec<SendReport>>>,
|
||||
|
||||
/// Input state
|
||||
input: Entity<InputState>,
|
||||
|
||||
/// Sent message ids
|
||||
sent_ids: Arc<RwLock<Vec<EventId>>>,
|
||||
|
||||
/// Replies to
|
||||
replies_to: Entity<HashSet<EventId>>,
|
||||
|
||||
@@ -89,6 +92,7 @@ impl ChatPanel {
|
||||
// Define attachments and replies_to entities
|
||||
let attachments = cx.new(|_| vec![]);
|
||||
let replies_to = cx.new(|_| HashSet::new());
|
||||
let reports_by_id = cx.new(|_| BTreeMap::new());
|
||||
|
||||
// Define list of messages
|
||||
let messages = BTreeSet::from([Message::system()]);
|
||||
@@ -141,7 +145,8 @@ impl ChatPanel {
|
||||
replies_to,
|
||||
attachments,
|
||||
rendered_texts_by_id: BTreeMap::new(),
|
||||
reports_by_id: Arc::new(RwLock::new(BTreeMap::new())),
|
||||
reports_by_id,
|
||||
sent_ids: Arc::new(RwLock::new(Vec::new())),
|
||||
uploading: false,
|
||||
subscriptions,
|
||||
tasks: vec![],
|
||||
@@ -152,7 +157,9 @@ impl ChatPanel {
|
||||
fn handle_notifications(&mut self, cx: &mut Context<Self>) {
|
||||
let nostr = NostrRegistry::global(cx);
|
||||
let client = nostr.read(cx).client();
|
||||
let reports = self.reports_by_id.clone();
|
||||
let sent_ids = self.sent_ids.clone();
|
||||
|
||||
let (tx, rx) = flume::bounded::<(EventId, RelayUrl)>(256);
|
||||
|
||||
self.tasks.push(cx.background_spawn(async move {
|
||||
let mut notifications = client.notifications();
|
||||
@@ -163,18 +170,33 @@ impl ChatPanel {
|
||||
relay_url,
|
||||
} = notification
|
||||
{
|
||||
let mut writer = reports.write().await;
|
||||
let sent_ids = sent_ids.read().await;
|
||||
|
||||
for reports in writer.values_mut() {
|
||||
if sent_ids.contains(&event_id) {
|
||||
tx.send_async((event_id, relay_url)).await.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
|
||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
||||
while let Ok((event_id, relay_url)) = rx.recv_async().await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.reports_by_id.update(cx, |this, cx| {
|
||||
for reports in this.values_mut() {
|
||||
for report in reports.iter_mut() {
|
||||
if let Some(output) = report.output.as_mut() {
|
||||
if output.id() == &event_id {
|
||||
output.success.insert(relay_url.clone());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -302,6 +324,7 @@ impl ChatPanel {
|
||||
|
||||
/// Send message in the background and wait for the response
|
||||
fn send_and_wait(&mut self, rumor: UnsignedEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let sent_ids = self.sent_ids.clone();
|
||||
// This can't fail, because we already ensured that the ID is set
|
||||
let id = rumor.id.unwrap();
|
||||
|
||||
@@ -317,6 +340,10 @@ impl ChatPanel {
|
||||
self.tasks.push(cx.spawn_in(window, async move |this, cx| {
|
||||
let outputs = task.await;
|
||||
|
||||
// Add sent IDs to the list
|
||||
let mut sent_ids = sent_ids.write().await;
|
||||
sent_ids.extend(outputs.iter().filter_map(|output| output.gift_wrap_id));
|
||||
|
||||
// Update the state
|
||||
this.update(cx, |this, cx| {
|
||||
this.insert_reports(id, outputs, cx);
|
||||
@@ -343,10 +370,12 @@ impl ChatPanel {
|
||||
})
|
||||
}
|
||||
|
||||
/// Synchronously insert reports
|
||||
/// Insert reports
|
||||
fn insert_reports(&mut self, id: EventId, reports: Vec<SendReport>, cx: &mut Context<Self>) {
|
||||
self.reports_by_id.write_blocking().insert(id, reports);
|
||||
self.reports_by_id.update(cx, |this, cx| {
|
||||
this.insert(id, reports);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
/// Insert a message into the chat panel
|
||||
@@ -380,24 +409,32 @@ impl ChatPanel {
|
||||
}
|
||||
|
||||
/// Check if a message is pending
|
||||
fn sent_pending(&self, id: &EventId) -> bool {
|
||||
fn sent_pending(&self, id: &EventId, cx: &App) -> bool {
|
||||
self.reports_by_id
|
||||
.read_blocking()
|
||||
.read(cx)
|
||||
.get(id)
|
||||
.is_some_and(|reports| reports.iter().any(|r| r.pending()))
|
||||
}
|
||||
|
||||
/// Check if a message was sent successfully by its ID
|
||||
fn sent_success(&self, id: &EventId) -> bool {
|
||||
fn sent_success(&self, id: &EventId, cx: &App) -> bool {
|
||||
self.reports_by_id
|
||||
.read_blocking()
|
||||
.read(cx)
|
||||
.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, cx: &App) -> Option<bool> {
|
||||
self.reports_by_id
|
||||
.read(cx)
|
||||
.get(id)
|
||||
.map(|reports| reports.iter().all(|r| !r.success()))
|
||||
}
|
||||
|
||||
/// Get all sent reports for a message by its ID
|
||||
fn sent_reports(&self, id: &EventId) -> Option<Vec<SendReport>> {
|
||||
self.reports_by_id.read_blocking().get(id).cloned()
|
||||
fn sent_reports(&self, id: &EventId, cx: &App) -> Option<Vec<SendReport>> {
|
||||
self.reports_by_id.read(cx).get(id).cloned()
|
||||
}
|
||||
|
||||
/// Get a message by its ID
|
||||
@@ -617,10 +654,13 @@ impl ChatPanel {
|
||||
let has_replies = !replies.is_empty();
|
||||
|
||||
// Check if message is sent failed
|
||||
let sent_pending = self.sent_pending(&id);
|
||||
let sent_pending = self.sent_pending(&id, cx);
|
||||
|
||||
// Check if message is sent successfully
|
||||
let sent_success = self.sent_success(&id);
|
||||
let sent_success = self.sent_success(&id, cx);
|
||||
|
||||
// Check if message is sent failed
|
||||
let sent_failed = self.sent_failed(&id, cx);
|
||||
|
||||
// Hide avatar setting
|
||||
let hide_avatar = AppSettings::get_hide_avatar(cx);
|
||||
@@ -679,8 +719,10 @@ impl ChatPanel {
|
||||
this.children(self.render_message_replies(replies, cx))
|
||||
})
|
||||
.child(rendered_text)
|
||||
.when(!sent_success, |this| {
|
||||
.when_some(sent_failed, |this, failed| {
|
||||
this.when(failed, |this| {
|
||||
this.child(deferred(self.render_message_reports(&id, cx)))
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
@@ -745,11 +787,11 @@ impl ChatPanel {
|
||||
items
|
||||
}
|
||||
|
||||
fn render_sent_indicator(&self, id: &EventId, _cx: &Context<Self>) -> impl IntoElement {
|
||||
fn render_sent_indicator(&self, id: &EventId, cx: &Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id(SharedString::from(id.to_hex()))
|
||||
.child(SharedString::from("• Sent"))
|
||||
.when_some(self.sent_reports(id), |this, reports| {
|
||||
.when_some(self.sent_reports(id, cx), |this, reports| {
|
||||
this.on_click(move |_e, window, cx| {
|
||||
let reports = reports.clone();
|
||||
|
||||
@@ -781,7 +823,7 @@ impl ChatPanel {
|
||||
.child(SharedString::from(
|
||||
"Failed to send message. Click to see details.",
|
||||
))
|
||||
.when_some(self.sent_reports(id), |this, reports| {
|
||||
.when_some(self.sent_reports(id, cx), |this, reports| {
|
||||
this.on_click(move |_e, window, cx| {
|
||||
let reports = reports.clone();
|
||||
|
||||
@@ -966,9 +1008,9 @@ impl ChatPanel {
|
||||
.icon(IconName::Ellipsis)
|
||||
.small()
|
||||
.ghost()
|
||||
.popup_menu({
|
||||
.dropdown_menu({
|
||||
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())
|
||||
@@ -1144,15 +1186,20 @@ impl Render for ChatPanel {
|
||||
v_flex()
|
||||
.on_action(cx.listener(Self::on_command))
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.child(
|
||||
list(
|
||||
self.list_state.clone(),
|
||||
cx.processor(|this, ix, window, cx| {
|
||||
// Get and render message by index
|
||||
cx.processor(move |this, ix, window, cx| {
|
||||
this.render_message(ix, window, cx)
|
||||
}),
|
||||
)
|
||||
.flex_1(),
|
||||
.size_full(),
|
||||
)
|
||||
.child(Scrollbar::vertical(&self.list_state)),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
@@ -1192,10 +1239,10 @@ impl Render for ChatPanel {
|
||||
.icon(IconName::Emoji)
|
||||
.ghost()
|
||||
.large()
|
||||
.popup_menu_with_anchor(
|
||||
.dropdown_menu_with_anchor(
|
||||
gpui::Corner::BottomLeft,
|
||||
move |this, _window, _cx| {
|
||||
this.axis(gpui::Axis::Horizontal)
|
||||
this.horizontal()
|
||||
.menu("👍", Box::new(Command::Insert("👍")))
|
||||
.menu("👎", Box::new(Command::Insert("👎")))
|
||||
.menu("😄", Box::new(Command::Insert("😄")))
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use chat::RoomKind;
|
||||
use chat_ui::{CopyPublicKey, OpenPublicKey};
|
||||
use dock::ClosePanel;
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
@@ -12,7 +11,6 @@ use nostr_sdk::prelude::*;
|
||||
use settings::AppSettings;
|
||||
use theme::ActiveTheme;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::context_menu::ContextMenuExt;
|
||||
use ui::modal::ModalButtonProps;
|
||||
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))
|
||||
.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| {
|
||||
this.on_click(move |event, window, cx| {
|
||||
handler(event, window, cx);
|
||||
|
||||
@@ -11,7 +11,7 @@ use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription,
|
||||
Task, Window,
|
||||
Task, UniformListScrollHandle, Window,
|
||||
};
|
||||
use nostr_sdk::prelude::*;
|
||||
use person::PersonRegistry;
|
||||
@@ -23,6 +23,7 @@ use ui::divider::Divider;
|
||||
use ui::indicator::Indicator;
|
||||
use ui::input::{InputEvent, InputState, TextInput};
|
||||
use ui::notification::Notification;
|
||||
use ui::scroll::Scrollbar;
|
||||
use ui::{
|
||||
h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension,
|
||||
};
|
||||
@@ -39,6 +40,7 @@ pub fn init(window: &mut Window, cx: &mut App) -> Entity<Sidebar> {
|
||||
pub struct Sidebar {
|
||||
name: SharedString,
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
|
||||
/// Image cache
|
||||
image_cache: Entity<RetainAllImageCache>,
|
||||
@@ -143,6 +145,7 @@ impl Sidebar {
|
||||
Self {
|
||||
name: "Sidebar".into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
image_cache: RetainAllImageCache::new(cx),
|
||||
find_input,
|
||||
find_debouncer: DebouncedDelay::new(),
|
||||
@@ -690,9 +693,11 @@ impl Render for Sidebar {
|
||||
this.render_list_items(range, cx)
|
||||
}),
|
||||
)
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.flex_1()
|
||||
.h_full(),
|
||||
)
|
||||
.child(Scrollbar::vertical(&self.scroll_handle))
|
||||
}),
|
||||
)
|
||||
.when(!self.selected_pkeys.read(cx).is_empty(), |this| {
|
||||
|
||||
@@ -16,7 +16,7 @@ use theme::{ActiveTheme, SIDEBAR_WIDTH, TITLEBAR_HEIGHT};
|
||||
use titlebar::TitleBar;
|
||||
use ui::avatar::Avatar;
|
||||
use ui::button::{Button, ButtonVariants};
|
||||
use ui::popup_menu::PopupMenuExt;
|
||||
use ui::menu::DropdownMenu;
|
||||
use ui::{h_flex, v_flex, Root, Sizable, WindowExtension};
|
||||
|
||||
use crate::panels::greeter;
|
||||
@@ -184,7 +184,7 @@ impl Workspace {
|
||||
.caret()
|
||||
.compact()
|
||||
.transparent()
|
||||
.popup_menu(move |this, _window, _cx| {
|
||||
.dropdown_menu(move |this, _window, _cx| {
|
||||
this.label(profile.name())
|
||||
.separator()
|
||||
.menu("Profile", Box::new(ClosePanel))
|
||||
|
||||
@@ -3,7 +3,7 @@ use gpui::{
|
||||
SharedString, Window,
|
||||
};
|
||||
use ui::button::Button;
|
||||
use ui::popup_menu::PopupMenu;
|
||||
use ui::menu::PopupMenu;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PanelEvent {
|
||||
|
||||
@@ -9,7 +9,7 @@ use gpui::{
|
||||
};
|
||||
use theme::{ActiveTheme, CLIENT_SIDE_DECORATION_ROUNDING, TABBAR_HEIGHT};
|
||||
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 crate::dock::DockPlacement;
|
||||
@@ -454,7 +454,7 @@ impl TabPanel {
|
||||
.small()
|
||||
.ghost()
|
||||
.rounded()
|
||||
.popup_menu({
|
||||
.dropdown_menu({
|
||||
let zoomable = state.zoomable;
|
||||
let closable = state.closable;
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub enum ScrollbarMode {
|
||||
#[default]
|
||||
Scrolling,
|
||||
Hover,
|
||||
Scrolling,
|
||||
Always,
|
||||
}
|
||||
|
||||
|
||||
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),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
294
crates/ui/src/geometry.rs
Normal file
294
crates/ui/src/geometry.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
|
||||
use gpui::{AbsoluteLength, Axis, Corner, Length, Pixels};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A enum for defining the placement of the element.
|
||||
///
|
||||
/// See also: [`Side`] if you need to define the left, right side.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Placement {
|
||||
#[serde(rename = "top")]
|
||||
Top,
|
||||
#[serde(rename = "bottom")]
|
||||
Bottom,
|
||||
#[serde(rename = "left")]
|
||||
Left,
|
||||
#[serde(rename = "right")]
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Display for Placement {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Placement::Top => write!(f, "Top"),
|
||||
Placement::Bottom => write!(f, "Bottom"),
|
||||
Placement::Left => write!(f, "Left"),
|
||||
Placement::Right => write!(f, "Right"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Placement {
|
||||
#[inline]
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
matches!(self, Placement::Left | Placement::Right)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
matches!(self, Placement::Top | Placement::Bottom)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn axis(&self) -> Axis {
|
||||
match self {
|
||||
Placement::Top | Placement::Bottom => Axis::Vertical,
|
||||
Placement::Left | Placement::Right => Axis::Horizontal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The anchor position of an element.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
|
||||
pub enum Anchor {
|
||||
#[default]
|
||||
#[serde(rename = "top-left")]
|
||||
TopLeft,
|
||||
#[serde(rename = "top-center")]
|
||||
TopCenter,
|
||||
#[serde(rename = "top-right")]
|
||||
TopRight,
|
||||
#[serde(rename = "bottom-left")]
|
||||
BottomLeft,
|
||||
#[serde(rename = "bottom-center")]
|
||||
BottomCenter,
|
||||
#[serde(rename = "bottom-right")]
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
impl Display for Anchor {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Anchor::TopLeft => write!(f, "TopLeft"),
|
||||
Anchor::TopCenter => write!(f, "TopCenter"),
|
||||
Anchor::TopRight => write!(f, "TopRight"),
|
||||
Anchor::BottomLeft => write!(f, "BottomLeft"),
|
||||
Anchor::BottomCenter => write!(f, "BottomCenter"),
|
||||
Anchor::BottomRight => write!(f, "BottomRight"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Anchor {
|
||||
/// Returns true if the anchor is at the top.
|
||||
#[inline]
|
||||
pub fn is_top(&self) -> bool {
|
||||
matches!(self, Self::TopLeft | Self::TopCenter | Self::TopRight)
|
||||
}
|
||||
|
||||
/// Returns true if the anchor is at the bottom.
|
||||
#[inline]
|
||||
pub fn is_bottom(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::BottomLeft | Self::BottomCenter | Self::BottomRight
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns true if the anchor is at the left.
|
||||
#[inline]
|
||||
pub fn is_left(&self) -> bool {
|
||||
matches!(self, Self::TopLeft | Self::BottomLeft)
|
||||
}
|
||||
|
||||
/// Returns true if the anchor is at the right.
|
||||
#[inline]
|
||||
pub fn is_right(&self) -> bool {
|
||||
matches!(self, Self::TopRight | Self::BottomRight)
|
||||
}
|
||||
|
||||
/// Returns true if the anchor is at the center.
|
||||
#[inline]
|
||||
pub fn is_center(&self) -> bool {
|
||||
matches!(self, Self::TopCenter | Self::BottomCenter)
|
||||
}
|
||||
|
||||
/// Swaps the vertical position of the anchor.
|
||||
pub fn swap_vertical(&self) -> Self {
|
||||
match self {
|
||||
Anchor::TopLeft => Anchor::BottomLeft,
|
||||
Anchor::TopCenter => Anchor::BottomCenter,
|
||||
Anchor::TopRight => Anchor::BottomRight,
|
||||
Anchor::BottomLeft => Anchor::TopLeft,
|
||||
Anchor::BottomCenter => Anchor::TopCenter,
|
||||
Anchor::BottomRight => Anchor::TopRight,
|
||||
}
|
||||
}
|
||||
|
||||
/// Swaps the horizontal position of the anchor.
|
||||
pub fn swap_horizontal(&self) -> Self {
|
||||
match self {
|
||||
Anchor::TopLeft => Anchor::TopRight,
|
||||
Anchor::TopCenter => Anchor::TopCenter,
|
||||
Anchor::TopRight => Anchor::TopLeft,
|
||||
Anchor::BottomLeft => Anchor::BottomRight,
|
||||
Anchor::BottomCenter => Anchor::BottomCenter,
|
||||
Anchor::BottomRight => Anchor::BottomLeft,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn other_side_corner_along(&self, axis: Axis) -> Anchor {
|
||||
match axis {
|
||||
Axis::Vertical => match self {
|
||||
Self::TopLeft => Self::BottomLeft,
|
||||
Self::TopCenter => Self::BottomCenter,
|
||||
Self::TopRight => Self::BottomRight,
|
||||
Self::BottomLeft => Self::TopLeft,
|
||||
Self::BottomCenter => Self::TopCenter,
|
||||
Self::BottomRight => Self::TopRight,
|
||||
},
|
||||
Axis::Horizontal => match self {
|
||||
Self::TopLeft => Self::TopRight,
|
||||
Self::TopCenter => Self::TopCenter,
|
||||
Self::TopRight => Self::TopLeft,
|
||||
Self::BottomLeft => Self::BottomRight,
|
||||
Self::BottomCenter => Self::BottomCenter,
|
||||
Self::BottomRight => Self::BottomLeft,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Corner> for Anchor {
|
||||
fn from(corner: Corner) -> Self {
|
||||
match corner {
|
||||
Corner::TopLeft => Anchor::TopLeft,
|
||||
Corner::TopRight => Anchor::TopRight,
|
||||
Corner::BottomLeft => Anchor::BottomLeft,
|
||||
Corner::BottomRight => Anchor::BottomRight,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Anchor> for Corner {
|
||||
fn from(anchor: Anchor) -> Self {
|
||||
match anchor {
|
||||
Anchor::TopLeft => Corner::TopLeft,
|
||||
Anchor::TopRight => Corner::TopRight,
|
||||
Anchor::BottomLeft => Corner::BottomLeft,
|
||||
Anchor::BottomRight => Corner::BottomRight,
|
||||
Anchor::TopCenter => Corner::TopLeft,
|
||||
Anchor::BottomCenter => Corner::BottomLeft,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A enum for defining the side of the element.
|
||||
///
|
||||
/// See also: [`Placement`] if you need to define the 4 edges.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum Side {
|
||||
#[serde(rename = "left")]
|
||||
Left,
|
||||
#[serde(rename = "right")]
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Side {
|
||||
/// Returns true if the side is left.
|
||||
#[inline]
|
||||
pub fn is_left(&self) -> bool {
|
||||
matches!(self, Self::Left)
|
||||
}
|
||||
|
||||
/// Returns true if the side is right.
|
||||
#[inline]
|
||||
pub fn is_right(&self) -> bool {
|
||||
matches!(self, Self::Right)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to extend the [`Axis`] enum with utility methods.
|
||||
pub trait AxisExt {
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_horizontal(self) -> bool;
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn is_vertical(self) -> bool;
|
||||
}
|
||||
|
||||
impl AxisExt for Axis {
|
||||
#[inline]
|
||||
fn is_horizontal(self) -> bool {
|
||||
self == Axis::Horizontal
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_vertical(self) -> bool {
|
||||
self == Axis::Vertical
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for converting [`Pixels`] to `f32` and `f64`.
|
||||
pub trait PixelsExt {
|
||||
fn as_f32(&self) -> f32;
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
fn as_f64(self) -> f64;
|
||||
}
|
||||
impl PixelsExt for Pixels {
|
||||
fn as_f32(&self) -> f32 {
|
||||
f32::from(self)
|
||||
}
|
||||
|
||||
fn as_f64(self) -> f64 {
|
||||
f64::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to extend the [`Length`] enum with utility methods.
|
||||
pub trait LengthExt {
|
||||
/// Converts the [`Length`] to [`Pixels`] based on a given `base_size` and `rem_size`.
|
||||
///
|
||||
/// If the [`Length`] is [`Length::Auto`], it returns `None`.
|
||||
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels>;
|
||||
}
|
||||
|
||||
impl LengthExt for Length {
|
||||
fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Option<Pixels> {
|
||||
match self {
|
||||
Length::Auto => None,
|
||||
Length::Definite(len) => Some(len.to_pixels(base_size, rem_size)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A struct for defining the edges of an element.
|
||||
///
|
||||
/// A extend version of [`gpui::Edges`] to serialize/deserialize.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub struct Edges<T: Clone + Debug + Default + PartialEq> {
|
||||
/// The size of the top edge.
|
||||
pub top: T,
|
||||
/// The size of the right edge.
|
||||
pub right: T,
|
||||
/// The size of the bottom edge.
|
||||
pub bottom: T,
|
||||
/// The size of the left edge.
|
||||
pub left: T,
|
||||
}
|
||||
|
||||
impl<T> Edges<T>
|
||||
where
|
||||
T: Clone + Debug + Default + PartialEq,
|
||||
{
|
||||
/// Creates a new `Edges` instance with all edges set to the same value.
|
||||
pub fn all(value: T) -> Self {
|
||||
Self {
|
||||
top: value.clone(),
|
||||
right: value.clone(),
|
||||
bottom: value.clone(),
|
||||
left: value,
|
||||
}
|
||||
}
|
||||
}
|
||||
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 event::InteractiveElementExt;
|
||||
pub use focusable::FocusableCycle;
|
||||
pub use geometry::*;
|
||||
pub use icon::*;
|
||||
pub use index_path::IndexPath;
|
||||
pub use kbd::*;
|
||||
pub use menu::{context_menu, popup_menu};
|
||||
pub use root::{window_paddings, Root};
|
||||
pub use styled::*;
|
||||
pub use window_ext::*;
|
||||
@@ -16,7 +18,6 @@ pub mod avatar;
|
||||
pub mod button;
|
||||
pub mod checkbox;
|
||||
pub mod divider;
|
||||
pub mod dropdown;
|
||||
pub mod history;
|
||||
pub mod indicator;
|
||||
pub mod input;
|
||||
@@ -30,10 +31,13 @@ pub mod skeleton;
|
||||
pub mod switch;
|
||||
pub mod tooltip;
|
||||
|
||||
mod anchored;
|
||||
mod element_ext;
|
||||
mod event;
|
||||
mod focusable;
|
||||
mod geometry;
|
||||
mod icon;
|
||||
mod index_path;
|
||||
mod kbd;
|
||||
mod root;
|
||||
mod styled;
|
||||
@@ -44,7 +48,6 @@ mod window_ext;
|
||||
/// This must be called before using any of the UI components.
|
||||
/// You can initialize the UI module at your application's entry point.
|
||||
pub fn init(cx: &mut gpui::App) {
|
||||
dropdown::init(cx);
|
||||
input::init(cx);
|
||||
list::init(cx);
|
||||
modal::init(cx);
|
||||
|
||||
221
crates/ui/src/list/cache.rs
Normal file
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,57 @@
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement, MouseButton,
|
||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _, Styled,
|
||||
Window,
|
||||
div, AnyElement, App, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
|
||||
MouseMoveEvent, ParentElement, RenderOnce, Stateful, StatefulInteractiveElement as _,
|
||||
StyleRefinement, Styled, Window,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::{h_flex, Disableable, Icon, IconName, Selectable, Sizable as _};
|
||||
use crate::{h_flex, Disableable, Icon, Selectable, Sizable as _, StyledExt};
|
||||
|
||||
type OnClick = Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>;
|
||||
type OnMouseEnter = Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>;
|
||||
type Suffix = Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
enum ListItemMode {
|
||||
#[default]
|
||||
Entry,
|
||||
Separator,
|
||||
}
|
||||
|
||||
impl ListItemMode {
|
||||
#[inline]
|
||||
fn is_separator(&self) -> bool {
|
||||
matches!(self, ListItemMode::Separator)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
base: Stateful<Div>,
|
||||
mode: ListItemMode,
|
||||
style: StyleRefinement,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
secondary_selected: bool,
|
||||
confirmed: bool,
|
||||
check_icon: Option<Icon>,
|
||||
on_click: OnClick,
|
||||
on_mouse_enter: OnMouseEnter,
|
||||
suffix: Suffix,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_mouse_enter: Option<Box<dyn Fn(&MouseMoveEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListItem {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
|
||||
Self {
|
||||
base: h_flex().id(id).gap_x_1().py_1().px_2().text_base(),
|
||||
mode: ListItemMode::Entry,
|
||||
base: h_flex().id(id),
|
||||
style: StyleRefinement::default(),
|
||||
disabled: false,
|
||||
selected: false,
|
||||
secondary_selected: false,
|
||||
confirmed: false,
|
||||
on_click: None,
|
||||
on_mouse_enter: None,
|
||||
@@ -43,9 +61,15 @@ impl ListItem {
|
||||
}
|
||||
}
|
||||
|
||||
/// Set this list item to as a separator, it not able to be selected.
|
||||
pub fn separator(mut self) -> Self {
|
||||
self.mode = ListItemMode::Separator;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set to show check icon, default is None.
|
||||
pub fn check_icon(mut self, icon: IconName) -> Self {
|
||||
self.check_icon = Some(Icon::new(icon));
|
||||
pub fn check_icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.check_icon = Some(icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -111,11 +135,16 @@ impl Selectable for ListItem {
|
||||
fn is_selected(&self) -> bool {
|
||||
self.selected
|
||||
}
|
||||
|
||||
fn secondary_selected(mut self, selected: bool) -> Self {
|
||||
self.secondary_selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Styled for ListItem {
|
||||
fn style(&mut self) -> &mut gpui::StyleRefinement {
|
||||
self.base.style()
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,35 +156,39 @@ impl ParentElement for ListItem {
|
||||
|
||||
impl RenderOnce for ListItem {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let is_active = self.selected || self.confirmed;
|
||||
let is_active = self.confirmed || self.selected;
|
||||
|
||||
let corner_radii = self.style.corner_radii.clone();
|
||||
|
||||
let _selected_style = StyleRefinement {
|
||||
corner_radii,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let is_selectable = !(self.disabled || self.mode.is_separator());
|
||||
|
||||
self.base
|
||||
.relative()
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_3()
|
||||
.text_base()
|
||||
.text_color(cx.theme().text)
|
||||
.relative()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
if !self.disabled {
|
||||
this.cursor_pointer()
|
||||
.on_mouse_down(MouseButton::Left, move |_, _window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(on_click)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(is_active, |this| this.bg(cx.theme().element_active))
|
||||
.when(!is_active && !self.disabled, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().elevated_surface_background))
|
||||
})
|
||||
// Mouse enter
|
||||
.refine_style(&self.style)
|
||||
.when(is_selectable, |this| {
|
||||
this.when_some(self.on_click, |this, on_click| this.on_click(on_click))
|
||||
.when_some(self.on_mouse_enter, |this, on_mouse_enter| {
|
||||
if !self.disabled {
|
||||
this.on_mouse_move(move |ev, window, cx| (on_mouse_enter)(ev, window, cx))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
.when(!is_active, |this| {
|
||||
this.hover(|this| this.bg(cx.theme().ghost_element_hover))
|
||||
})
|
||||
})
|
||||
.when(!is_selectable, |this| {
|
||||
this.text_color(cx.theme().text_muted)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -177,5 +210,17 @@ impl RenderOnce for ListItem {
|
||||
}),
|
||||
)
|
||||
.when_some(self.suffix, |this, suffix| this.child(suffix(window, cx)))
|
||||
.map(|this| {
|
||||
if is_selectable && (self.selected || self.secondary_selected) {
|
||||
let bg = if self.selected {
|
||||
cx.theme().ghost_element_active
|
||||
} else {
|
||||
cx.theme().ghost_element_background
|
||||
};
|
||||
this.bg(bg)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ impl RenderOnce for LoadingItem {
|
||||
.gap_1p5()
|
||||
.overflow_hidden()
|
||||
.child(Skeleton::new().h_5().w_48().max_w_full())
|
||||
.child(Skeleton::new().secondary(true).h_3().w_64().max_w_full()),
|
||||
.child(Skeleton::new().secondary().h_3().w_64().max_w_full()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
pub(crate) mod cache;
|
||||
mod delegate;
|
||||
#[allow(clippy::module_inception)]
|
||||
mod list;
|
||||
mod list_item;
|
||||
mod loading;
|
||||
mod separator_item;
|
||||
|
||||
pub use delegate::*;
|
||||
pub use list::*;
|
||||
pub use list_item::*;
|
||||
pub use separator_item::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Settings for List.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListSettings {
|
||||
/// Whether to use active highlight style on ListItem, default
|
||||
pub active_highlight: bool,
|
||||
}
|
||||
|
||||
impl Default for ListSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
active_highlight: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
50
crates/ui/src/list/separator_item.rs
Normal file
50
crates/ui/src/list/separator_item.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use gpui::{AnyElement, ParentElement, RenderOnce, StyleRefinement};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::list::ListItem;
|
||||
use crate::{Selectable, StyledExt};
|
||||
|
||||
pub struct ListSeparatorItem {
|
||||
style: StyleRefinement,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl ListSeparatorItem {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
style: StyleRefinement::default(),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ListSeparatorItem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ListSeparatorItem {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl Selectable for ListSeparatorItem {
|
||||
fn selected(self, _: bool) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
fn is_selected(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListSeparatorItem {
|
||||
fn render(self, _: &mut gpui::Window, _: &mut gpui::App) -> impl gpui::IntoElement {
|
||||
ListItem::new("separator")
|
||||
.refine_style(&self.style)
|
||||
.children(self.children)
|
||||
.disabled(true)
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, deferred, div, px, App, AppContext as _, ClickEvent, Context, DismissEvent, Entity,
|
||||
Focusable, InteractiveElement as _, IntoElement, KeyBinding, OwnedMenu, ParentElement, Render,
|
||||
SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
Focusable, InteractiveElement as _, IntoElement, KeyBinding, MouseButton, OwnedMenu,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Window,
|
||||
};
|
||||
|
||||
use crate::actions::{Cancel, SelectLeft, SelectRight};
|
||||
use crate::button::{Button, ButtonVariants};
|
||||
use crate::popup_menu::PopupMenu;
|
||||
use crate::menu::PopupMenu;
|
||||
use crate::{h_flex, Selectable, Sizable};
|
||||
|
||||
const CONTEXT: &str = "AppMenuBar";
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.bind_keys([
|
||||
KeyBinding::new("escape", Cancel, Some(CONTEXT)),
|
||||
@@ -22,67 +23,74 @@ pub fn init(cx: &mut App) {
|
||||
/// The application menu bar, for Windows and Linux.
|
||||
pub struct AppMenuBar {
|
||||
menus: Vec<Entity<AppMenu>>,
|
||||
selected_ix: Option<usize>,
|
||||
selected_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl AppMenuBar {
|
||||
/// Create a new app menu bar.
|
||||
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
|
||||
pub fn new(cx: &mut App) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let mut this = Self {
|
||||
selected_index: None,
|
||||
menus: Vec::new(),
|
||||
};
|
||||
this.reload(cx);
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
/// Reload the menus from the app.
|
||||
pub fn reload(&mut self, cx: &mut Context<Self>) {
|
||||
let menu_bar = cx.entity();
|
||||
let menus = cx
|
||||
self.menus = cx
|
||||
.get_menus()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), window, cx))
|
||||
.map(|(ix, menu)| AppMenu::new(ix, menu, menu_bar.clone(), cx))
|
||||
.collect();
|
||||
|
||||
Self {
|
||||
selected_ix: None,
|
||||
menus,
|
||||
}
|
||||
})
|
||||
self.selected_index = None;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_ix) = self.selected_ix else {
|
||||
fn on_move_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_index) = self.selected_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_ix = if selected_ix == 0 {
|
||||
let new_ix = if selected_index == 0 {
|
||||
self.menus.len().saturating_sub(1)
|
||||
} else {
|
||||
selected_ix.saturating_sub(1)
|
||||
selected_index.saturating_sub(1)
|
||||
};
|
||||
self.set_selected_ix(Some(new_ix), window, cx);
|
||||
self.set_selected_index(Some(new_ix), window, cx);
|
||||
}
|
||||
|
||||
fn move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_ix) = self.selected_ix else {
|
||||
fn on_move_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(selected_index) = self.selected_index else {
|
||||
return;
|
||||
};
|
||||
|
||||
let new_ix = if selected_ix + 1 >= self.menus.len() {
|
||||
let new_ix = if selected_index + 1 >= self.menus.len() {
|
||||
0
|
||||
} else {
|
||||
selected_ix + 1
|
||||
selected_index + 1
|
||||
};
|
||||
self.set_selected_ix(Some(new_ix), window, cx);
|
||||
self.set_selected_index(Some(new_ix), window, cx);
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_selected_ix(None, window, cx);
|
||||
fn on_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_selected_index(None, window, cx);
|
||||
}
|
||||
|
||||
fn set_selected_ix(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_ix = ix;
|
||||
fn set_selected_index(&mut self, ix: Option<usize>, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn has_activated_menu(&self) -> bool {
|
||||
self.selected_ix.is_some()
|
||||
self.selected_index.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +99,9 @@ impl Render for AppMenuBar {
|
||||
h_flex()
|
||||
.id("app-menu-bar")
|
||||
.key_context(CONTEXT)
|
||||
.on_action(cx.listener(Self::move_left))
|
||||
.on_action(cx.listener(Self::move_right))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::on_move_left))
|
||||
.on_action(cx.listener(Self::on_move_right))
|
||||
.on_action(cx.listener(Self::on_cancel))
|
||||
.size_full()
|
||||
.gap_x_1()
|
||||
.overflow_x_scroll()
|
||||
@@ -117,7 +125,6 @@ impl AppMenu {
|
||||
ix: usize,
|
||||
menu: &OwnedMenu,
|
||||
menu_bar: Entity<AppMenuBar>,
|
||||
_: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Entity<Self> {
|
||||
let name = menu.name.clone();
|
||||
@@ -173,7 +180,7 @@ impl AppMenu {
|
||||
self._subscription.take();
|
||||
self.popup_menu.take();
|
||||
self.menu_bar.update(cx, |state, cx| {
|
||||
state.cancel(&Cancel, window, cx);
|
||||
state.on_cancel(&Cancel, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,11 +190,11 @@ impl AppMenu {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let is_selected = self.menu_bar.read(cx).selected_ix == Some(self.ix);
|
||||
let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix);
|
||||
|
||||
self.menu_bar.update(cx, |state, cx| {
|
||||
let new_ix = if is_selected { None } else { Some(self.ix) };
|
||||
state.set_selected_ix(new_ix, window, cx);
|
||||
state.set_selected_index(new_ix, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -202,7 +209,7 @@ impl AppMenu {
|
||||
}
|
||||
|
||||
self.menu_bar.update(cx, |state, cx| {
|
||||
state.set_selected_ix(Some(self.ix), window, cx);
|
||||
state.set_selected_index(Some(self.ix), window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -210,7 +217,7 @@ impl AppMenu {
|
||||
impl Render for AppMenu {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let menu_bar = self.menu_bar.read(cx);
|
||||
let is_selected = menu_bar.selected_ix == Some(self.ix);
|
||||
let is_selected = menu_bar.selected_index == Some(self.ix);
|
||||
|
||||
div()
|
||||
.id(self.ix)
|
||||
@@ -219,10 +226,15 @@ impl Render for AppMenu {
|
||||
Button::new("menu")
|
||||
.small()
|
||||
.py_0p5()
|
||||
.xsmall()
|
||||
.compact()
|
||||
.ghost()
|
||||
.label(self.name.clone())
|
||||
.selected(is_selected)
|
||||
.on_mouse_down(MouseButton::Left, |_, window, cx| {
|
||||
// Stop propagation to avoid dragging the window.
|
||||
window.prevent_default();
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_click(cx.listener(Self::handle_trigger_click)),
|
||||
)
|
||||
.on_hover(cx.listener(Self::handle_hover))
|
||||
|
||||
@@ -3,49 +3,66 @@ use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, deferred, div, px, relative, AnyElement, App, Context, Corner, DismissEvent, Element,
|
||||
ElementId, Entity, Focusable, GlobalElementId, InspectorElementId, InteractiveElement,
|
||||
IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Position, Stateful,
|
||||
Style, Subscription, Window,
|
||||
anchored, deferred, div, px, AnyElement, App, Context, Corner, DismissEvent, Element,
|
||||
ElementId, Entity, Focusable, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId,
|
||||
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
StyleRefinement, Styled, Subscription, Window,
|
||||
};
|
||||
|
||||
use crate::popup_menu::PopupMenu;
|
||||
use crate::menu::PopupMenu;
|
||||
|
||||
pub trait ContextMenuExt: ParentElement + Sized {
|
||||
/// A extension trait for adding a context menu to an element.
|
||||
pub trait ContextMenuExt: ParentElement + Styled {
|
||||
/// Add a context menu to the element.
|
||||
///
|
||||
/// This will changed the element to be `relative` positioned, and add a child `ContextMenu` element.
|
||||
/// Because the `ContextMenu` element is positioned `absolute`, it will not affect the layout of the parent element.
|
||||
fn context_menu(
|
||||
self,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Self {
|
||||
self.child(ContextMenu::new("context-menu").menu(f))
|
||||
) -> ContextMenu<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// Generate a unique ID based on the element's memory address to ensure
|
||||
// each context menu has its own state and doesn't share with others
|
||||
let id = format!("context-menu-{:p}", &self as *const _);
|
||||
ContextMenu::new(id, self).menu(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ContextMenuExt for Stateful<E> where E: ParentElement {}
|
||||
impl<E: ParentElement + Styled> ContextMenuExt for E {}
|
||||
|
||||
/// A context menu that can be shown on right-click.
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub struct ContextMenu {
|
||||
pub struct ContextMenu<E: ParentElement + Styled + Sized> {
|
||||
id: ElementId,
|
||||
menu:
|
||||
Option<Box<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static>>,
|
||||
element: Option<E>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
|
||||
// This is not in use, just for style refinement forwarding.
|
||||
_ignore_style: StyleRefinement,
|
||||
anchor: Corner,
|
||||
}
|
||||
|
||||
impl ContextMenu {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
impl<E: ParentElement + Styled> ContextMenu<E> {
|
||||
/// Create a new context menu with the given ID.
|
||||
pub fn new(id: impl Into<ElementId>, element: E) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
element: Some(element),
|
||||
menu: None,
|
||||
anchor: Corner::TopLeft,
|
||||
_ignore_style: StyleRefinement::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the context menu using the given builder function.
|
||||
#[must_use]
|
||||
pub fn menu<F>(mut self, builder: F) -> Self
|
||||
fn menu<F>(mut self, builder: F) -> Self
|
||||
where
|
||||
F: Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
{
|
||||
self.menu = Some(Box::new(builder));
|
||||
self.menu = Some(Rc::new(builder));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -67,7 +84,25 @@ impl ContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for ContextMenu {
|
||||
impl<E: ParentElement + Styled> ParentElement for ContextMenu<E> {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.extend(elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled> Styled for ContextMenu<E> {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.style()
|
||||
} else {
|
||||
&mut self._ignore_style
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: ParentElement + Styled + IntoElement + 'static> IntoElement for ContextMenu<E> {
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
@@ -83,14 +118,14 @@ struct ContextMenuSharedState {
|
||||
}
|
||||
|
||||
pub struct ContextMenuState {
|
||||
menu_element: Option<AnyElement>,
|
||||
element: Option<AnyElement>,
|
||||
shared_state: Rc<RefCell<ContextMenuSharedState>>,
|
||||
}
|
||||
|
||||
impl Default for ContextMenuState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
menu_element: None,
|
||||
element: None,
|
||||
shared_state: Rc::new(RefCell::new(ContextMenuSharedState {
|
||||
menu_view: None,
|
||||
open: false,
|
||||
@@ -101,8 +136,8 @@ impl Default for ContextMenuState {
|
||||
}
|
||||
}
|
||||
|
||||
impl Element for ContextMenu {
|
||||
type PrepaintState = ();
|
||||
impl<E: ParentElement + Styled + IntoElement + 'static> Element for ContextMenu<E> {
|
||||
type PrepaintState = Hitbox;
|
||||
type RequestLayoutState = ContextMenuState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
@@ -113,7 +148,6 @@ impl Element for ContextMenu {
|
||||
None
|
||||
}
|
||||
|
||||
#[allow(clippy::field_reassign_with_default)]
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
@@ -121,71 +155,73 @@ impl Element for ContextMenu {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
// Set the layout style relative to the table view to get same size.
|
||||
style.position = Position::Absolute;
|
||||
style.flex_grow = 1.0;
|
||||
style.flex_shrink = 1.0;
|
||||
style.size.width = relative(1.).into();
|
||||
style.size.height = relative(1.).into();
|
||||
|
||||
let anchor = self.anchor;
|
||||
|
||||
self.with_element_state(
|
||||
id.unwrap(),
|
||||
window,
|
||||
cx,
|
||||
|_, state: &mut ContextMenuState, window, cx| {
|
||||
|this, state: &mut ContextMenuState, window, cx| {
|
||||
let (position, open) = {
|
||||
let shared_state = state.shared_state.borrow();
|
||||
(shared_state.position, shared_state.open)
|
||||
};
|
||||
let menu_view = state.shared_state.borrow().menu_view.clone();
|
||||
let (menu_element, menu_layout_id) = if open {
|
||||
let mut menu_element = None;
|
||||
if open {
|
||||
let has_menu_item = menu_view
|
||||
.as_ref()
|
||||
.map(|menu| !menu.read(cx).is_empty())
|
||||
.unwrap_or(false);
|
||||
|
||||
if has_menu_item {
|
||||
let mut menu_element = deferred(
|
||||
menu_element = Some(
|
||||
deferred(
|
||||
anchored().child(
|
||||
div()
|
||||
.w(window.bounds().size.width)
|
||||
.h(window.bounds().size.height)
|
||||
.on_scroll_wheel(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.child(
|
||||
anchored()
|
||||
.position(position)
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.anchor(anchor)
|
||||
.when_some(menu_view, |this, menu| {
|
||||
// Focus the menu, so that can be handle the action.
|
||||
if !menu.focus_handle(cx).contains_focused(window, cx) {
|
||||
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();
|
||||
|
||||
let menu_layout_id = menu_element.request_layout(window, cx);
|
||||
(Some(menu_element), Some(menu_layout_id))
|
||||
} else {
|
||||
(None, None)
|
||||
.into_any(),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let mut layout_ids = vec![];
|
||||
if let Some(menu_layout_id) = menu_layout_id {
|
||||
layout_ids.push(menu_layout_id);
|
||||
}
|
||||
|
||||
let layout_id = window.request_layout(style, layout_ids, cx);
|
||||
let mut element = this
|
||||
.element
|
||||
.take()
|
||||
.expect("Element should exists.")
|
||||
.children(menu_element)
|
||||
.into_any_element();
|
||||
|
||||
let layout_id = element.request_layout(window, cx);
|
||||
|
||||
(
|
||||
layout_id,
|
||||
ContextMenuState {
|
||||
menu_element,
|
||||
|
||||
element: Some(element),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
@@ -197,33 +233,33 @@ impl Element for ContextMenu {
|
||||
&mut self,
|
||||
_: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: gpui::Bounds<gpui::Pixels>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
if let Some(menu_element) = &mut request_layout.menu_element {
|
||||
menu_element.prepaint(window, cx);
|
||||
if let Some(element) = &mut request_layout.element {
|
||||
element.prepaint(window, cx);
|
||||
}
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
bounds: gpui::Bounds<gpui::Pixels>,
|
||||
_: gpui::Bounds<gpui::Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
hitbox: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(menu_element) = &mut request_layout.menu_element {
|
||||
menu_element.paint(window, cx);
|
||||
if let Some(element) = &mut request_layout.element {
|
||||
element.paint(window, cx);
|
||||
}
|
||||
|
||||
let Some(builder) = self.menu.take() else {
|
||||
return;
|
||||
};
|
||||
// Take the builder before setting up element state to avoid borrow issues
|
||||
let builder = self.menu.clone();
|
||||
|
||||
self.with_element_state(
|
||||
id.unwrap(),
|
||||
@@ -232,34 +268,54 @@ impl Element for ContextMenu {
|
||||
|_view, state: &mut ContextMenuState, window, _| {
|
||||
let shared_state = state.shared_state.clone();
|
||||
|
||||
let hitbox = hitbox.clone();
|
||||
// When right mouse click, to build content menu, and show it at the mouse position.
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase.bubble()
|
||||
&& event.button == MouseButton::Right
|
||||
&& bounds.contains(&event.position)
|
||||
&& hitbox.is_hovered(window)
|
||||
{
|
||||
{
|
||||
let mut shared_state = shared_state.borrow_mut();
|
||||
// Clear any existing menu view to allow immediate replacement
|
||||
// Set the new position and open the menu
|
||||
shared_state.menu_view = None;
|
||||
shared_state._subscription = None;
|
||||
shared_state.position = event.position;
|
||||
shared_state.open = true;
|
||||
}
|
||||
|
||||
let menu = PopupMenu::build(window, cx, |menu, window, cx| {
|
||||
(builder)(menu, window, cx)
|
||||
// Use defer to build the menu in the next frame, avoiding race conditions
|
||||
window.defer(cx, {
|
||||
let shared_state = shared_state.clone();
|
||||
let builder = builder.clone();
|
||||
move |window, cx| {
|
||||
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
|
||||
let Some(build) = &builder else {
|
||||
return menu;
|
||||
};
|
||||
build(menu, window, cx)
|
||||
});
|
||||
|
||||
// Set up the subscription for dismiss handling
|
||||
let _subscription = window.subscribe(&menu, cx, {
|
||||
let shared_state = shared_state.clone();
|
||||
move |_, _: &DismissEvent, window, _| {
|
||||
move |_, _: &DismissEvent, window, _cx| {
|
||||
shared_state.borrow_mut().open = false;
|
||||
window.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
shared_state.borrow_mut().menu_view = Some(menu.clone());
|
||||
shared_state.borrow_mut()._subscription = Some(_subscription);
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
142
crates/ui/src/menu/dropdown_menu.rs
Normal file
142
crates/ui/src/menu/dropdown_menu.rs
Normal file
@@ -0,0 +1,142 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
Context, Corner, DismissEvent, ElementId, Entity, Focusable, InteractiveElement, IntoElement,
|
||||
RenderOnce, SharedString, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use crate::button::Button;
|
||||
use crate::menu::PopupMenu;
|
||||
use crate::popover::Popover;
|
||||
use crate::Selectable;
|
||||
|
||||
/// A dropdown menu trait for buttons and other interactive elements
|
||||
pub trait DropdownMenu: Styled + Selectable + InteractiveElement + IntoElement + 'static {
|
||||
/// Create a dropdown menu with the given items, anchored to the TopLeft corner
|
||||
fn dropdown_menu(
|
||||
self,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> DropdownMenuPopover<Self> {
|
||||
self.dropdown_menu_with_anchor(Corner::TopLeft, f)
|
||||
}
|
||||
|
||||
/// Create a dropdown menu with the given items, anchored to the given corner
|
||||
fn dropdown_menu_with_anchor(
|
||||
mut self,
|
||||
anchor: impl Into<Corner>,
|
||||
f: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> DropdownMenuPopover<Self> {
|
||||
let style = self.style().clone();
|
||||
let id = self.interactivity().element_id.clone();
|
||||
|
||||
DropdownMenuPopover::new(id.unwrap_or(0.into()), anchor, self, f).trigger_style(style)
|
||||
}
|
||||
}
|
||||
|
||||
impl DropdownMenu for Button {}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
|
||||
id: ElementId,
|
||||
style: StyleRefinement,
|
||||
anchor: Corner,
|
||||
trigger: T,
|
||||
#[allow(clippy::type_complexity)]
|
||||
builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
|
||||
}
|
||||
|
||||
impl<T> DropdownMenuPopover<T>
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
fn new(
|
||||
id: ElementId,
|
||||
anchor: impl Into<Corner>,
|
||||
trigger: T,
|
||||
builder: impl Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu + 'static,
|
||||
) -> Self {
|
||||
Self {
|
||||
id: SharedString::from(format!("dropdown-menu:{:?}", id)).into(),
|
||||
style: StyleRefinement::default(),
|
||||
anchor: anchor.into(),
|
||||
trigger,
|
||||
builder: Rc::new(builder),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the anchor corner for the dropdown menu popover.
|
||||
pub fn anchor(mut self, anchor: impl Into<Corner>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style refinement for the dropdown menu trigger.
|
||||
fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DropdownMenuState {
|
||||
menu: Option<Entity<PopupMenu>>,
|
||||
}
|
||||
|
||||
impl<T> RenderOnce for DropdownMenuPopover<T>
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
|
||||
let builder = self.builder.clone();
|
||||
let menu_state =
|
||||
window.use_keyed_state(self.id.clone(), cx, |_, _| DropdownMenuState::default());
|
||||
|
||||
Popover::new(SharedString::from(format!("popover:{}", self.id)))
|
||||
.appearance(false)
|
||||
.overlay_closable(false)
|
||||
.trigger(self.trigger)
|
||||
.trigger_style(self.style)
|
||||
.anchor(self.anchor)
|
||||
.content(move |_, window, cx| {
|
||||
// Here is special logic to only create the PopupMenu once and reuse it.
|
||||
// Because this `content` will called in every time render, so we need to store the menu
|
||||
// in state to avoid recreating at every render.
|
||||
//
|
||||
// And we also need to rebuild the menu when it is dismissed, to rebuild menu items
|
||||
// dynamically for support `dropdown_menu` method, so we listen for DismissEvent below.
|
||||
let menu = match menu_state.read(cx).menu.clone() {
|
||||
Some(menu) => menu,
|
||||
None => {
|
||||
let builder = builder.clone();
|
||||
let menu = PopupMenu::build(window, cx, move |menu, window, cx| {
|
||||
builder(menu, window, cx)
|
||||
});
|
||||
menu_state.update(cx, |state, _| {
|
||||
state.menu = Some(menu.clone());
|
||||
});
|
||||
menu.focus_handle(cx).focus(window, cx);
|
||||
|
||||
// Listen for dismiss events from the PopupMenu to close the popover.
|
||||
let popover_state = cx.entity();
|
||||
window
|
||||
.subscribe(&menu, cx, {
|
||||
let menu_state = menu_state.clone();
|
||||
move |_, _: &DismissEvent, window, cx| {
|
||||
popover_state.update(cx, |state, cx| {
|
||||
state.dismiss(window, cx);
|
||||
});
|
||||
menu_state.update(cx, |state, _| {
|
||||
state.menu = None;
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
menu.clone()
|
||||
}
|
||||
};
|
||||
|
||||
menu.clone()
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -10,20 +10,22 @@ use theme::ActiveTheme;
|
||||
use crate::{h_flex, Disableable, StyledExt};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub(crate) struct MenuItemElement {
|
||||
id: ElementId,
|
||||
group_name: SharedString,
|
||||
style: StyleRefinement,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_hover: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl MenuItemElement {
|
||||
pub fn new(id: impl Into<ElementId>, group_name: impl Into<SharedString>) -> Self {
|
||||
/// Create a new MenuItem with the given ID and group name.
|
||||
pub(crate) fn new(id: impl Into<ElementId>, group_name: impl Into<SharedString>) -> Self {
|
||||
let id: ElementId = id.into();
|
||||
Self {
|
||||
id: id.clone(),
|
||||
@@ -38,17 +40,19 @@ impl MenuItemElement {
|
||||
}
|
||||
|
||||
/// Set ListItem as the selected item style.
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
pub(crate) fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
/// Set the disabled state of the MenuItem.
|
||||
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
/// Set a handler for when the MenuItem is clicked.
|
||||
pub(crate) fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
@@ -88,7 +92,7 @@ impl RenderOnce for MenuItemElement {
|
||||
h_flex()
|
||||
.id(self.id)
|
||||
.group(&self.group_name)
|
||||
.gap_x_2()
|
||||
.gap_x_1()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.text_base()
|
||||
@@ -102,12 +106,12 @@ impl RenderOnce for MenuItemElement {
|
||||
})
|
||||
.when(!self.disabled, |this| {
|
||||
this.group_hover(self.group_name, |this| {
|
||||
this.bg(cx.theme().elevated_surface_background)
|
||||
.text_color(cx.theme().text)
|
||||
this.bg(cx.theme().secondary_background)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
})
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().elevated_surface_background)
|
||||
.text_color(cx.theme().text)
|
||||
this.bg(cx.theme().secondary_background)
|
||||
.text_color(cx.theme().secondary_foreground)
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
use gpui::App;
|
||||
|
||||
mod app_menu_bar;
|
||||
mod context_menu;
|
||||
mod dropdown_menu;
|
||||
mod menu_item;
|
||||
|
||||
pub mod context_menu;
|
||||
pub mod popup_menu;
|
||||
mod popup_menu;
|
||||
|
||||
pub use app_menu_bar::AppMenuBar;
|
||||
pub use context_menu::{ContextMenu, ContextMenuExt, ContextMenuState};
|
||||
pub use dropdown_menu::DropdownMenu;
|
||||
pub use popup_menu::{PopupMenu, PopupMenuItem};
|
||||
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
app_menu_bar::init(cx);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ use std::time::Duration;
|
||||
|
||||
use gpui::prelude::FluentBuilder;
|
||||
use gpui::{
|
||||
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Axis, Bounds,
|
||||
anchored, div, hsla, point, px, Animation, AnimationExt as _, AnyElement, App, Bounds,
|
||||
BoxShadow, ClickEvent, Div, FocusHandle, InteractiveElement, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement, Pixels, Point, RenderOnce, SharedString, StyleRefinement, Styled,
|
||||
Window,
|
||||
@@ -13,6 +13,7 @@ use theme::ActiveTheme;
|
||||
use crate::actions::{Cancel, Confirm};
|
||||
use crate::animation::cubic_bezier;
|
||||
use crate::button::{Button, ButtonCustomVariant, ButtonVariant, ButtonVariants as _};
|
||||
use crate::scroll::ScrollableElement;
|
||||
use crate::{h_flex, v_flex, IconName, Root, Sizable, StyledExt, WindowExtension};
|
||||
|
||||
const CONTEXT: &str = "Modal";
|
||||
@@ -489,13 +490,13 @@ impl RenderOnce for Modal {
|
||||
.w_full()
|
||||
.h_auto()
|
||||
.flex_1()
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
v_flex()
|
||||
.pr(padding_right)
|
||||
.pl(padding_left)
|
||||
.scrollable(Axis::Vertical)
|
||||
.size_full()
|
||||
.overflow_y_scrollbar()
|
||||
.child(self.content),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1,129 +1,78 @@
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
actions, anchored, deferred, div, px, AnyElement, App, Bounds, Context, Corner, DismissEvent,
|
||||
DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render,
|
||||
ScrollHandle, StatefulInteractiveElement, Style, StyleRefinement, Styled, Window,
|
||||
deferred, div, px, AnyElement, App, Bounds, Context, Deferred, DismissEvent, Div, ElementId,
|
||||
EventEmitter, FocusHandle, Focusable, Half, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
MouseButton, ParentElement, Pixels, Point, Render, RenderOnce, Stateful, StyleRefinement,
|
||||
Styled, Subscription, Window,
|
||||
};
|
||||
|
||||
use crate::{Selectable, StyledExt as _};
|
||||
use crate::actions::Cancel;
|
||||
use crate::{anchored, v_flex, Anchor, ElementExt, Selectable, StyledExt as _};
|
||||
|
||||
const CONTEXT: &str = "Popover";
|
||||
|
||||
actions!(popover, [Escape]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.bind_keys([KeyBinding::new("escape", Escape, Some(CONTEXT))])
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
|
||||
}
|
||||
|
||||
type PopoverChild<T> = Rc<dyn Fn(&mut Window, &mut Context<T>) -> AnyElement>;
|
||||
|
||||
pub struct PopoverContent {
|
||||
focus_handle: FocusHandle,
|
||||
scroll_handle: ScrollHandle,
|
||||
max_width: Option<Pixels>,
|
||||
max_height: Option<Pixels>,
|
||||
scrollable: bool,
|
||||
child: PopoverChild<Self>,
|
||||
}
|
||||
|
||||
impl PopoverContent {
|
||||
pub fn new<B>(_window: &mut Window, cx: &mut App, content: B) -> Self
|
||||
where
|
||||
B: Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
|
||||
{
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = ScrollHandle::default();
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
scroll_handle,
|
||||
child: Rc::new(content),
|
||||
max_width: None,
|
||||
max_height: None,
|
||||
scrollable: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn max_w(mut self, max_width: Pixels) -> Self {
|
||||
self.max_width = Some(max_width);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_h(mut self, max_height: Pixels) -> Self {
|
||||
self.max_height = Some(max_height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scrollable(mut self) -> Self {
|
||||
self.scrollable = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PopoverContent {}
|
||||
|
||||
impl Focusable for PopoverContent {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PopoverContent {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.id("popup-content")
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context(CONTEXT)
|
||||
.on_action(cx.listener(|_, _: &Escape, _, cx| cx.emit(DismissEvent)))
|
||||
.p_2()
|
||||
.when(self.scrollable, |this| {
|
||||
this.overflow_y_scroll().track_scroll(&self.scroll_handle)
|
||||
})
|
||||
.when_some(self.max_width, |this, v| this.max_w(v))
|
||||
.when_some(self.max_height, |this, v| this.max_h(v))
|
||||
.child(self.child.clone()(window, cx))
|
||||
}
|
||||
}
|
||||
|
||||
type Trigger = Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>;
|
||||
type Content<M> = Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>;
|
||||
|
||||
pub struct Popover<M: ManagedView> {
|
||||
/// A popover element that can be triggered by a button or any other element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Popover {
|
||||
id: ElementId,
|
||||
anchor: Corner,
|
||||
trigger: Trigger,
|
||||
content: Content<M>,
|
||||
style: StyleRefinement,
|
||||
anchor: Anchor,
|
||||
default_open: bool,
|
||||
open: Option<bool>,
|
||||
tracked_focus_handle: Option<FocusHandle>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
content: Option<
|
||||
Rc<
|
||||
dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
|
||||
+ 'static,
|
||||
>,
|
||||
>,
|
||||
children: Vec<AnyElement>,
|
||||
/// Style for trigger element.
|
||||
/// This is used for hotfix the trigger element style to support w_full.
|
||||
trigger_style: Option<StyleRefinement>,
|
||||
mouse_button: MouseButton,
|
||||
no_style: bool,
|
||||
appearance: bool,
|
||||
overlay_closable: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
}
|
||||
|
||||
impl<M> Popover<M>
|
||||
where
|
||||
M: ManagedView,
|
||||
{
|
||||
impl Popover {
|
||||
/// Create a new Popover with `view` mode.
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
anchor: Corner::TopLeft,
|
||||
style: StyleRefinement::default(),
|
||||
anchor: Anchor::TopLeft,
|
||||
trigger: None,
|
||||
trigger_style: None,
|
||||
content: None,
|
||||
tracked_focus_handle: None,
|
||||
children: vec![],
|
||||
mouse_button: MouseButton::Left,
|
||||
no_style: false,
|
||||
appearance: true,
|
||||
overlay_closable: true,
|
||||
default_open: false,
|
||||
open: None,
|
||||
on_open_change: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn anchor(mut self, anchor: Corner) -> Self {
|
||||
self.anchor = anchor;
|
||||
/// Set the anchor corner of the popover, default is `Corner::TopLeft`.
|
||||
///
|
||||
/// This method is kept for backward compatibility with `Corner` type.
|
||||
/// Internally, it converts `Corner` to `Anchor`.
|
||||
pub fn anchor(mut self, anchor: impl Into<Anchor>) -> Self {
|
||||
self.anchor = anchor.into();
|
||||
self
|
||||
}
|
||||
|
||||
@@ -133,29 +82,75 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the trigger element of the popover.
|
||||
pub fn trigger<T>(mut self, trigger: T) -> Self
|
||||
where
|
||||
T: Selectable + IntoElement + 'static,
|
||||
{
|
||||
self.trigger = Some(Box::new(|is_open, _, _| {
|
||||
trigger.selected(is_open).into_any_element()
|
||||
let selected = trigger.is_selected();
|
||||
trigger.selected(selected || is_open).into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the default open state of the popover, default is `false`.
|
||||
///
|
||||
/// This is only used to initialize the open state of the popover.
|
||||
///
|
||||
/// And please note that if you use the `open` method, this value will be ignored.
|
||||
pub fn default_open(mut self, open: bool) -> Self {
|
||||
self.default_open = open;
|
||||
self
|
||||
}
|
||||
|
||||
/// Force set the open state of the popover.
|
||||
///
|
||||
/// If this is set, the popover will be controlled by this value.
|
||||
///
|
||||
/// NOTE: You must be used in conjunction with `on_open_change` to handle state changes.
|
||||
pub fn open(mut self, open: bool) -> Self {
|
||||
self.open = Some(open);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a callback to be called when the open state changes.
|
||||
///
|
||||
/// The first `&bool` parameter is the **new open state**.
|
||||
///
|
||||
/// This is useful when using the `open` method to control the popover state.
|
||||
pub fn on_open_change<F>(mut self, callback: F) -> Self
|
||||
where
|
||||
F: Fn(&bool, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
self.on_open_change = Some(Rc::new(callback));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the style for the trigger element.
|
||||
pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
|
||||
self.trigger_style = Some(style);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content of the popover.
|
||||
/// Set whether clicking outside the popover will dismiss it, default is `true`.
|
||||
pub fn overlay_closable(mut self, closable: bool) -> Self {
|
||||
self.overlay_closable = closable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the content builder for content of the Popover.
|
||||
///
|
||||
/// The `content` is a closure that returns an `AnyElement`.
|
||||
pub fn content<C>(mut self, content: C) -> Self
|
||||
/// This callback will called every time on render the popover.
|
||||
/// So, you should avoid creating new elements or entities in the content closure.
|
||||
pub fn content<F, E>(mut self, content: F) -> Self
|
||||
where
|
||||
C: Fn(&mut Window, &mut App) -> Entity<M> + 'static,
|
||||
E: IntoElement,
|
||||
F: Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> E + 'static,
|
||||
{
|
||||
self.content = Some(Rc::new(content));
|
||||
self.content = Some(Rc::new(move |state, window, cx| {
|
||||
content(state, window, cx).into_any_element()
|
||||
}));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -165,302 +160,265 @@ where
|
||||
///
|
||||
/// - The popover will not have a bg, border, shadow, or padding.
|
||||
/// - The click out of the popover will not dismiss it.
|
||||
pub fn no_style(mut self) -> Self {
|
||||
self.no_style = true;
|
||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||
self.appearance = appearance;
|
||||
self
|
||||
}
|
||||
|
||||
fn render_trigger(&mut self, is_open: bool, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
let Some(trigger) = self.trigger.take() else {
|
||||
return div().into_any_element();
|
||||
/// Bind the focus handle to receive focus when the popover is opened.
|
||||
/// If you not set this, a new focus handle will be created for the popover to
|
||||
///
|
||||
/// If popover is opened, the focus will be moved to the focus handle.
|
||||
pub fn track_focus(mut self, handle: &FocusHandle) -> Self {
|
||||
self.tracked_focus_handle = Some(handle.clone());
|
||||
self
|
||||
}
|
||||
|
||||
fn resolved_corner(anchor: Anchor, trigger_bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||
let offset = if anchor.is_center() {
|
||||
gpui::point(trigger_bounds.size.width.half(), px(0.))
|
||||
} else {
|
||||
Point::default()
|
||||
};
|
||||
|
||||
(trigger)(is_open, window, cx)
|
||||
trigger_bounds.corner(anchor.swap_vertical().into())
|
||||
+ offset
|
||||
+ Point {
|
||||
x: px(0.),
|
||||
y: -trigger_bounds.size.height,
|
||||
}
|
||||
|
||||
fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
|
||||
bounds.corner(match self.anchor {
|
||||
Corner::TopLeft => Corner::BottomLeft,
|
||||
Corner::TopRight => Corner::BottomRight,
|
||||
Corner::BottomLeft => Corner::TopLeft,
|
||||
Corner::BottomRight => Corner::TopRight,
|
||||
})
|
||||
}
|
||||
|
||||
fn with_element_state<R>(
|
||||
&mut self,
|
||||
id: &GlobalElementId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(&mut Self, &mut PopoverElementState<M>, &mut Window, &mut App) -> R,
|
||||
) -> R {
|
||||
window.with_optional_element_state::<PopoverElementState<M>, _>(
|
||||
Some(id),
|
||||
|element_state, window| {
|
||||
let mut element_state = element_state.unwrap().unwrap_or_default();
|
||||
let result = f(self, &mut element_state, window, cx);
|
||||
(result, Some(element_state))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> IntoElement for Popover<M>
|
||||
where
|
||||
M: ManagedView,
|
||||
{
|
||||
type Element = Self;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
impl ParentElement for Popover {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PopoverElementState<M> {
|
||||
trigger_layout_id: Option<LayoutId>,
|
||||
popover_layout_id: Option<LayoutId>,
|
||||
popover_element: Option<AnyElement>,
|
||||
trigger_element: Option<AnyElement>,
|
||||
content_view: Rc<RefCell<Option<Entity<M>>>>,
|
||||
/// Trigger bounds for positioning the popover.
|
||||
trigger_bounds: Option<Bounds<Pixels>>,
|
||||
impl Styled for Popover {
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Default for PopoverElementState<M> {
|
||||
fn default() -> Self {
|
||||
pub struct PopoverState {
|
||||
focus_handle: FocusHandle,
|
||||
pub(crate) tracked_focus_handle: Option<FocusHandle>,
|
||||
trigger_bounds: Bounds<Pixels>,
|
||||
open: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
|
||||
|
||||
_dismiss_subscription: Option<Subscription>,
|
||||
}
|
||||
|
||||
impl PopoverState {
|
||||
pub fn new(default_open: bool, cx: &mut App) -> Self {
|
||||
Self {
|
||||
trigger_layout_id: None,
|
||||
popover_layout_id: None,
|
||||
popover_element: None,
|
||||
trigger_element: None,
|
||||
content_view: Rc::new(RefCell::new(None)),
|
||||
trigger_bounds: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PrepaintState {
|
||||
hitbox: Hitbox,
|
||||
/// Trigger bounds for limit a rect to handle mouse click.
|
||||
trigger_bounds: Option<Bounds<Pixels>>,
|
||||
}
|
||||
|
||||
impl<M: ManagedView> Element for Popover<M> {
|
||||
type PrepaintState = PrepaintState;
|
||||
type RequestLayoutState = PopoverElementState<M>;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&gpui::GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut style = Style::default();
|
||||
|
||||
// FIXME: Remove this and find a better way to handle this.
|
||||
// Apply trigger style, for support w_full for trigger.
|
||||
//
|
||||
// If remove this, the trigger will not support w_full.
|
||||
if let Some(trigger_style) = self.trigger_style.clone() {
|
||||
if let Some(width) = trigger_style.size.width {
|
||||
style.size.width = width;
|
||||
}
|
||||
if let Some(display) = trigger_style.display {
|
||||
style.display = display;
|
||||
focus_handle: cx.focus_handle(),
|
||||
tracked_focus_handle: None,
|
||||
trigger_bounds: Bounds::default(),
|
||||
open: default_open,
|
||||
on_open_change: None,
|
||||
_dismiss_subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
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));
|
||||
/// Check if the popover is open.
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.open
|
||||
}
|
||||
|
||||
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()
|
||||
/// Dismiss the popover if it is open.
|
||||
pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.open {
|
||||
self.toggle_open(window, cx);
|
||||
}
|
||||
})
|
||||
.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;
|
||||
}
|
||||
|
||||
/// Open the popover if it is closed.
|
||||
pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.open {
|
||||
self.toggle_open(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.open = !self.open;
|
||||
if self.open {
|
||||
let state = cx.entity();
|
||||
let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
|
||||
{
|
||||
tracked_focus_handle
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
};
|
||||
focus_handle.focus(window, cx);
|
||||
|
||||
self._dismiss_subscription =
|
||||
Some(
|
||||
window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
|
||||
state.update(cx, |state, cx| {
|
||||
state.dismiss(window, cx);
|
||||
});
|
||||
window.refresh();
|
||||
})
|
||||
}),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
self._dismiss_subscription = None;
|
||||
}
|
||||
|
||||
if let Some(callback) = self.on_open_change.as_ref() {
|
||||
callback(&self.open, window, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.dismiss(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for PopoverState {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PopoverState {
|
||||
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for PopoverState {}
|
||||
|
||||
impl Popover {
|
||||
pub(crate) fn render_popover<E>(
|
||||
anchor: Anchor,
|
||||
trigger_bounds: Bounds<Pixels>,
|
||||
content: E,
|
||||
_: &mut Window,
|
||||
_: &mut App,
|
||||
) -> Deferred
|
||||
where
|
||||
E: IntoElement + 'static,
|
||||
{
|
||||
deferred(
|
||||
anchored()
|
||||
.snap_to_window_with_margin(px(8.))
|
||||
.anchor(anchor)
|
||||
.position(Self::resolved_corner(anchor, trigger_bounds))
|
||||
.child(div().relative().child(content)),
|
||||
)
|
||||
.with_priority(1)
|
||||
.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,
|
||||
pub(crate) fn render_popover_content(
|
||||
anchor: Anchor,
|
||||
appearance: bool,
|
||||
_: &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,
|
||||
) -> 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
_: Option<&gpui::InspectorElementId>,
|
||||
_bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.with_element_state(
|
||||
id.unwrap(),
|
||||
window,
|
||||
cx,
|
||||
|this, element_state, window, cx| {
|
||||
element_state.trigger_bounds = prepaint.trigger_bounds;
|
||||
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)
|
||||
});
|
||||
|
||||
if let Some(mut element) = request_layout.trigger_element.take() {
|
||||
element.paint(window, cx);
|
||||
state.update(cx, |state, _| {
|
||||
if let Some(tracked_focus_handle) = tracked_focus_handle {
|
||||
state.tracked_focus_handle = Some(tracked_focus_handle);
|
||||
}
|
||||
|
||||
if let Some(mut element) = request_layout.popover_element.take() {
|
||||
element.paint(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
// When mouse click down in the trigger bounds, open the popover.
|
||||
let Some(content_build) = this.content.take() else {
|
||||
return;
|
||||
};
|
||||
let old_content_view = element_state.content_view.clone();
|
||||
let hitbox_id = prepaint.hitbox.id;
|
||||
let mouse_button = this.mouse_button;
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble
|
||||
&& event.button == mouse_button
|
||||
&& hitbox_id.is_hovered(window)
|
||||
{
|
||||
cx.stop_propagation();
|
||||
window.prevent_default();
|
||||
|
||||
let new_content_view = (content_build)(window, cx);
|
||||
let old_content_view1 = old_content_view.clone();
|
||||
|
||||
let previous_focus_handle = window.focused(cx);
|
||||
|
||||
window
|
||||
.subscribe(
|
||||
&new_content_view,
|
||||
cx,
|
||||
move |modal, _: &DismissEvent, window, cx| {
|
||||
if modal.focus_handle(cx).contains_focused(window, cx) {
|
||||
if let Some(previous_focus_handle) =
|
||||
previous_focus_handle.as_ref()
|
||||
{
|
||||
window.focus(previous_focus_handle, cx);
|
||||
}
|
||||
}
|
||||
*old_content_view1.borrow_mut() = None;
|
||||
|
||||
window.refresh();
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
window.focus(&new_content_view.focus_handle(cx), cx);
|
||||
*old_content_view.borrow_mut() = Some(new_content_view);
|
||||
window.refresh();
|
||||
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::{
|
||||
div, relative, AnyElement, App, Bounds, Div, Element, ElementId, GlobalElementId,
|
||||
InspectorElementId, InteractiveElement, Interactivity, IntoElement, LayoutId, ParentElement,
|
||||
Pixels, Position, ScrollHandle, SharedString, Size, Stateful, StatefulInteractiveElement,
|
||||
Style, StyleRefinement, Styled, Window,
|
||||
div, App, Div, Element, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce,
|
||||
ScrollHandle, Stateful, StatefulInteractiveElement, StyleRefinement, Styled, Window,
|
||||
};
|
||||
|
||||
use super::{Scrollbar, ScrollbarAxis, ScrollbarState};
|
||||
use super::{Scrollbar, ScrollbarAxis};
|
||||
use crate::scroll::ScrollbarHandle;
|
||||
use crate::StyledExt;
|
||||
|
||||
/// A scroll view is a container that allows the user to scroll through a large amount of content.
|
||||
pub struct Scrollable<E> {
|
||||
/// A trait for elements that can be made scrollable with scrollbars.
|
||||
pub trait ScrollableElement: InteractiveElement + Styled + ParentElement + Element {
|
||||
/// Adds a scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn scrollbar<H: ScrollbarHandle + Clone>(
|
||||
self,
|
||||
scroll_handle: &H,
|
||||
axis: impl Into<ScrollbarAxis>,
|
||||
) -> Self {
|
||||
self.child(ScrollbarLayer {
|
||||
id: "scrollbar_layer".into(),
|
||||
axis: axis.into(),
|
||||
scroll_handle: Rc::new(scroll_handle.clone()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds a vertical scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn vertical_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
|
||||
self.scrollbar(scroll_handle, ScrollbarAxis::Vertical)
|
||||
}
|
||||
/// Adds a horizontal scrollbar to the element.
|
||||
#[track_caller]
|
||||
fn horizontal_scrollbar<H: ScrollbarHandle + Clone>(self, scroll_handle: &H) -> Self {
|
||||
self.scrollbar(scroll_handle, ScrollbarAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_scroll`], but adds scrollbars.
|
||||
#[track_caller]
|
||||
fn overflow_scrollbar(self) -> Scrollable<Self> {
|
||||
Scrollable::new(self, ScrollbarAxis::Both)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_x_scroll`], but adds Horizontal scrollbar.
|
||||
#[track_caller]
|
||||
fn overflow_x_scrollbar(self) -> Scrollable<Self> {
|
||||
Scrollable::new(self, ScrollbarAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// Almost equivalent to [`StatefulInteractiveElement::overflow_y_scroll`], but adds Vertical scrollbar.
|
||||
#[track_caller]
|
||||
fn overflow_y_scrollbar(self) -> Scrollable<Self> {
|
||||
Scrollable::new(self, ScrollbarAxis::Vertical)
|
||||
}
|
||||
}
|
||||
|
||||
/// A scrollable element wrapper that adds scrollbars to an interactive element.
|
||||
#[derive(IntoElement)]
|
||||
pub struct Scrollable<E: InteractiveElement + Styled + ParentElement + Element> {
|
||||
id: ElementId,
|
||||
element: Option<E>,
|
||||
element: E,
|
||||
axis: ScrollbarAxis,
|
||||
/// This is a fake element to handle Styled, InteractiveElement, not used.
|
||||
_element: Stateful<Div>,
|
||||
}
|
||||
|
||||
impl<E> Scrollable<E>
|
||||
where
|
||||
E: Element,
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
pub(crate) fn new(axis: impl Into<ScrollbarAxis>, element: E) -> Self {
|
||||
let id = ElementId::Name(SharedString::from(
|
||||
format!("scrollable-{:?}", element.id(),),
|
||||
));
|
||||
|
||||
#[track_caller]
|
||||
fn new(element: E, axis: impl Into<ScrollbarAxis>) -> Self {
|
||||
let caller = Location::caller();
|
||||
Self {
|
||||
element: Some(element),
|
||||
_element: div().id("fake"),
|
||||
id,
|
||||
id: ElementId::CodeLocation(*caller),
|
||||
element,
|
||||
axis: axis.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set only a vertical scrollbar.
|
||||
pub fn vertical(mut self) -> Self {
|
||||
self.set_axis(ScrollbarAxis::Vertical);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set only a horizontal scrollbar.
|
||||
/// In current implementation, this is not supported yet.
|
||||
pub fn horizontal(mut self) -> Self {
|
||||
self.set_axis(ScrollbarAxis::Horizontal);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the axis of the scroll view.
|
||||
pub fn set_axis(&mut self, axis: impl Into<ScrollbarAxis>) {
|
||||
self.axis = axis.into();
|
||||
}
|
||||
|
||||
fn with_element_state<R>(
|
||||
&mut self,
|
||||
id: &GlobalElementId,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
f: impl FnOnce(&mut Self, &mut ScrollViewState, &mut Window, &mut App) -> R,
|
||||
) -> R {
|
||||
window.with_optional_element_state::<ScrollViewState, _>(
|
||||
Some(id),
|
||||
|element_state, window| {
|
||||
let mut element_state = element_state.unwrap().unwrap_or_default();
|
||||
let result = f(self, &mut element_state, window, cx);
|
||||
(result, Some(element_state))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScrollViewState {
|
||||
state: ScrollbarState,
|
||||
handle: ScrollHandle,
|
||||
}
|
||||
|
||||
impl Default for ScrollViewState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
handle: ScrollHandle::new(),
|
||||
state: ScrollbarState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ParentElement for Scrollable<E>
|
||||
where
|
||||
E: Element + ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.extend(elements);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Styled for Scrollable<E>
|
||||
where
|
||||
E: Element + Styled,
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.style()
|
||||
} else {
|
||||
self._element.style()
|
||||
}
|
||||
self.element.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> InteractiveElement for Scrollable<E>
|
||||
impl<E> ParentElement for Scrollable<E>
|
||||
where
|
||||
E: Element + InteractiveElement,
|
||||
E: InteractiveElement + Styled + ParentElement + Element,
|
||||
{
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
if let Some(element) = &mut self.element {
|
||||
element.interactivity()
|
||||
} else {
|
||||
self._element.interactivity()
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = gpui::AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
}
|
||||
impl<E> StatefulInteractiveElement for Scrollable<E> where E: Element + StatefulInteractiveElement {}
|
||||
|
||||
impl<E> IntoElement for Scrollable<E>
|
||||
impl InteractiveElement for Scrollable<Div> {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl InteractiveElement for Scrollable<Stateful<Div>> {
|
||||
fn interactivity(&mut self) -> &mut gpui::Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> RenderOnce for Scrollable<E>
|
||||
where
|
||||
E: Element,
|
||||
E: InteractiveElement + Styled + ParentElement + Element + 'static,
|
||||
{
|
||||
type Element = Self;
|
||||
fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let scroll_handle = window
|
||||
.use_keyed_state(self.id.clone(), cx, |_, _| ScrollHandle::default())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Element for Scrollable<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
type PrepaintState = ScrollViewState;
|
||||
type RequestLayoutState = AnyElement;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let style = Style {
|
||||
position: Position::Relative,
|
||||
flex_grow: 1.0,
|
||||
flex_shrink: 1.0,
|
||||
size: Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
// Inherit the size from the element style.
|
||||
let style = StyleRefinement {
|
||||
size: self.element.style().size.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let axis = self.axis;
|
||||
let scroll_id = self.id.clone();
|
||||
let content = self.element.take().map(|c| c.into_any_element());
|
||||
|
||||
self.with_element_state(id.unwrap(), window, cx, |_, element_state, window, cx| {
|
||||
let mut element = div()
|
||||
.relative()
|
||||
div()
|
||||
.id(self.id)
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.refine_style(&style)
|
||||
.relative()
|
||||
.child(
|
||||
div()
|
||||
.id(scroll_id)
|
||||
.track_scroll(&element_state.handle)
|
||||
.overflow_scroll()
|
||||
.relative()
|
||||
.id("scroll-area")
|
||||
.flex()
|
||||
.size_full()
|
||||
.child(div().children(content)),
|
||||
)
|
||||
.track_scroll(&scroll_handle)
|
||||
.map(|this| match self.axis {
|
||||
ScrollbarAxis::Vertical => this.flex_col().overflow_y_scroll(),
|
||||
ScrollbarAxis::Horizontal => this.flex_row().overflow_x_scroll(),
|
||||
ScrollbarAxis::Both => this.overflow_scroll(),
|
||||
})
|
||||
.child(
|
||||
self.element
|
||||
// Refine element size to `flex_1`.
|
||||
.size_auto()
|
||||
.flex_1(),
|
||||
),
|
||||
)
|
||||
.child(render_scrollbar(
|
||||
"scrollbar",
|
||||
&scroll_handle,
|
||||
self.axis,
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollableElement for Div {}
|
||||
impl<E> ScrollableElement for Stateful<E>
|
||||
where
|
||||
E: ParentElement + Styled + Element,
|
||||
Self: InteractiveElement,
|
||||
{
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct ScrollbarLayer<H: ScrollbarHandle + Clone> {
|
||||
id: ElementId,
|
||||
axis: ScrollbarAxis,
|
||||
scroll_handle: Rc<H>,
|
||||
}
|
||||
|
||||
impl<H> RenderOnce for ScrollbarLayer<H>
|
||||
where
|
||||
H: ScrollbarHandle + Clone + 'static,
|
||||
{
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
render_scrollbar(self.id, self.scroll_handle.as_ref(), self.axis, window, cx)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[track_caller]
|
||||
fn render_scrollbar<H: ScrollbarHandle + Clone>(
|
||||
id: impl Into<ElementId>,
|
||||
scroll_handle: &H,
|
||||
axis: ScrollbarAxis,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Div {
|
||||
// Do not render scrollbar when inspector is picking elements,
|
||||
// to allow us to pick the background elements.
|
||||
let is_inspector_picking = window.is_inspector_picking(cx);
|
||||
if is_inspector_picking {
|
||||
return div();
|
||||
}
|
||||
|
||||
div()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.left_0()
|
||||
.right_0()
|
||||
.bottom_0()
|
||||
.child(
|
||||
Scrollbar::both(&element_state.state, &element_state.handle).axis(axis),
|
||||
),
|
||||
)
|
||||
.into_any_element();
|
||||
|
||||
let element_id = element.request_layout(window, cx);
|
||||
let layout_id = window.request_layout(style, vec![element_id], cx);
|
||||
|
||||
(layout_id, element)
|
||||
})
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Self::PrepaintState {
|
||||
element.prepaint(window, cx);
|
||||
// do nothing
|
||||
ScrollViewState::default()
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
_: Option<&GlobalElementId>,
|
||||
_: Option<&InspectorElementId>,
|
||||
_: Bounds<Pixels>,
|
||||
element: &mut Self::RequestLayoutState,
|
||||
_: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
element.paint(window, cx)
|
||||
}
|
||||
.child(Scrollbar::new(scroll_handle).id(id).axis(axis))
|
||||
}
|
||||
|
||||
@@ -1,43 +1,50 @@
|
||||
use std::cell::Cell;
|
||||
use std::ops::Deref;
|
||||
use std::panic::Location;
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{
|
||||
fill, point, px, relative, size, App, Axis, BorderStyle, Bounds, ContentMask, Corner,
|
||||
CursorStyle, Edges, Element, GlobalElementId, Hitbox, HitboxBehavior, Hsla, InspectorElementId,
|
||||
IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, Pixels, Point,
|
||||
Position, ScrollHandle, ScrollWheelEvent, Size, UniformListScrollHandle, Window,
|
||||
CursorStyle, Edges, Element, ElementId, GlobalElementId, Hitbox, HitboxBehavior, Hsla,
|
||||
InspectorElementId, IntoElement, IsZero, LayoutId, ListState, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, PaintQuad, Pixels, Point, Position, ScrollHandle, ScrollWheelEvent, Size, Style,
|
||||
UniformListScrollHandle, Window,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use theme::{ActiveTheme, ScrollbarMode};
|
||||
|
||||
use crate::AxisExt;
|
||||
|
||||
const WIDTH: Pixels = px(2. * 2. + 8.);
|
||||
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
|
||||
const WIDTH: Pixels = px(1. * 2. + 8.);
|
||||
const MIN_THUMB_SIZE: f32 = 48.;
|
||||
|
||||
const THUMB_WIDTH: Pixels = px(6.);
|
||||
const THUMB_RADIUS: Pixels = px(6. / 2.);
|
||||
const THUMB_INSET: Pixels = px(2.);
|
||||
const THUMB_INSET: Pixels = px(1.);
|
||||
|
||||
const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
|
||||
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
|
||||
const THUMB_ACTIVE_INSET: Pixels = px(2.);
|
||||
const THUMB_ACTIVE_INSET: Pixels = px(1.);
|
||||
|
||||
const FADE_OUT_DURATION: f32 = 3.0;
|
||||
const FADE_OUT_DELAY: f32 = 2.0;
|
||||
|
||||
pub trait ScrollHandleOffsetable {
|
||||
/// A trait for scroll handles that can get and set offset.
|
||||
pub trait ScrollbarHandle: 'static {
|
||||
/// Get the current offset of the scroll handle.
|
||||
fn offset(&self) -> Point<Pixels>;
|
||||
/// Set the offset of the scroll handle.
|
||||
fn set_offset(&self, offset: Point<Pixels>);
|
||||
fn is_uniform_list(&self) -> bool {
|
||||
false
|
||||
}
|
||||
/// The full size of the content, including padding.
|
||||
fn content_size(&self) -> Size<Pixels>;
|
||||
/// Called when start dragging the scrollbar thumb.
|
||||
fn start_drag(&self) {}
|
||||
/// Called when end dragging the scrollbar thumb.
|
||||
fn end_drag(&self) {}
|
||||
}
|
||||
|
||||
impl ScrollHandleOffsetable for ScrollHandle {
|
||||
impl ScrollbarHandle for ScrollHandle {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.offset()
|
||||
}
|
||||
@@ -51,7 +58,7 @@ impl ScrollHandleOffsetable for ScrollHandle {
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollHandleOffsetable for UniformListScrollHandle {
|
||||
impl ScrollbarHandle for UniformListScrollHandle {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.0.borrow().base_handle.offset()
|
||||
}
|
||||
@@ -60,21 +67,41 @@ impl ScrollHandleOffsetable for UniformListScrollHandle {
|
||||
self.0.borrow_mut().base_handle.set_offset(offset)
|
||||
}
|
||||
|
||||
fn is_uniform_list(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
let base_handle = &self.0.borrow().base_handle;
|
||||
base_handle.max_offset() + base_handle.bounds().size
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
||||
impl ScrollbarHandle for ListState {
|
||||
fn offset(&self) -> Point<Pixels> {
|
||||
self.scroll_px_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn set_offset(&self, offset: Point<Pixels>) {
|
||||
self.set_offset_from_scrollbar(offset);
|
||||
}
|
||||
|
||||
fn content_size(&self) -> Size<Pixels> {
|
||||
self.viewport_bounds().size + self.max_offset_for_scrollbar()
|
||||
}
|
||||
|
||||
fn start_drag(&self) {
|
||||
self.scrollbar_drag_started();
|
||||
}
|
||||
|
||||
fn end_drag(&self) {
|
||||
self.scrollbar_drag_ended();
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone)]
|
||||
struct ScrollbarState(Rc<Cell<ScrollbarStateInner>>);
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ScrollbarStateInner {
|
||||
struct ScrollbarStateInner {
|
||||
hovered_axis: Option<Axis>,
|
||||
hovered_on_thumb: Option<Axis>,
|
||||
dragged_axis: Option<Axis>,
|
||||
@@ -83,6 +110,7 @@ pub struct ScrollbarStateInner {
|
||||
last_scroll_time: Option<Instant>,
|
||||
// Last update offset
|
||||
last_update: Instant,
|
||||
idle_timer_scheduled: bool,
|
||||
}
|
||||
|
||||
impl Default for ScrollbarState {
|
||||
@@ -95,6 +123,7 @@ impl Default for ScrollbarState {
|
||||
last_scroll_offset: point(px(0.), px(0.)),
|
||||
last_scroll_time: None,
|
||||
last_update: Instant::now(),
|
||||
idle_timer_scheduled: false,
|
||||
})))
|
||||
}
|
||||
}
|
||||
@@ -167,6 +196,12 @@ impl ScrollbarStateInner {
|
||||
state
|
||||
}
|
||||
|
||||
fn with_idle_timer_scheduled(&self, scheduled: bool) -> Self {
|
||||
let mut state = *self;
|
||||
state.idle_timer_scheduled = scheduled;
|
||||
state
|
||||
}
|
||||
|
||||
fn is_scrollbar_visible(&self) -> bool {
|
||||
// On drag
|
||||
if self.dragged_axis.is_some() {
|
||||
@@ -182,10 +217,14 @@ impl ScrollbarStateInner {
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrollbar axis.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScrollbarAxis {
|
||||
/// Vertical scrollbar.
|
||||
Vertical,
|
||||
/// Horizontal scrollbar.
|
||||
Horizontal,
|
||||
/// Show both vertical and horizontal scrollbars.
|
||||
Both,
|
||||
}
|
||||
|
||||
@@ -200,25 +239,30 @@ impl From<Axis> for ScrollbarAxis {
|
||||
|
||||
impl ScrollbarAxis {
|
||||
/// Return true if the scrollbar axis is vertical.
|
||||
#[inline]
|
||||
pub fn is_vertical(&self) -> bool {
|
||||
matches!(self, Self::Vertical)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar axis is horizontal.
|
||||
#[inline]
|
||||
pub fn is_horizontal(&self) -> bool {
|
||||
matches!(self, Self::Horizontal)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar axis is both vertical and horizontal.
|
||||
#[inline]
|
||||
pub fn is_both(&self) -> bool {
|
||||
matches!(self, Self::Both)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar has vertical axis.
|
||||
#[inline]
|
||||
pub fn has_vertical(&self) -> bool {
|
||||
matches!(self, Self::Vertical | Self::Both)
|
||||
}
|
||||
|
||||
/// Return true if the scrollbar has horizontal axis.
|
||||
#[inline]
|
||||
pub fn has_horizontal(&self) -> bool {
|
||||
matches!(self, Self::Horizontal | Self::Both)
|
||||
@@ -238,9 +282,10 @@ impl ScrollbarAxis {
|
||||
|
||||
/// Scrollbar control for scroll-area or a uniform-list.
|
||||
pub struct Scrollbar {
|
||||
pub(crate) id: ElementId,
|
||||
axis: ScrollbarAxis,
|
||||
scroll_handle: Rc<Box<dyn ScrollHandleOffsetable>>,
|
||||
state: ScrollbarState,
|
||||
scrollbar_mode: Option<ScrollbarMode>,
|
||||
scroll_handle: Rc<dyn ScrollbarHandle>,
|
||||
scroll_size: Option<Size<Pixels>>,
|
||||
/// Maximum frames per second for scrolling by drag. Default is 120 FPS.
|
||||
///
|
||||
@@ -250,50 +295,46 @@ pub struct Scrollbar {
|
||||
}
|
||||
|
||||
impl Scrollbar {
|
||||
fn new(
|
||||
axis: impl Into<ScrollbarAxis>,
|
||||
state: &ScrollbarState,
|
||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||
) -> Self {
|
||||
/// Create a new scrollbar.
|
||||
///
|
||||
/// This will have both vertical and horizontal scrollbars.
|
||||
#[track_caller]
|
||||
pub fn new<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||
let caller = Location::caller();
|
||||
Self {
|
||||
state: state.clone(),
|
||||
axis: axis.into(),
|
||||
scroll_handle: Rc::new(Box::new(scroll_handle.clone())),
|
||||
id: ElementId::CodeLocation(*caller),
|
||||
axis: ScrollbarAxis::Both,
|
||||
scrollbar_mode: None,
|
||||
scroll_handle: Rc::new(scroll_handle.clone()),
|
||||
max_fps: 120,
|
||||
scroll_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with vertical and horizontal scrollbar.
|
||||
pub fn both(
|
||||
state: &ScrollbarState,
|
||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||
) -> Self {
|
||||
Self::new(ScrollbarAxis::Both, state, scroll_handle)
|
||||
}
|
||||
|
||||
/// Create with horizontal scrollbar.
|
||||
pub fn horizontal(
|
||||
state: &ScrollbarState,
|
||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||
) -> Self {
|
||||
Self::new(ScrollbarAxis::Horizontal, state, scroll_handle)
|
||||
#[track_caller]
|
||||
pub fn horizontal<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||
Self::new(scroll_handle).axis(ScrollbarAxis::Horizontal)
|
||||
}
|
||||
|
||||
/// Create with vertical scrollbar.
|
||||
pub fn vertical(
|
||||
state: &ScrollbarState,
|
||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||
) -> Self {
|
||||
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
|
||||
#[track_caller]
|
||||
pub fn vertical<H: ScrollbarHandle + Clone>(scroll_handle: &H) -> Self {
|
||||
Self::new(scroll_handle).axis(ScrollbarAxis::Vertical)
|
||||
}
|
||||
|
||||
/// Create vertical scrollbar for uniform list.
|
||||
pub fn uniform_scroll(
|
||||
state: &ScrollbarState,
|
||||
scroll_handle: &(impl ScrollHandleOffsetable + Clone + 'static),
|
||||
) -> Self {
|
||||
Self::new(ScrollbarAxis::Vertical, state, scroll_handle)
|
||||
/// Set a specific element id, default is the [`Location::caller`].
|
||||
///
|
||||
/// NOTE: In most cases, you don't need to set a specific id for scrollbar.
|
||||
pub fn id(mut self, id: impl Into<ElementId>) -> Self {
|
||||
self.id = id.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the scrollbar show mode [`ScrollbarShow`], if not set use the `cx.theme().scrollbar_show`.
|
||||
pub fn scrollbar_mode(mut self, mode: ScrollbarMode) -> Self {
|
||||
self.scrollbar_mode = Some(mode);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a special scroll size of the content area, default is None.
|
||||
@@ -315,11 +356,18 @@ impl Scrollbar {
|
||||
/// If you have very high CPU usage, consider reducing this value to improve performance.
|
||||
///
|
||||
/// Available values: 30..120
|
||||
pub fn max_fps(mut self, max_fps: usize) -> Self {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
|
||||
self.max_fps = max_fps.clamp(30, 120);
|
||||
self
|
||||
}
|
||||
|
||||
// Get the width of the scrollbar.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) const fn width() -> Pixels {
|
||||
WIDTH
|
||||
}
|
||||
|
||||
fn style_for_active(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
(
|
||||
cx.theme().scrollbar_thumb_hover_background,
|
||||
@@ -353,11 +401,28 @@ impl Scrollbar {
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_idle(cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
let (width, inset, radius) = if cx.theme().scrollbar_mode.is_scrolling() {
|
||||
(THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS)
|
||||
} else {
|
||||
(THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS)
|
||||
fn style_for_normal(&self, cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
let scrollbar_mode = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||
let (width, inset, radius) = match scrollbar_mode {
|
||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
||||
};
|
||||
|
||||
(
|
||||
cx.theme().scrollbar_thumb_background,
|
||||
cx.theme().scrollbar_track_background,
|
||||
gpui::transparent_black(),
|
||||
width,
|
||||
inset,
|
||||
radius,
|
||||
)
|
||||
}
|
||||
|
||||
fn style_for_idle(&self, _cx: &App) -> (Hsla, Hsla, Hsla, Pixels, Pixels, Pixels) {
|
||||
let scrollbar_mode = self.scrollbar_mode.unwrap_or(ScrollbarMode::Always);
|
||||
let (width, inset, radius) = match scrollbar_mode {
|
||||
ScrollbarMode::Scrolling => (THUMB_WIDTH, THUMB_INSET, THUMB_RADIUS),
|
||||
_ => (THUMB_ACTIVE_WIDTH, THUMB_ACTIVE_INSET, THUMB_ACTIVE_RADIUS),
|
||||
};
|
||||
|
||||
(
|
||||
@@ -379,11 +444,14 @@ impl IntoElement for Scrollbar {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct PrepaintState {
|
||||
hitbox: Hitbox,
|
||||
scrollbar_state: ScrollbarState,
|
||||
states: Vec<AxisPrepaintState>,
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct AxisPrepaintState {
|
||||
axis: Axis,
|
||||
bar_hitbox: Hitbox,
|
||||
@@ -406,7 +474,7 @@ impl Element for Scrollbar {
|
||||
type RequestLayoutState = ();
|
||||
|
||||
fn id(&self) -> Option<gpui::ElementId> {
|
||||
None
|
||||
Some(self.id.clone())
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
|
||||
@@ -420,11 +488,11 @@ impl Element for Scrollbar {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
let style = gpui::Style {
|
||||
let style = Style {
|
||||
position: Position::Absolute,
|
||||
flex_grow: 1.0,
|
||||
flex_shrink: 1.0,
|
||||
size: gpui::Size {
|
||||
size: Size {
|
||||
width: relative(1.).into(),
|
||||
height: relative(1.).into(),
|
||||
},
|
||||
@@ -447,6 +515,11 @@ impl Element for Scrollbar {
|
||||
window.insert_hitbox(bounds, HitboxBehavior::Normal)
|
||||
});
|
||||
|
||||
let state = window
|
||||
.use_state(cx, |_, _| ScrollbarState::default())
|
||||
.read(cx)
|
||||
.clone();
|
||||
|
||||
let mut states = vec![];
|
||||
let mut has_both = self.axis.is_both();
|
||||
let scroll_size = self
|
||||
@@ -470,9 +543,8 @@ impl Element for Scrollbar {
|
||||
};
|
||||
|
||||
// The horizontal scrollbar is set avoid overlapping with the vertical scrollbar, if the vertical scrollbar is visible.
|
||||
|
||||
let margin_end = if has_both && !is_vertical {
|
||||
THUMB_ACTIVE_WIDTH
|
||||
WIDTH
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
@@ -512,11 +584,12 @@ impl Element for Scrollbar {
|
||||
},
|
||||
};
|
||||
|
||||
let state = self.state.clone();
|
||||
let is_always_to_show = cx.theme().scrollbar_mode.is_always();
|
||||
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
|
||||
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||
let is_always_to_show = scrollbar_show.is_always();
|
||||
let is_hover_to_show = scrollbar_show.is_hover();
|
||||
let is_hovered_on_bar = state.get().hovered_axis == Some(axis);
|
||||
let is_hovered_on_thumb = state.get().hovered_on_thumb == Some(axis);
|
||||
let is_offset_changed = state.get().last_scroll_offset != self.scroll_handle.offset();
|
||||
|
||||
let (thumb_bg, bar_bg, bar_border, thumb_width, inset, radius) =
|
||||
if state.get().dragged_axis == Some(axis) {
|
||||
@@ -527,19 +600,19 @@ impl Element for Scrollbar {
|
||||
} else {
|
||||
Self::style_for_hovered_bar(cx)
|
||||
}
|
||||
} else if is_offset_changed {
|
||||
self.style_for_normal(cx)
|
||||
} else if is_always_to_show {
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
if is_hovered_on_thumb {
|
||||
Self::style_for_hovered_thumb(cx)
|
||||
} else {
|
||||
Self::style_for_hovered_bar(cx)
|
||||
}
|
||||
} else {
|
||||
let mut idle_state = Self::style_for_idle(cx);
|
||||
let mut idle_state = self.style_for_idle(cx);
|
||||
// Delay 2s to fade out the scrollbar thumb (in 1s)
|
||||
if let Some(last_time) = state.get().last_scroll_time {
|
||||
let elapsed = Instant::now().duration_since(last_time).as_secs_f32();
|
||||
if elapsed < FADE_OUT_DURATION {
|
||||
if is_hovered_on_bar {
|
||||
state.set(state.get().with_last_scroll_time(Some(Instant::now())));
|
||||
idle_state = if is_hovered_on_thumb {
|
||||
@@ -547,20 +620,29 @@ impl Element for Scrollbar {
|
||||
} else {
|
||||
Self::style_for_hovered_bar(cx)
|
||||
};
|
||||
} else {
|
||||
if elapsed < FADE_OUT_DELAY {
|
||||
} else if elapsed < FADE_OUT_DELAY {
|
||||
idle_state.0 = cx.theme().scrollbar_thumb_background;
|
||||
} else {
|
||||
// opacity = 1 - (x - 2)^10
|
||||
|
||||
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);
|
||||
};
|
||||
idle_state.0 = cx.theme().scrollbar_thumb_background.opacity(opacity);
|
||||
|
||||
window.request_animation_frame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idle_state
|
||||
};
|
||||
@@ -617,7 +699,11 @@ impl Element for Scrollbar {
|
||||
})
|
||||
}
|
||||
|
||||
PrepaintState { hitbox, states }
|
||||
PrepaintState {
|
||||
hitbox,
|
||||
states,
|
||||
scrollbar_state: state,
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(
|
||||
@@ -630,19 +716,21 @@ impl Element for Scrollbar {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let scrollbar_state = &prepaint.scrollbar_state;
|
||||
let scrollbar_show = self.scrollbar_mode.unwrap_or(cx.theme().scrollbar_mode);
|
||||
let view_id = window.current_view();
|
||||
let hitbox_bounds = prepaint.hitbox.bounds;
|
||||
let is_visible =
|
||||
self.state.get().is_scrollbar_visible() || cx.theme().scrollbar_mode.is_always();
|
||||
let is_hover_to_show = cx.theme().scrollbar_mode.is_hover();
|
||||
let is_visible = scrollbar_state.get().is_scrollbar_visible() || scrollbar_show.is_always();
|
||||
let is_hover_to_show = scrollbar_show.is_hover();
|
||||
|
||||
// Update last_scroll_time when offset is changed.
|
||||
if self.scroll_handle.offset() != self.state.get().last_scroll_offset {
|
||||
self.state.set(
|
||||
self.state
|
||||
if self.scroll_handle.offset() != scrollbar_state.get().last_scroll_offset {
|
||||
scrollbar_state.set(
|
||||
scrollbar_state
|
||||
.get()
|
||||
.with_last_scroll(self.scroll_handle.offset(), Some(Instant::now())),
|
||||
);
|
||||
cx.notify(view_id);
|
||||
}
|
||||
|
||||
window.with_content_mask(
|
||||
@@ -652,7 +740,10 @@ impl Element for Scrollbar {
|
||||
|window| {
|
||||
for state in prepaint.states.iter() {
|
||||
let axis = state.axis;
|
||||
let radius = state.radius;
|
||||
let mut radius = state.radius;
|
||||
if cx.theme().radius.is_zero() {
|
||||
radius = px(0.);
|
||||
}
|
||||
let bounds = state.bounds;
|
||||
let thumb_bounds = state.thumb_bounds;
|
||||
let scroll_area_size = state.scroll_size;
|
||||
@@ -686,7 +777,7 @@ impl Element for Scrollbar {
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let state = scrollbar_state.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |event: &ScrollWheelEvent, phase, _, cx| {
|
||||
@@ -707,7 +798,7 @@ impl Element for Scrollbar {
|
||||
|
||||
if is_hover_to_show || is_visible {
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let state = scrollbar_state.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |event: &MouseDownEvent, phase, _, cx| {
|
||||
@@ -718,6 +809,7 @@ impl Element for Scrollbar {
|
||||
// click on the thumb bar, set the drag position
|
||||
let pos = event.position - thumb_bounds.origin;
|
||||
|
||||
scroll_handle.start_drag();
|
||||
state.set(state.get().with_drag_pos(axis, pos));
|
||||
|
||||
cx.notify(view_id);
|
||||
@@ -755,7 +847,7 @@ impl Element for Scrollbar {
|
||||
|
||||
window.on_mouse_event({
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
let state = self.state.clone();
|
||||
let state = scrollbar_state.clone();
|
||||
let max_fps_duration = Duration::from_millis((1000 / self.max_fps) as u64);
|
||||
|
||||
move |event: &MouseMoveEvent, _, _, cx| {
|
||||
@@ -770,9 +862,7 @@ impl Element for Scrollbar {
|
||||
if state.get().hovered_axis != Some(axis) {
|
||||
notify = true;
|
||||
}
|
||||
} else if state.get().hovered_axis == Some(axis)
|
||||
&& state.get().hovered_axis.is_some()
|
||||
{
|
||||
} else if state.get().hovered_axis == Some(axis) {
|
||||
state.set(state.get().with_hovered(None));
|
||||
notify = true;
|
||||
}
|
||||
@@ -790,6 +880,9 @@ impl Element for Scrollbar {
|
||||
|
||||
// Move thumb position on dragging
|
||||
if state.get().dragged_axis == Some(axis) && event.dragging() {
|
||||
// Stop the event propagation to avoid selecting text or other side effects.
|
||||
cx.stop_propagation();
|
||||
|
||||
// drag_pos is the position of the mouse down event
|
||||
// We need to keep the thumb bar still at the origin down position
|
||||
let drag_pos = state.get().drag_pos;
|
||||
@@ -836,10 +929,12 @@ impl Element for Scrollbar {
|
||||
});
|
||||
|
||||
window.on_mouse_event({
|
||||
let state = self.state.clone();
|
||||
let state = scrollbar_state.clone();
|
||||
let scroll_handle = self.scroll_handle.clone();
|
||||
|
||||
move |_event: &MouseUpEvent, phase, _, cx| {
|
||||
if phase.bubble() {
|
||||
scroll_handle.end_drag();
|
||||
state.set(state.get().with_unset_drag_pos());
|
||||
cx.notify(view_id);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@ impl Skeleton {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secondary(mut self, secondary: bool) -> Self {
|
||||
self.secondary = secondary;
|
||||
pub fn secondary(mut self) -> Self {
|
||||
self.secondary = true;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
|
||||
use gpui::{div, px, App, Axis, Div, Element, Pixels, Refineable, StyleRefinement, Styled};
|
||||
use gpui::{div, px, App, Div, Pixels, Refineable, StyleRefinement, Styled};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use theme::ActiveTheme;
|
||||
|
||||
use crate::scroll::{Scrollable, ScrollbarAxis};
|
||||
|
||||
/// Returns a `Div` as horizontal flex layout.
|
||||
pub fn h_flex() -> Div {
|
||||
div().h_flex()
|
||||
@@ -50,17 +46,6 @@ pub trait StyledExt: Styled + Sized {
|
||||
self.flex().flex_col()
|
||||
}
|
||||
|
||||
/// Wraps the element in a ScrollView.
|
||||
///
|
||||
/// Current this is only have a vertical scrollbar.
|
||||
#[inline]
|
||||
fn scrollable(self, axis: impl Into<ScrollbarAxis>) -> Scrollable<Self>
|
||||
where
|
||||
Self: Element,
|
||||
{
|
||||
Scrollable::new(axis, self)
|
||||
}
|
||||
|
||||
font_weight!(font_thin, THIN);
|
||||
font_weight!(font_extralight, EXTRA_LIGHT);
|
||||
font_weight!(font_light, LIGHT);
|
||||
@@ -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.
|
||||
pub trait Collapsible {
|
||||
fn collapsed(self, collapsed: bool) -> Self;
|
||||
|
||||
Reference in New Issue
Block a user