From 65c617aff15cc0a1bc6a988a015f620792376a97 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Thu, 8 Oct 2026 04:27:26 +0700 Subject: [PATCH] scroll the browse panel header with the group cards --- crates/workspace/src/panels/browse.rs | 279 +++++++++++++------------- 1 file changed, 145 insertions(+), 134 deletions(-) diff --git a/crates/workspace/src/panels/browse.rs b/crates/workspace/src/panels/browse.rs index e43030ed..69910689 100644 --- a/crates/workspace/src/panels/browse.rs +++ b/crates/workspace/src/panels/browse.rs @@ -4,8 +4,9 @@ use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, - IntoElement, ParentElement, Render, ScrollStrategy, SharedString, Styled, Subscription, Task, - WeakEntity, Window, div, px, relative, size, + InteractiveElement, IntoElement, ParentElement, Render, ScrollStrategy, SharedString, + StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, Window, div, px, relative, + size, }; use gpui_base::{VirtualListScrollHandle, v_virtual_list}; 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 INPUT_PLACEHOLDER: &str = "wss://relay.example.com"; +const HEADER_HEIGHT: f32 = 96.0; const CARD_HEIGHT: f32 = 164.0; const NO_DESCRIPTION: &str = "No description"; @@ -43,7 +45,7 @@ pub struct BrowsePanel { discovery: Discovery, /// Whether the discovery sync is still running discovering: bool, - /// The scroll position of the group list + /// The scroll position of the panel scroll_handle: VirtualListScrollHandle, /// Whether the relay is still answering loading: bool, @@ -222,8 +224,7 @@ impl BrowsePanel { } /// Join a browsed group and open it. - fn join(&self, key: GroupKey, window: &mut Window, cx: &mut App) { - let dock = self.dock.clone(); + fn join(dock: &WeakEntity, key: GroupKey, window: &mut Window, cx: &mut App) { let registry = GroupsRegistry::global(cx); registry.update(cx, |registry, cx| { @@ -235,7 +236,7 @@ impl BrowsePanel { }; ui::dock::add_panel_to( - &dock, + dock, PanelHandle::new(nip29_ui::init(group, window, cx)), DockPlacement::Center, window, @@ -243,67 +244,114 @@ impl BrowsePanel { ); } - fn render_discovered_rows( - &mut self, - range: Range, - cx: &mut Context, - ) -> Vec { - let registry = GroupsRegistry::global(cx); - - let joined: Vec = registry + /// The discovered groups the user has not joined yet. + fn discovered_cards(&self, cx: &App) -> Vec { + let joined: Vec = GroupsRegistry::global(cx) .read(cx) .groups() .iter() .map(|group| group.read(cx).key().clone()) .collect(); - let cards: Vec = self - .discovery + self.discovery .discovered() .iter() .filter(|candidate| !joined.contains(&candidate.key)) - .skip(range.start * 2) - .take((range.end - range.start) * 2) .cloned() - .collect(); - - self.render_card_rows(&cards, cx) + .collect() } - fn render_browse_rows( - &mut self, - range: Range, - cx: &mut Context, - ) -> Vec { - let start = range.start * 2; - let end = (range.end * 2).min(self.candidates.len()); - let cards = self.candidates.get(start..end).unwrap_or(&[]); - - self.render_card_rows(cards, cx) - } - - fn render_card_rows( - &self, - cards: &[GroupCandidate], - cx: &mut Context, - ) -> Vec { - cards - .chunks(2) - .map(|pair| { - let first = pair.first(); - let second = pair.get(1); - + /// The panel header, item 0 of the list so it scrolls with the cards. + fn render_header(&self, cx: &Context) -> AnyElement { + h_flex() + .w_full() + .px_4() + .h_24() + .flex_shrink_0() + .justify_between() + .child( + v_flex() + .min_w_0() + .child( + div() + .text_sm() + .font_semibold() + .line_height(relative(1.2)) + .child("Public Groups"), + ) + .child( + div() + .truncate() + .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() + .gap_1() .w_full() - .h(px(CARD_HEIGHT)) - .p_2() - .gap_2() - .children(first.map(|card| self.render_card(card, cx))) - .child(match second { - Some(card) => self.render_card(card, cx), - None => div().flex_1().into_any_element(), - }) - .into_any_element() + .max_w_64() + .px_1() + .rounded(cx.theme().radius) + .bg(cx.theme().surface_background) + .child( + Input::new(&self.input) + .appearance(false) + .small() + .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, cx: &mut Context) -> Vec { + let discover = self.relay.is_none(); + + let cards: Vec = 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() } @@ -337,6 +385,7 @@ impl BrowsePanel { .into_any_element() } else { let panel = cx.entity().downgrade(); + let dock = self.dock.clone(); let clicked = key; Button::new(format!("join-{seed}")) @@ -344,9 +393,9 @@ impl BrowsePanel { .small() .ghost() .on_click(move |_event, window, cx| { - let _ = panel.update(cx, |panel, cx| { - panel.join(clicked.clone(), window, cx); - }); + Self::join(&dock, clicked.clone(), window, cx); + + let _ = panel.update(cx, |_, cx| cx.notify()); }) .into_any_element() }; @@ -435,7 +484,8 @@ impl Panel for BrowsePanel { fn title(&self, cx: &App) -> AnyElement { h_flex() - .gap_1p5() + .gap_1() + .text_xs() .child( Icon::new(IconName::Compass) .small() @@ -456,97 +506,58 @@ impl Focusable for BrowsePanel { impl Render for BrowsePanel { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let count = self.candidates.len(); + let view = cx.entity(); let discover = self.relay.is_none(); - let (discovered_count, discovering) = if discover { - let registry = GroupsRegistry::global(cx); - - let joined: Vec = 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) + let cards: Vec = if discover { + self.discovered_cards(cx) } else { - (0, false) + self.candidates.clone() }; - let body = if discover { - if discovered_count > 0 { - let rows = discovered_count.div_ceil(2); - let item_sizes = Rc::new(vec![size(px(0.), px(CARD_HEIGHT)); rows]); + let rows = cards.len().div_ceil(2); - v_virtual_list( - cx.entity(), - "discovered", - 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]); + let content: AnyElement = if rows > 0 { + let mut item_sizes = vec![size(px(0.), px(HEADER_HEIGHT))]; + item_sizes.resize(rows + 1, size(px(0.), px(CARD_HEIGHT))); - v_virtual_list(cx.entity(), "groups", item_sizes, |this, range, _, cx| { - this.render_browse_rows(range, cx) - }) + v_virtual_list( + view, + "groups", + Rc::new(item_sizes), + |this, range, _window, cx| this.render_list_rows(range, cx), + ) .track_scroll(&self.scroll_handle) .h_full() .into_any_element() - } else if self.loading { - notice("Loading groups…", cx) } 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() - .child( - v_flex() - .h_32() - .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)), - ) + .relative() + .child(content) + .child(Scrollbar::vertical(&self.scroll_handle)) } }