diff --git a/crates/workspace/src/sidebar/entry.rs b/crates/workspace/src/sidebar/entry.rs index 145996bb..05808e86 100644 --- a/crates/workspace/src/sidebar/entry.rs +++ b/crates/workspace/src/sidebar/entry.rs @@ -16,6 +16,9 @@ use ui::{Icon, IconName, Selectable, Sizable, StyledExt, WindowExtension, h_flex use crate::dialogs::screening; +/// Group name callers can target from a `trailing` element to react to row hover. +pub const ROOM_ENTRY_GROUP: &str = "room-entry"; + #[derive(IntoElement)] pub struct RoomEntry { ix: usize, @@ -112,6 +115,7 @@ impl RenderOnce for RoomEntry { h_flex() .id(self.ix) + .group(ROOM_ENTRY_GROUP) .h_8() .w_full() .pl(px(6. + self.depth as f32 * 14.)) diff --git a/crates/workspace/src/sidebar/mod.rs b/crates/workspace/src/sidebar/mod.rs index d1b98fcd..99313cfa 100644 --- a/crates/workspace/src/sidebar/mod.rs +++ b/crates/workspace/src/sidebar/mod.rs @@ -30,6 +30,7 @@ use crate::Command; mod entry; mod tree; +use entry::ROOM_ENTRY_GROUP; pub(crate) use entry::RoomEntry; use tree::{SidebarRow, TreeRow, TreeRowKind, TreeSection, dummy_communities}; @@ -241,8 +242,10 @@ impl Sidebar { SidebarRow::Room { room, depth, - pinned: _pinned, + pinned, } => { + let pinned = *pinned; + let room_id = room.read(cx).id; let public_key = room.read(cx).display_member(cx).public_key(); let name = room.read(cx).display_name(cx); let avatar = room.read(cx).display_image(cx); @@ -255,6 +258,45 @@ impl Sidebar { }); }); + let sidebar = cx.entity().downgrade(); + let trailing = + Button::new(ElementId::NamedInteger("room-menu".into(), index as u64)) + .icon(IconName::Ellipsis) + .ghost_alt() + .xsmall() + .compact() + .invisible() + .group_hover(ROOM_ENTRY_GROUP, |style| style.visible()) + .dropdown_menu(move |this, _window, _cx| { + let sidebar = sidebar.clone(); + + if pinned { + this.item(PopupMenuItem::new("Unpin").on_click( + move |_event, _window, cx| { + if let Err(error) = + sidebar.update(cx, |sidebar, cx| { + sidebar.unpin_room(room_id, cx); + }) + { + log::error!("Failed to unpin room: {error}"); + } + }, + )) + } else { + this.item(PopupMenuItem::new("Pin").on_click( + move |_event, _window, cx| { + if let Err(error) = + sidebar.update(cx, |sidebar, cx| { + sidebar.pin_room(room_id, cx); + }) + { + log::error!("Failed to pin room: {error}"); + } + }, + )) + } + }); + RoomEntry::new(index) .name(name) .avatar(avatar) @@ -262,6 +304,7 @@ impl Sidebar { .kind(kind) .created_at(created_at) .depth(*depth) + .trailing(trailing) .on_click(handler) .into_any_element() } diff --git a/docs/sidebar-tree-redesign.md b/docs/sidebar-tree-redesign.md index 788eda71..a81ce9eb 100644 --- a/docs/sidebar-tree-redesign.md +++ b/docs/sidebar-tree-redesign.md @@ -1,10 +1,9 @@ # Sidebar tree redesign -Status: steps 1-5 implemented. Search now lives in `panels/search.rs`; the -sidebar renders the nav rail and the flattened tree. Remaining: step 6 (pin UI), -step 7 (community rows are already rendered from dummy data, tracked by the -`TODO(concord)`), optional step 8 (persistence), step 9 (cleanup of the step-6 -dead code). +Status: steps 1-6 implemented. Search lives in `panels/search.rs`; the sidebar +renders the nav rail, the flattened tree, and per-row pin/unpin menus. Remaining: +step 7 (confirm the placeholder community names), optional step 8 (persistence), +step 9 (remove the unused `TreeRow::selected` and run the final cleanup). Scope: `crates/workspace/src/sidebar` (`mod.rs`, `entry.rs`, new `tree.rs`), new panel shells in `crates/workspace/src/panels/`, and the `Command` wiring in @@ -264,10 +263,10 @@ Search is now a panel, not a sidebar mode: - Pin state: `pinned_rooms: Vec` in `Sidebar`, order = pin order. - UI: hover ellipsis (`IconName::Ellipsis`, `ghost_alt`, `xsmall`, `compact`) on each room row, opening a `DropdownMenu` with `Pin` / `Unpin` - (`PopupMenuItem::new(...).on_click(...)`). Verify the trigger click does not - also fire the row's `emit_room` click; if it does, `cx.stop_propagation()` - in the menu trigger's `on_click`. (There is no right-click menu pattern in - the codebase yet; a context menu is a follow-up.) + (`PopupMenuItem::new(...).on_click(...)`). The ellipsis is a `RoomEntry` + trailing element, hidden by default and revealed with `group_hover` against the + row's `ROOM_ENTRY_GROUP` group. (There is no right-click menu pattern in the + codebase yet; a context menu is a follow-up.) - `Pinned` folder is hidden when no pinned room resolves to a live room; otherwise expanded by default, showing pinned rooms in pin order. - A pinned room remains listed under `Messages`. @@ -335,8 +334,15 @@ unused until step 5 consumes them. Run the checks in ยง15 after each step. `uniform_list("sidebar-tree")`. `has_search`, `find_focused`, `set_input_focus` were dropped because they only existed to switch the sidebar between the room list and the search view. -- [ ] **Step 6 โ€” pin UI.** Build the per-row ellipsis dropdown, wire - `pin_room`/`unpin_room`. +- [x] **Step 6 โ€” pin UI.** Per-row ellipsis (`IconName::Ellipsis`, `ghost_alt`, + `xsmall`, `compact`) passed to `RoomEntry::trailing`, revealed on row hover + through the `ROOM_ENTRY_GROUP` group name, opening a `DropdownMenu` with + Pin/Unpin; the handlers call `pin_room`/`unpin_room` through a + `WeakEntity`. Click propagation: `gpui_base::Popover` registers the + trigger's `on_mouse_down` with `cx.stop_propagation()`, and GPUI only fires an + element's `on_click` when that element recorded the matching mouse-down, so the + row's `emit_room` click does not fire when the menu trigger is clicked. No extra + handling was needed. - [ ] **Step 7 โ€” community section.** Render dummy entries and hint; add the `TODO(concord)` marker. The flattening and rendering landed with step 5 (`SidebarRow::Community` -> `TreeRow`, dummy data from `dummy_communities()`),