.
Some checks failed
Rust / build (ubuntu-latest, stable) (push) Failing after 5m17s

This commit is contained in:
2026-02-19 10:10:16 +07:00
parent 3e8efdd0ef
commit fad30a89f1
17 changed files with 226 additions and 149 deletions

View File

@@ -231,13 +231,13 @@ impl AutoUpdater {
fn subscribe_to_updates(cx: &App) -> Task<()> { fn subscribe_to_updates(cx: &App) -> Task<()> {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); let _client = nostr.read(cx).client();
cx.background_spawn(async move { 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 app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
let filter = Filter::new() let _filter = Filter::new()
.kind(Kind::ReleaseArtifactSet) .kind(Kind::ReleaseArtifactSet)
.author(app_pubkey) .author(app_pubkey)
.limit(1); .limit(1);
@@ -253,7 +253,7 @@ impl AutoUpdater {
}); });
cx.background_spawn(async move { 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 app_pubkey = PublicKey::parse(APP_PUBKEY).unwrap();
let filter = Filter::new() let filter = Filter::new()
@@ -274,7 +274,7 @@ impl AutoUpdater {
// Get all file metadata event ids // Get all file metadata event ids
let ids: Vec<EventId> = event.tags.event_ids().copied().collect(); let ids: Vec<EventId> = event.tags.event_ids().copied().collect();
let filter = Filter::new() let _filter = Filter::new()
.kind(Kind::FileMetadata) .kind(Kind::FileMetadata)
.author(app_pubkey) .author(app_pubkey)
.ids(ids.clone()); .ids(ids.clone());

View File

@@ -16,6 +16,7 @@ use crate::{ChatRegistry, NewMessage};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SendReport { pub struct SendReport {
pub receiver: PublicKey, pub receiver: PublicKey,
pub gift_wrap_id: Option<EventId>,
pub error: Option<SharedString>, pub error: Option<SharedString>,
pub output: Option<Output<EventId>>, pub output: Option<Output<EventId>>,
} }
@@ -24,12 +25,18 @@ impl SendReport {
pub fn new(receiver: PublicKey) -> Self { pub fn new(receiver: PublicKey) -> Self {
Self { Self {
receiver, receiver,
gift_wrap_id: None,
error: None, error: None,
output: 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. /// Set the output.
pub fn output(mut self, output: Output<EventId>) -> Self { pub fn output(mut self, output: Output<EventId>) -> Self {
self.output = Some(output); self.output = Some(output);
@@ -53,7 +60,7 @@ impl SendReport {
/// Returns true if the send was successful. /// Returns true if the send was successful.
pub fn success(&self) -> bool { pub fn success(&self) -> bool {
if let Some(output) = self.output.as_ref() { if let Some(output) = self.output.as_ref() {
!output.failed.is_empty() !output.success.is_empty()
} else { } else {
false false
} }
@@ -533,7 +540,11 @@ impl Room {
.await .await
{ {
Ok(output) => { 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) => { Err(e) => {
reports.push( reports.push(

View File

@@ -30,6 +30,7 @@ use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::menu::{ContextMenuExt, DropdownMenu}; use ui::menu::{ContextMenuExt, DropdownMenu};
use ui::notification::Notification; use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{ use ui::{
h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt, h_flex, v_flex, Disableable, Icon, IconName, InteractiveElementExt, Sizable, StyledExt,
WindowExtension, WindowExtension,
@@ -62,11 +63,14 @@ pub struct ChatPanel {
rendered_texts_by_id: BTreeMap<EventId, RenderedText>, rendered_texts_by_id: BTreeMap<EventId, RenderedText>,
/// Mapping message (rumor event) ids to their reports /// 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 state
input: Entity<InputState>, input: Entity<InputState>,
/// Sent message ids
sent_ids: Arc<RwLock<Vec<EventId>>>,
/// Replies to /// Replies to
replies_to: Entity<HashSet<EventId>>, replies_to: Entity<HashSet<EventId>>,
@@ -88,6 +92,7 @@ impl ChatPanel {
// Define attachments and replies_to entities // Define attachments and replies_to entities
let attachments = cx.new(|_| vec![]); let attachments = cx.new(|_| vec![]);
let replies_to = cx.new(|_| HashSet::new()); let replies_to = cx.new(|_| HashSet::new());
let reports_by_id = cx.new(|_| BTreeMap::new());
// Define list of messages // Define list of messages
let messages = BTreeSet::from([Message::system()]); let messages = BTreeSet::from([Message::system()]);
@@ -140,7 +145,8 @@ impl ChatPanel {
replies_to, replies_to,
attachments, attachments,
rendered_texts_by_id: BTreeMap::new(), 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, uploading: false,
subscriptions, subscriptions,
tasks: vec![], tasks: vec![],
@@ -151,7 +157,9 @@ impl ChatPanel {
fn handle_notifications(&mut self, cx: &mut Context<Self>) { fn handle_notifications(&mut self, cx: &mut Context<Self>) {
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
let client = nostr.read(cx).client(); 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 { self.tasks.push(cx.background_spawn(async move {
let mut notifications = client.notifications(); let mut notifications = client.notifications();
@@ -162,18 +170,33 @@ impl ChatPanel {
relay_url, relay_url,
} = notification } = 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) {
for report in reports.iter_mut() { tx.send_async((event_id, relay_url)).await.ok();
if let Some(output) = report.output.as_mut() { }
if output.id() == &event_id { }
output.success.insert(relay_url.clone()); }
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(()) Ok(())
@@ -301,6 +324,7 @@ impl ChatPanel {
/// Send message in the background and wait for the response /// 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>) { 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 // This can't fail, because we already ensured that the ID is set
let id = rumor.id.unwrap(); let id = rumor.id.unwrap();
@@ -316,6 +340,10 @@ impl ChatPanel {
self.tasks.push(cx.spawn_in(window, async move |this, cx| { self.tasks.push(cx.spawn_in(window, async move |this, cx| {
let outputs = task.await; 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 // Update the state
this.update(cx, |this, cx| { this.update(cx, |this, cx| {
this.insert_reports(id, outputs, cx); this.insert_reports(id, outputs, cx);
@@ -342,10 +370,12 @@ impl ChatPanel {
}) })
} }
/// Synchronously insert reports /// Insert reports
fn insert_reports(&mut self, id: EventId, reports: Vec<SendReport>, cx: &mut Context<Self>) { 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| {
cx.notify(); this.insert(id, reports);
cx.notify();
});
} }
/// Insert a message into the chat panel /// Insert a message into the chat panel
@@ -379,32 +409,32 @@ impl ChatPanel {
} }
/// Check if a message is pending /// 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 self.reports_by_id
.read_blocking() .read(cx)
.get(id) .get(id)
.is_some_and(|reports| reports.iter().any(|r| r.pending())) .is_some_and(|reports| reports.iter().any(|r| r.pending()))
} }
/// Check if a message was sent successfully by its ID /// 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 self.reports_by_id
.read_blocking() .read(cx)
.get(id) .get(id)
.is_some_and(|reports| reports.iter().any(|r| r.success())) .is_some_and(|reports| reports.iter().any(|r| r.success()))
} }
/// Check if a message failed to send by its ID /// Check if a message failed to send by its ID
fn sent_failed(&self, id: &EventId) -> bool { fn sent_failed(&self, id: &EventId, cx: &App) -> Option<bool> {
self.reports_by_id self.reports_by_id
.read_blocking() .read(cx)
.get(id) .get(id)
.is_some_and(|reports| reports.iter().all(|r| !r.success())) .map(|reports| reports.iter().all(|r| !r.success()))
} }
/// Get all sent reports for a message by its ID /// Get all sent reports for a message by its ID
fn sent_reports(&self, id: &EventId) -> Option<Vec<SendReport>> { fn sent_reports(&self, id: &EventId, cx: &App) -> Option<Vec<SendReport>> {
self.reports_by_id.read_blocking().get(id).cloned() self.reports_by_id.read(cx).get(id).cloned()
} }
/// Get a message by its ID /// Get a message by its ID
@@ -624,13 +654,13 @@ impl ChatPanel {
let has_replies = !replies.is_empty(); let has_replies = !replies.is_empty();
// Check if message is sent failed // 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 // 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 // Check if message is sent failed
let sent_failed = self.sent_failed(&id); let sent_failed = self.sent_failed(&id, cx);
// Hide avatar setting // Hide avatar setting
let hide_avatar = AppSettings::get_hide_avatar(cx); let hide_avatar = AppSettings::get_hide_avatar(cx);
@@ -689,8 +719,10 @@ impl ChatPanel {
this.children(self.render_message_replies(replies, cx)) this.children(self.render_message_replies(replies, cx))
}) })
.child(rendered_text) .child(rendered_text)
.when(sent_failed, |this| { .when_some(sent_failed, |this, failed| {
this.child(deferred(self.render_message_reports(&id, cx))) this.when(failed, |this| {
this.child(deferred(self.render_message_reports(&id, cx)))
})
}), }),
), ),
) )
@@ -755,11 +787,11 @@ impl ChatPanel {
items 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() div()
.id(SharedString::from(id.to_hex())) .id(SharedString::from(id.to_hex()))
.child(SharedString::from("• Sent")) .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| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
@@ -791,7 +823,7 @@ impl ChatPanel {
.child(SharedString::from( .child(SharedString::from(
"Failed to send message. Click to see details.", "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| { this.on_click(move |_e, window, cx| {
let reports = reports.clone(); let reports = reports.clone();
@@ -1155,15 +1187,19 @@ impl Render for ChatPanel {
.on_action(cx.listener(Self::on_command)) .on_action(cx.listener(Self::on_command))
.size_full() .size_full()
.child( .child(
list( div()
self.list_state.clone(), .flex_1()
cx.processor(|this, ix, window, cx| { .size_full()
// Get and render message by index .child(
this.render_message(ix, window, cx) list(
}), self.list_state.clone(),
) cx.processor(move |this, ix, window, cx| {
.flex_1() this.render_message(ix, window, cx)
.size_full(), }),
)
.size_full(),
)
.child(Scrollbar::vertical(&self.list_state)),
) )
.child( .child(
v_flex() v_flex()
@@ -1206,7 +1242,7 @@ impl Render for ChatPanel {
.dropdown_menu_with_anchor( .dropdown_menu_with_anchor(
gpui::Corner::BottomLeft, gpui::Corner::BottomLeft,
move |this, _window, _cx| { move |this, _window, _cx| {
this//.axis(gpui::Axis::Horizontal) this.horizontal()
.menu("👍", Box::new(Command::Insert("👍"))) .menu("👍", Box::new(Command::Insert("👍")))
.menu("👎", Box::new(Command::Insert("👎"))) .menu("👎", Box::new(Command::Insert("👎")))
.menu("😄", Box::new(Command::Insert("😄"))) .menu("😄", Box::new(Command::Insert("😄")))

View File

@@ -11,7 +11,7 @@ use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, div, uniform_list, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription, IntoElement, ParentElement, Render, RetainAllImageCache, SharedString, Styled, Subscription,
Task, Window, Task, UniformListScrollHandle, Window,
}; };
use nostr_sdk::prelude::*; use nostr_sdk::prelude::*;
use person::PersonRegistry; use person::PersonRegistry;
@@ -23,6 +23,7 @@ use ui::divider::Divider;
use ui::indicator::Indicator; use ui::indicator::Indicator;
use ui::input::{InputEvent, InputState, TextInput}; use ui::input::{InputEvent, InputState, TextInput};
use ui::notification::Notification; use ui::notification::Notification;
use ui::scroll::Scrollbar;
use ui::{ use ui::{
h_flex, v_flex, Disableable, Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, 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 { pub struct Sidebar {
name: SharedString, name: SharedString,
focus_handle: FocusHandle, focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
/// Image cache /// Image cache
image_cache: Entity<RetainAllImageCache>, image_cache: Entity<RetainAllImageCache>,
@@ -143,6 +145,7 @@ impl Sidebar {
Self { Self {
name: "Sidebar".into(), name: "Sidebar".into(),
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
scroll_handle: UniformListScrollHandle::new(),
image_cache: RetainAllImageCache::new(cx), image_cache: RetainAllImageCache::new(cx),
find_input, find_input,
find_debouncer: DebouncedDelay::new(), find_debouncer: DebouncedDelay::new(),
@@ -690,9 +693,11 @@ impl Render for Sidebar {
this.render_list_items(range, cx) this.render_list_items(range, cx)
}), }),
) )
.track_scroll(&self.scroll_handle)
.flex_1() .flex_1()
.h_full(), .h_full(),
) )
.child(Scrollbar::vertical(&self.scroll_handle))
}), }),
) )
.when(!self.selected_pkeys.read(cx).is_empty(), |this| { .when(!self.selected_pkeys.read(cx).is_empty(), |this| {

View File

@@ -4,8 +4,8 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
pub enum ScrollbarMode { pub enum ScrollbarMode {
#[default] #[default]
Scrolling,
Hover, Hover,
Scrolling,
Always, Always,
} }

View File

@@ -32,18 +32,12 @@ impl Display for Placement {
impl Placement { impl Placement {
#[inline] #[inline]
pub fn is_horizontal(&self) -> bool { pub fn is_horizontal(&self) -> bool {
match self { matches!(self, Placement::Left | Placement::Right)
Placement::Left | Placement::Right => true,
_ => false,
}
} }
#[inline] #[inline]
pub fn is_vertical(&self) -> bool { pub fn is_vertical(&self) -> bool {
match self { matches!(self, Placement::Top | Placement::Bottom)
Placement::Top | Placement::Bottom => true,
_ => false,
}
} }
#[inline] #[inline]
@@ -217,7 +211,9 @@ impl Side {
/// A trait to extend the [`Axis`] enum with utility methods. /// A trait to extend the [`Axis`] enum with utility methods.
pub trait AxisExt { pub trait AxisExt {
#[allow(clippy::wrong_self_convention)]
fn is_horizontal(self) -> bool; fn is_horizontal(self) -> bool;
#[allow(clippy::wrong_self_convention)]
fn is_vertical(self) -> bool; fn is_vertical(self) -> bool;
} }
@@ -236,6 +232,7 @@ impl AxisExt for Axis {
/// A trait for converting [`Pixels`] to `f32` and `f64`. /// A trait for converting [`Pixels`] to `f32` and `f64`.
pub trait PixelsExt { pub trait PixelsExt {
fn as_f32(&self) -> f32; fn as_f32(&self) -> f32;
#[allow(clippy::wrong_self_convention)]
fn as_f64(self) -> f64; fn as_f64(self) -> f64;
} }
impl PixelsExt for Pixels { impl PixelsExt for Pixels {

View File

@@ -152,6 +152,7 @@ where
} }
/// Return true if either the list or the search input is focused. /// Return true if either the list or the search input is focused.
#[allow(dead_code)]
pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool { pub(crate) fn is_focused(&self, window: &Window, cx: &App) -> bool {
self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window) self.focus_handle.is_focused(window) || self.query_input.focus_handle(cx).is_focused(window)
} }
@@ -330,6 +331,7 @@ where
} }
} }
#[allow(dead_code)]
pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self { pub(crate) fn reset_on_cancel(mut self, reset: bool) -> Self {
self.reset_on_cancel = reset; self.reset_on_cancel = reset;
self self

View File

@@ -33,8 +33,11 @@ pub struct ListItem {
secondary_selected: bool, secondary_selected: bool,
confirmed: bool, confirmed: bool,
check_icon: Option<Icon>, check_icon: Option<Icon>,
#[allow(clippy::type_complexity)]
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, 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>>, 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>>, suffix: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>>,
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
} }
@@ -157,8 +160,10 @@ impl RenderOnce for ListItem {
let corner_radii = self.style.corner_radii.clone(); let corner_radii = self.style.corner_radii.clone();
let mut selected_style = StyleRefinement::default(); let _selected_style = StyleRefinement {
selected_style.corner_radii = corner_radii; corner_radii,
..Default::default()
};
let is_selectable = !(self.disabled || self.mode.is_separator()); let is_selectable = !(self.disabled || self.mode.is_separator());

View File

@@ -1,5 +1,6 @@
pub(crate) mod cache; pub(crate) mod cache;
mod delegate; mod delegate;
#[allow(clippy::module_inception)]
mod list; mod list;
mod list_item; mod list_item;
mod loading; mod loading;

View File

@@ -18,6 +18,12 @@ impl ListSeparatorItem {
} }
} }
impl Default for ListSeparatorItem {
fn default() -> Self {
Self::new()
}
}
impl ParentElement for ListSeparatorItem { impl ParentElement for ListSeparatorItem {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements); self.children.extend(elements);

View File

@@ -192,7 +192,7 @@ impl AppMenu {
) { ) {
let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix); let is_selected = self.menu_bar.read(cx).selected_index == Some(self.ix);
_ = self.menu_bar.update(cx, |state, cx| { self.menu_bar.update(cx, |state, cx| {
let new_ix = if is_selected { None } else { Some(self.ix) }; let new_ix = if is_selected { None } else { Some(self.ix) };
state.set_selected_index(new_ix, window, cx); state.set_selected_index(new_ix, window, cx);
}); });
@@ -208,7 +208,7 @@ impl AppMenu {
return; return;
} }
_ = self.menu_bar.update(cx, |state, cx| { self.menu_bar.update(cx, |state, cx| {
state.set_selected_index(Some(self.ix), window, cx); state.set_selected_index(Some(self.ix), window, cx);
}); });
} }

View File

@@ -37,6 +37,7 @@ impl<E: ParentElement + Styled> ContextMenuExt for E {}
pub struct ContextMenu<E: ParentElement + Styled + Sized> { pub struct ContextMenu<E: ParentElement + Styled + Sized> {
id: ElementId, id: ElementId,
element: Option<E>, element: Option<E>,
#[allow(clippy::type_complexity)]
menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>, menu: Option<Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>>,
// This is not in use, just for style refinement forwarding. // This is not in use, just for style refinement forwarding.
_ignore_style: StyleRefinement, _ignore_style: StyleRefinement,

View File

@@ -41,6 +41,7 @@ pub struct DropdownMenuPopover<T: Selectable + IntoElement + 'static> {
style: StyleRefinement, style: StyleRefinement,
anchor: Corner, anchor: Corner,
trigger: T, trigger: T,
#[allow(clippy::type_complexity)]
builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>, builder: Rc<dyn Fn(PopupMenu, &mut Window, &mut Context<PopupMenu>) -> PopupMenu>,
} }

View File

@@ -16,7 +16,9 @@ pub(crate) struct MenuItemElement {
style: StyleRefinement, style: StyleRefinement,
disabled: bool, disabled: bool,
selected: bool, selected: bool,
#[allow(clippy::type_complexity)]
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, 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>>, on_hover: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
children: SmallVec<[AnyElement; 2]>, children: SmallVec<[AnyElement; 2]>,
} }
@@ -104,12 +106,12 @@ impl RenderOnce for MenuItemElement {
}) })
.when(!self.disabled, |this| { .when(!self.disabled, |this| {
this.group_hover(self.group_name, |this| { this.group_hover(self.group_name, |this| {
this.bg(cx.theme().element_background) this.bg(cx.theme().secondary_background)
.text_color(cx.theme().element_foreground) .text_color(cx.theme().secondary_foreground)
}) })
.when(self.selected, |this| { .when(self.selected, |this| {
this.bg(cx.theme().element_background) this.bg(cx.theme().secondary_background)
.text_color(cx.theme().element_foreground) .text_color(cx.theme().secondary_foreground)
}) })
.when_some(self.on_click, |this, on_click| { .when_some(self.on_click, |this, on_click| {
this.on_mouse_down(MouseButton::Left, move |_, _, cx| { this.on_mouse_down(MouseButton::Left, move |_, _, cx| {

View File

@@ -2,8 +2,8 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
anchored, div, px, rems, Action, AnyElement, App, AppContext, Bounds, ClickEvent, Context, anchored, div, px, rems, Action, AnyElement, App, AppContext, Axis, Bounds, ClickEvent,
Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half, Context, Corner, DismissEvent, Edges, Entity, EventEmitter, FocusHandle, Focusable, Half,
InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement, InteractiveElement, IntoElement, KeyBinding, MouseDownEvent, OwnedMenuItem, ParentElement,
Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled, Pixels, Point, Render, ScrollHandle, SharedString, StatefulInteractiveElement, Styled,
Subscription, WeakEntity, Window, Subscription, WeakEntity, Window,
@@ -44,6 +44,7 @@ pub enum PopupMenuItem {
is_link: bool, is_link: bool,
action: Option<Box<dyn Action>>, action: Option<Box<dyn Action>>,
// For link item // For link item
#[allow(clippy::type_complexity)]
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>, handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
}, },
/// A menu item with custom element render. /// A menu item with custom element render.
@@ -52,7 +53,9 @@ pub enum PopupMenuItem {
disabled: bool, disabled: bool,
checked: bool, checked: bool,
action: Option<Box<dyn Action>>, action: Option<Box<dyn Action>>,
#[allow(clippy::type_complexity)]
render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>, render: Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>,
#[allow(clippy::type_complexity)]
handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>, handler: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
}, },
/// A submenu item that opens another popup menu. /// A submenu item that opens another popup menu.
@@ -275,8 +278,11 @@ impl PopupMenuItem {
pub struct PopupMenu { pub struct PopupMenu {
pub(crate) focus_handle: FocusHandle, pub(crate) focus_handle: FocusHandle,
pub(crate) menu_items: Vec<PopupMenuItem>, pub(crate) menu_items: Vec<PopupMenuItem>,
/// The focus handle of Entity to handle actions. /// The focus handle of Entity to handle actions.
pub(crate) action_context: Option<FocusHandle>, pub(crate) action_context: Option<FocusHandle>,
axis: Axis,
selected_index: Option<usize>, selected_index: Option<usize>,
min_width: Option<Pixels>, min_width: Option<Pixels>,
max_width: Option<Pixels>, max_width: Option<Pixels>,
@@ -290,7 +296,8 @@ pub struct PopupMenu {
scrollable: bool, scrollable: bool,
external_link_icon: bool, external_link_icon: bool,
scroll_handle: ScrollHandle, scroll_handle: ScrollHandle,
// This will update on render
/// This will update on render
submenu_anchor: (Corner, Pixels), submenu_anchor: (Corner, Pixels),
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
@@ -304,6 +311,7 @@ impl PopupMenu {
parent_menu: None, parent_menu: None,
menu_items: Vec::new(), menu_items: Vec::new(),
selected_index: None, selected_index: None,
axis: Axis::Vertical,
min_width: None, min_width: None,
max_width: None, max_width: None,
max_height: None, max_height: None,
@@ -354,6 +362,12 @@ impl PopupMenu {
self self
} }
/// Set the axis of children to horizontal.
pub fn horizontal(mut self) -> Self {
self.axis = Axis::Horizontal;
self
}
/// Set the menu to be scrollable to show vertical scrollbar. /// Set the menu to be scrollable to show vertical scrollbar.
/// ///
/// NOTE: If this is true, the sub-menus will cannot be support. /// NOTE: If this is true, the sub-menus will cannot be support.
@@ -643,6 +657,7 @@ impl PopupMenu {
} }
/// Use small size, the menu item will have smaller height. /// Use small size, the menu item will have smaller height.
#[allow(dead_code)]
pub(crate) fn small(mut self) -> Self { pub(crate) fn small(mut self) -> Self {
self.size = Size::Small; self.size = Size::Small;
self self
@@ -734,41 +749,38 @@ impl PopupMenu {
} }
fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) { fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context<Self>) {
match self.selected_index { if let Some(index) = self.selected_index {
Some(index) => { let item = self.menu_items.get(index);
let item = self.menu_items.get(index); match item {
match item { Some(PopupMenuItem::Item {
Some(PopupMenuItem::Item { handler, action, ..
handler, action, .. }) => {
}) => { if let Some(handler) = handler {
if let Some(handler) = handler { handler(&ClickEvent::default(), window, cx);
handler(&ClickEvent::default(), window, cx); } else if let Some(action) = action.as_ref() {
} else if let Some(action) = action.as_ref() { self.dispatch_confirm_action(action.as_ref(), window, cx);
self.dispatch_confirm_action(action, window, cx); }
}
self.dismiss(&Cancel, window, cx) self.dismiss(&Cancel, window, cx)
}
Some(PopupMenuItem::ElementItem {
handler, action, ..
}) => {
if let Some(handler) = handler {
handler(&ClickEvent::default(), window, cx);
} else if let Some(action) = action.as_ref() {
self.dispatch_confirm_action(action, window, cx);
}
self.dismiss(&Cancel, window, cx)
}
_ => {}
} }
Some(PopupMenuItem::ElementItem {
handler, action, ..
}) => {
if let Some(handler) = handler {
handler(&ClickEvent::default(), window, cx);
} else if let Some(action) = action.as_ref() {
self.dispatch_confirm_action(action.as_ref(), window, cx);
}
self.dismiss(&Cancel, window, cx)
}
_ => {}
} }
_ => {}
} }
} }
fn dispatch_confirm_action( fn dispatch_confirm_action(
&self, &self,
action: &Box<dyn Action>, action: &dyn Action,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) { ) {
@@ -878,8 +890,7 @@ impl PopupMenu {
cx.notify(); cx.notify();
return true; return true;
} }
false
return false;
} }
fn _unselect_submenu(&mut self, _: &mut Window, cx: &mut Context<Self>) -> bool { fn _unselect_submenu(&mut self, _: &mut Window, cx: &mut Context<Self>) -> bool {
@@ -890,8 +901,7 @@ impl PopupMenu {
}); });
return true; return true;
} }
false
return false;
} }
fn _focus_parent_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn _focus_parent_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -1248,7 +1258,9 @@ impl PopupMenu {
} }
impl FluentBuilder for PopupMenu {} impl FluentBuilder for PopupMenu {}
impl EventEmitter<DismissEvent> for PopupMenu {} impl EventEmitter<DismissEvent> for PopupMenu {}
impl Focusable for PopupMenu { impl Focusable for PopupMenu {
fn focus_handle(&self, _: &App) -> FocusHandle { fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone() self.focus_handle.clone()
@@ -1302,13 +1314,17 @@ impl Render for PopupMenu {
.relative() .relative()
.occlude() .occlude()
.child( .child(
v_flex() div()
.id("items") .id("items")
.p_1() .p_1()
.gap_y_0p5() .gap_y_0p5()
.min_w(rems(8.)) .min_w(rems(8.))
.when_some(self.min_width, |this, min_width| this.min_w(min_width)) .when_some(self.min_width, |this, min_width| this.min_w(min_width))
.max_w(max_width) .max_w(max_width)
.map(|this| match self.axis {
Axis::Horizontal => this.flex().flex_row().items_center(),
Axis::Vertical => this.flex().flex_col(),
})
.when(self.scrollable, |this| { .when(self.scrollable, |this| {
this.max_h(max_height) this.max_h(max_height)
.overflow_y_scroll() .overflow_y_scroll()

View File

@@ -26,7 +26,9 @@ pub struct Popover {
default_open: bool, default_open: bool,
open: Option<bool>, open: Option<bool>,
tracked_focus_handle: Option<FocusHandle>, tracked_focus_handle: Option<FocusHandle>,
#[allow(clippy::type_complexity)]
trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>, trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
#[allow(clippy::type_complexity)]
content: Option< content: Option<
Rc< Rc<
dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
@@ -40,6 +42,7 @@ pub struct Popover {
mouse_button: MouseButton, mouse_button: MouseButton,
appearance: bool, appearance: bool,
overlay_closable: bool, overlay_closable: bool,
#[allow(clippy::type_complexity)]
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>, on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
} }
@@ -204,6 +207,7 @@ pub struct PopoverState {
pub(crate) tracked_focus_handle: Option<FocusHandle>, pub(crate) tracked_focus_handle: Option<FocusHandle>,
trigger_bounds: Bounds<Pixels>, trigger_bounds: Bounds<Pixels>,
open: bool, open: bool,
#[allow(clippy::type_complexity)]
on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>, on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
_dismiss_subscription: Option<Subscription>, _dismiss_subscription: Option<Subscription>,

View File

@@ -16,16 +16,16 @@ use theme::{ActiveTheme, ScrollbarMode};
use crate::AxisExt; use crate::AxisExt;
/// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH) /// The width of the scrollbar (THUMB_ACTIVE_INSET * 2 + THUMB_ACTIVE_WIDTH)
const WIDTH: Pixels = px(4. * 2. + 8.); const WIDTH: Pixels = px(1. * 2. + 8.);
const MIN_THUMB_SIZE: f32 = 48.; const MIN_THUMB_SIZE: f32 = 48.;
const THUMB_WIDTH: Pixels = px(6.); const THUMB_WIDTH: Pixels = px(6.);
const THUMB_RADIUS: Pixels = px(6. / 2.); const THUMB_RADIUS: Pixels = px(6. / 2.);
const THUMB_INSET: Pixels = px(4.); const THUMB_INSET: Pixels = px(1.);
const THUMB_ACTIVE_WIDTH: Pixels = px(8.); const THUMB_ACTIVE_WIDTH: Pixels = px(8.);
const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.); const THUMB_ACTIVE_RADIUS: Pixels = px(8. / 2.);
const THUMB_ACTIVE_INSET: Pixels = px(4.); const THUMB_ACTIVE_INSET: Pixels = px(1.);
const FADE_OUT_DURATION: f32 = 3.0; const FADE_OUT_DURATION: f32 = 3.0;
const FADE_OUT_DELAY: f32 = 2.0; const FADE_OUT_DELAY: f32 = 2.0;
@@ -167,10 +167,8 @@ impl ScrollbarStateInner {
fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self { fn with_hovered_on_thumb(&self, axis: Option<Axis>) -> Self {
let mut state = *self; let mut state = *self;
state.hovered_on_thumb = axis; state.hovered_on_thumb = axis;
if self.is_scrollbar_visible() { if self.is_scrollbar_visible() && axis.is_some() {
if axis.is_some() { state.last_scroll_time = Some(std::time::Instant::now());
state.last_scroll_time = Some(std::time::Instant::now());
}
} }
state state
} }
@@ -358,12 +356,14 @@ impl Scrollbar {
/// If you have very high CPU usage, consider reducing this value to improve performance. /// If you have very high CPU usage, consider reducing this value to improve performance.
/// ///
/// Available values: 30..120 /// Available values: 30..120
#[allow(dead_code)]
pub(crate) fn max_fps(mut self, max_fps: usize) -> Self { pub(crate) fn max_fps(mut self, max_fps: usize) -> Self {
self.max_fps = max_fps.clamp(30, 120); self.max_fps = max_fps.clamp(30, 120);
self self
} }
// Get the width of the scrollbar. // Get the width of the scrollbar.
#[allow(dead_code)]
pub(crate) const fn width() -> Pixels { pub(crate) const fn width() -> Pixels {
WIDTH WIDTH
} }
@@ -488,12 +488,16 @@ impl Element for Scrollbar {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) { ) -> (LayoutId, Self::RequestLayoutState) {
let mut style = Style::default(); let style = Style {
style.position = Position::Absolute; position: Position::Absolute,
style.flex_grow = 1.0; flex_grow: 1.0,
style.flex_shrink = 1.0; flex_shrink: 1.0,
style.size.width = relative(1.).into(); size: Size {
style.size.height = relative(1.).into(); width: relative(1.).into(),
height: relative(1.).into(),
},
..Default::default()
};
(window.request_layout(style, None, cx), ()) (window.request_layout(style, None, cx), ())
} }
@@ -757,20 +761,11 @@ impl Element for Scrollbar {
bounds, bounds,
corner_radii: (0.).into(), corner_radii: (0.).into(),
background: gpui::transparent_black().into(), background: gpui::transparent_black().into(),
border_widths: if is_vertical { border_widths: Edges {
Edges { top: px(0.),
top: px(0.), right: px(0.),
right: px(0.), bottom: px(0.),
bottom: px(0.), left: px(0.),
left: px(0.),
}
} else {
Edges {
top: px(0.),
right: px(0.),
bottom: px(0.),
left: px(0.),
}
}, },
border_color: state.border, border_color: state.border,
border_style: BorderStyle::default(), border_style: BorderStyle::default(),
@@ -786,14 +781,15 @@ impl Element for Scrollbar {
let scroll_handle = self.scroll_handle.clone(); let scroll_handle = self.scroll_handle.clone();
move |event: &ScrollWheelEvent, phase, _, cx| { move |event: &ScrollWheelEvent, phase, _, cx| {
if phase.bubble() && hitbox_bounds.contains(&event.position) { if phase.bubble()
if scroll_handle.offset() != state.get().last_scroll_offset { && hitbox_bounds.contains(&event.position)
state.set(state.get().with_last_scroll( && scroll_handle.offset() != state.get().last_scroll_offset
scroll_handle.offset(), {
Some(Instant::now()), state.set(state.get().with_last_scroll(
)); scroll_handle.offset(),
cx.notify(view_id); Some(Instant::now()),
} ));
cx.notify(view_id);
} }
} }
}); });
@@ -866,13 +862,9 @@ impl Element for Scrollbar {
if state.get().hovered_axis != Some(axis) { if state.get().hovered_axis != Some(axis) {
notify = true; notify = true;
} }
} else { } else if state.get().hovered_axis == Some(axis) {
if state.get().hovered_axis == Some(axis) { state.set(state.get().with_hovered(None));
if state.get().hovered_axis.is_some() { notify = true;
state.set(state.get().with_hovered(None));
notify = true;
}
}
} }
// Update hovered state for scrollbar thumb // Update hovered state for scrollbar thumb
@@ -881,11 +873,9 @@ impl Element for Scrollbar {
state.set(state.get().with_hovered_on_thumb(Some(axis))); state.set(state.get().with_hovered_on_thumb(Some(axis)));
notify = true; notify = true;
} }
} else { } else if state.get().hovered_on_thumb == Some(axis) {
if state.get().hovered_on_thumb == Some(axis) { state.set(state.get().with_hovered_on_thumb(None));
state.set(state.get().with_hovered_on_thumb(None)); notify = true;
notify = true;
}
} }
// Move thumb position on dragging // Move thumb position on dragging