scroll the browse panel header with the group cards

This commit is contained in:
2026-10-08 04:27:26 +07:00
parent 64b49443ff
commit 65c617aff1
+145 -134
View File
@@ -4,8 +4,9 @@ use std::rc::Rc;
use gpui::prelude::FluentBuilder; use gpui::prelude::FluentBuilder;
use gpui::{ use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, ParentElement, Render, ScrollStrategy, SharedString, Styled, Subscription, Task, InteractiveElement, IntoElement, ParentElement, Render, ScrollStrategy, SharedString,
WeakEntity, Window, div, px, relative, size, StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, px, relative,
size,
}; };
use gpui_base::{VirtualListScrollHandle, v_virtual_list}; use gpui_base::{VirtualListScrollHandle, v_virtual_list};
use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry}; use nip29::{Discovery, GroupCandidate, GroupKey, GroupsEvent, GroupsRegistry};
@@ -23,6 +24,7 @@ use ui::{Disableable, Icon, IconName, Sizable, StyledExt, WindowExtension, h_fle
const TITLE: &str = "Browse"; const TITLE: &str = "Browse";
const INPUT_PLACEHOLDER: &str = "wss://relay.example.com"; const INPUT_PLACEHOLDER: &str = "wss://relay.example.com";
const HEADER_HEIGHT: f32 = 96.0;
const CARD_HEIGHT: f32 = 164.0; const CARD_HEIGHT: f32 = 164.0;
const NO_DESCRIPTION: &str = "No description"; const NO_DESCRIPTION: &str = "No description";
@@ -43,7 +45,7 @@ pub struct BrowsePanel {
discovery: Discovery, discovery: Discovery,
/// Whether the discovery sync is still running /// Whether the discovery sync is still running
discovering: bool, discovering: bool,
/// The scroll position of the group list /// The scroll position of the panel
scroll_handle: VirtualListScrollHandle, scroll_handle: VirtualListScrollHandle,
/// Whether the relay is still answering /// Whether the relay is still answering
loading: bool, loading: bool,
@@ -222,8 +224,7 @@ impl BrowsePanel {
} }
/// Join a browsed group and open it. /// Join a browsed group and open it.
fn join(&self, key: GroupKey, window: &mut Window, cx: &mut App) { fn join(dock: &WeakEntity<DockArea>, key: GroupKey, window: &mut Window, cx: &mut App) {
let dock = self.dock.clone();
let registry = GroupsRegistry::global(cx); let registry = GroupsRegistry::global(cx);
registry.update(cx, |registry, cx| { registry.update(cx, |registry, cx| {
@@ -235,7 +236,7 @@ impl BrowsePanel {
}; };
ui::dock::add_panel_to( ui::dock::add_panel_to(
&dock, dock,
PanelHandle::new(nip29_ui::init(group, window, cx)), PanelHandle::new(nip29_ui::init(group, window, cx)),
DockPlacement::Center, DockPlacement::Center,
window, window,
@@ -243,67 +244,114 @@ impl BrowsePanel {
); );
} }
fn render_discovered_rows( /// The discovered groups the user has not joined yet.
&mut self, fn discovered_cards(&self, cx: &App) -> Vec<GroupCandidate> {
range: Range<usize>, let joined: Vec<GroupKey> = GroupsRegistry::global(cx)
cx: &mut Context<Self>,
) -> Vec<AnyElement> {
let registry = GroupsRegistry::global(cx);
let joined: Vec<GroupKey> = registry
.read(cx) .read(cx)
.groups() .groups()
.iter() .iter()
.map(|group| group.read(cx).key().clone()) .map(|group| group.read(cx).key().clone())
.collect(); .collect();
let cards: Vec<GroupCandidate> = self self.discovery
.discovery
.discovered() .discovered()
.iter() .iter()
.filter(|candidate| !joined.contains(&candidate.key)) .filter(|candidate| !joined.contains(&candidate.key))
.skip(range.start * 2)
.take((range.end - range.start) * 2)
.cloned() .cloned()
.collect(); .collect()
self.render_card_rows(&cards, cx)
} }
fn render_browse_rows( /// The panel header, item 0 of the list so it scrolls with the cards.
&mut self, fn render_header(&self, cx: &Context<Self>) -> AnyElement {
range: Range<usize>, h_flex()
cx: &mut Context<Self>, .w_full()
) -> Vec<AnyElement> { .px_4()
let start = range.start * 2; .h_24()
let end = (range.end * 2).min(self.candidates.len()); .flex_shrink_0()
let cards = self.candidates.get(start..end).unwrap_or(&[]); .justify_between()
.child(
self.render_card_rows(cards, cx) v_flex()
} .min_w_0()
.child(
fn render_card_rows( div()
&self, .text_sm()
cards: &[GroupCandidate], .font_semibold()
cx: &mut Context<Self>, .line_height(relative(1.2))
) -> Vec<AnyElement> { .child("Public Groups"),
cards )
.chunks(2) .child(
.map(|pair| { div()
let first = pair.first(); .truncate()
let second = pair.get(1); .text_xs()
.text_color(cx.theme().text_muted)
.map(|this| match &self.relay {
Some(relay) => {
this.child(SharedString::from(relay.as_str().to_owned()))
}
None => this.child(SharedString::from(
"Hosted on current connected relays",
)),
}),
),
)
.child(
h_flex() h_flex()
.gap_1()
.w_full() .w_full()
.h(px(CARD_HEIGHT)) .max_w_64()
.p_2() .px_1()
.gap_2() .rounded(cx.theme().radius)
.children(first.map(|card| self.render_card(card, cx))) .bg(cx.theme().surface_background)
.child(match second { .child(
Some(card) => self.render_card(card, cx), Input::new(&self.input)
None => div().flex_1().into_any_element(), .appearance(false)
}) .small()
.into_any_element() .text_sm()
.flex_1(),
)
.child(
Button::new("relay-browse")
.icon(IconName::ArrowRight)
.tooltip("Browse by Relay")
.ghost()
.small()
.on_click(cx.listener(|this, _event, window, cx| {
this.browse(window, cx);
})),
),
)
.into_any_element()
}
fn render_list_rows(&mut self, range: Range<usize>, cx: &mut Context<Self>) -> Vec<AnyElement> {
let discover = self.relay.is_none();
let cards: Vec<GroupCandidate> = if discover {
self.discovered_cards(cx)
} else {
self.candidates.clone()
};
range
.map(|ix| {
if ix == 0 {
self.render_header(cx)
} else {
let pair = cards.chunks(2).nth(ix - 1).unwrap_or(&[]);
h_flex()
.w_full()
.h(px(CARD_HEIGHT))
.px_4()
.pb_4()
.gap_4()
.children(pair.first().map(|card| self.render_card(card, cx)))
.child(match pair.get(1) {
Some(card) => self.render_card(card, cx),
None => div().flex_1().into_any_element(),
})
.into_any_element()
}
}) })
.collect() .collect()
} }
@@ -337,6 +385,7 @@ impl BrowsePanel {
.into_any_element() .into_any_element()
} else { } else {
let panel = cx.entity().downgrade(); let panel = cx.entity().downgrade();
let dock = self.dock.clone();
let clicked = key; let clicked = key;
Button::new(format!("join-{seed}")) Button::new(format!("join-{seed}"))
@@ -344,9 +393,9 @@ impl BrowsePanel {
.small() .small()
.ghost() .ghost()
.on_click(move |_event, window, cx| { .on_click(move |_event, window, cx| {
let _ = panel.update(cx, |panel, cx| { Self::join(&dock, clicked.clone(), window, cx);
panel.join(clicked.clone(), window, cx);
}); let _ = panel.update(cx, |_, cx| cx.notify());
}) })
.into_any_element() .into_any_element()
}; };
@@ -435,7 +484,8 @@ impl Panel for BrowsePanel {
fn title(&self, cx: &App) -> AnyElement { fn title(&self, cx: &App) -> AnyElement {
h_flex() h_flex()
.gap_1p5() .gap_1()
.text_xs()
.child( .child(
Icon::new(IconName::Compass) Icon::new(IconName::Compass)
.small() .small()
@@ -456,97 +506,58 @@ impl Focusable for BrowsePanel {
impl Render for BrowsePanel { impl Render for BrowsePanel {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let count = self.candidates.len(); let view = cx.entity();
let discover = self.relay.is_none(); let discover = self.relay.is_none();
let (discovered_count, discovering) = if discover { let cards: Vec<GroupCandidate> = if discover {
let registry = GroupsRegistry::global(cx); self.discovered_cards(cx)
let joined: Vec<GroupKey> = registry
.read(cx)
.groups()
.iter()
.map(|group| group.read(cx).key().clone())
.collect();
let discovered_count = self
.discovery
.discovered()
.iter()
.filter(|candidate| !joined.contains(&candidate.key))
.count();
(discovered_count, self.discovering)
} else { } else {
(0, false) self.candidates.clone()
}; };
let body = if discover { let rows = cards.len().div_ceil(2);
if discovered_count > 0 {
let rows = discovered_count.div_ceil(2);
let item_sizes = Rc::new(vec![size(px(0.), px(CARD_HEIGHT)); rows]);
v_virtual_list( let content: AnyElement = if rows > 0 {
cx.entity(), let mut item_sizes = vec![size(px(0.), px(HEADER_HEIGHT))];
"discovered", item_sizes.resize(rows + 1, size(px(0.), px(CARD_HEIGHT)));
item_sizes,
|this, range, _, cx| this.render_discovered_rows(range, cx),
)
.track_scroll(&self.scroll_handle)
.h_full()
.into_any_element()
} else if discovering {
notice("Discovering groups…", cx)
} else {
notice("No groups found.", cx)
}
} else if count > 0 {
let rows = count.div_ceil(2);
let item_sizes = Rc::new(vec![size(px(0.), px(CARD_HEIGHT)); rows]);
v_virtual_list(cx.entity(), "groups", item_sizes, |this, range, _, cx| { v_virtual_list(
this.render_browse_rows(range, cx) view,
}) "groups",
Rc::new(item_sizes),
|this, range, _window, cx| this.render_list_rows(range, cx),
)
.track_scroll(&self.scroll_handle) .track_scroll(&self.scroll_handle)
.h_full() .h_full()
.into_any_element() .into_any_element()
} else if self.loading {
notice("Loading groups…", cx)
} else { } else {
notice("No groups found.", cx) let text = if discover {
if self.discovering {
"Discovering groups…"
} else {
"No groups found."
}
} else if self.loading {
"Loading groups…"
} else {
"No groups found."
};
v_flex()
.id("browse-content")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.child(self.render_header(cx))
.child(notice(text, cx))
.into_any_element()
}; };
v_flex() div()
.size_full() .size_full()
.child( .relative()
v_flex() .child(content)
.h_32() .child(Scrollbar::vertical(&self.scroll_handle))
.flex_shrink_0()
.items_center()
.justify_center()
.bg(cx.theme().surface_background)
.child(
h_flex()
.w_full()
.max_w_56()
.gap_1()
.child(Input::new(&self.input).cleanable(true).flex_1())
.child(Button::new("browse-go").label("Go").primary().on_click(
cx.listener(|this, _event, window, cx| {
this.browse(window, cx);
}),
)),
),
)
.child(
div()
.flex_1()
.min_h_0()
.relative()
.px_2()
.child(body)
.child(Scrollbar::vertical(&self.scroll_handle)),
)
} }
} }