From aadcf4e1dfecbdeb0e39d72fb2a6d1bdb27601d4 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 27 Mar 2025 14:22:00 +0800 Subject: [PATCH] accordion: Fix toggle accordion item. (#749) Close #740 ![CleanShot 2025-03-27 at 11 25 54](https://github.com/user-attachments/assets/7aa2e9ed-a3f2-4683-854a-bc1768912f54) --- crates/story/examples/accordion.rs | 35 ++++++++++++ crates/story/src/accordion_story.rs | 13 +++++ crates/ui/src/accordion.rs | 83 ++++++++++++++--------------- 3 files changed, 87 insertions(+), 44 deletions(-) create mode 100644 crates/story/examples/accordion.rs diff --git a/crates/story/examples/accordion.rs b/crates/story/examples/accordion.rs new file mode 100644 index 00000000..5c3fdfd8 --- /dev/null +++ b/crates/story/examples/accordion.rs @@ -0,0 +1,35 @@ +use gpui::*; +use story::{AccordionStory, Assets}; + +pub struct Example { + root: Entity, +} + +impl Example { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let root = AccordionStory::view(window, cx); + + Self { root } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().p_4().size_full().child(self.root.clone()) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Accordion Example", Example::view, cx); + }); +} diff --git a/crates/story/src/accordion_story.rs b/crates/story/src/accordion_story.rs index c2a875b5..e5334c41 100644 --- a/crates/story/src/accordion_story.rs +++ b/crates/story/src/accordion_story.rs @@ -16,6 +16,7 @@ pub struct AccordionStory { size: Size, bordered: bool, disabled: bool, + multiple: bool, focus_handle: FocusHandle, } @@ -40,6 +41,7 @@ impl AccordionStory { open_ixs: Vec::new(), size: Size::default(), disabled: false, + multiple: false, focus_handle: cx.focus_handle(), } } @@ -105,6 +107,16 @@ impl Render for AccordionStory { this.set_size(size, window, cx); })), ) + .child( + Checkbox::new("multiple") + .label("Multiple") + .checked(self.multiple) + .on_click(cx.listener(|this, checked, _, cx| { + this.multiple = *checked; + cx.notify(); + })), + ) + .child( Checkbox::new("disabled") .label("Disabled") @@ -129,6 +141,7 @@ impl Render for AccordionStory { .bordered(self.bordered) .with_size(self.size) .disabled(self.disabled) + .multiple(self.multiple) .item(|this| this.open(self.open_ixs.contains(&0)) .icon(IconName::Info) diff --git a/crates/ui/src/accordion.rs b/crates/ui/src/accordion.rs index b7b198b0..4f477dd9 100644 --- a/crates/ui/src/accordion.rs +++ b/crates/ui/src/accordion.rs @@ -1,4 +1,4 @@ -use std::{cell::Cell, rc::Rc, sync::Arc}; +use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc}; use gpui::{ div, prelude::FluentBuilder as _, rems, AnyElement, App, Div, ElementId, @@ -80,18 +80,8 @@ impl Sizable for Accordion { impl RenderOnce for Accordion { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let mut open_ixs: Vec = Vec::new(); - let multiple = self.multiple; - let state = Rc::new(Cell::new(None)); - - self.children - .iter() - .enumerate() - .for_each(|(ix, accordion)| { - if accordion.open { - open_ixs.push(ix); - } - }); + let open_ixs = Rc::new(RefCell::new(HashSet::new())); + let is_multiple = self.multiple; self.base .id(self.id) @@ -100,36 +90,37 @@ impl RenderOnce for Accordion { .into_iter() .enumerate() .map(|(ix, accordion)| { - let state = Rc::clone(&state); + if accordion.open { + open_ixs.borrow_mut().insert(ix); + } + accordion + .index(ix) .with_size(self.size) .bordered(self.bordered) - .when(self.disabled, |this| this.disabled(true)) - .on_toggle_click(move |_, _, _| { - state.set(Some(ix)); + .disabled(self.disabled) + .on_toggle_click({ + let open_ixs = Rc::clone(&open_ixs); + move |open, _, _| { + let mut open_ixs = open_ixs.borrow_mut(); + if *open { + if !is_multiple { + open_ixs.clear(); + } + open_ixs.insert(ix); + } else { + open_ixs.remove(&ix); + } + } }) }), ) .when_some( self.on_toggle_click.filter(|_| !self.disabled), move |this, on_toggle_click| { + let open_ixs = Rc::clone(&open_ixs); this.on_click(move |_, window, cx| { - let mut open_ixs = open_ixs.clone(); - if let Some(ix) = state.get() { - if multiple { - if let Some(pos) = open_ixs.iter().position(|&i| i == ix) { - open_ixs.remove(pos); - } else { - open_ixs.push(ix); - } - } else { - let was_open = open_ixs.iter().any(|&i| i == ix); - open_ixs.clear(); - if !was_open { - open_ixs.push(ix); - } - } - } + let open_ixs: Vec = open_ixs.borrow().iter().map(|&ix| ix).collect(); on_toggle_click(&open_ixs, window, cx); }) @@ -141,6 +132,7 @@ impl RenderOnce for Accordion { /// An Accordion is a vertically stacked list of items, each of which can be expanded to reveal the content associated with it. #[derive(IntoElement)] pub struct AccordionItem { + index: usize, icon: Option, title: AnyElement, content: AnyElement, @@ -154,6 +146,7 @@ pub struct AccordionItem { impl AccordionItem { pub fn new() -> Self { Self { + index: 0, icon: None, title: SharedString::default().into_any_element(), content: SharedString::default().into_any_element(), @@ -165,6 +158,11 @@ impl AccordionItem { } } + fn index(mut self, index: usize) -> Self { + self.index = index; + self + } + pub fn icon(mut self, icon: impl Into) -> Self { self.icon = Some(icon.into()); self @@ -230,7 +228,7 @@ impl RenderOnce for AccordionItem { .text_size(text_size) .child( h_flex() - .id("accordion-title") + .id(self.index) .justify_between() .map(|this| match self.size { Size::XSmall => this.py_0().px_1p5(), @@ -274,17 +272,14 @@ impl RenderOnce for AccordionItem { .xsmall() .text_color(cx.theme().muted_foreground), ) - }) - .when_some( - self.on_toggle_click.filter(|_| !self.disabled), - |this, on_toggle_click| { - this.on_click({ - move |_, window, cx| { - on_toggle_click(&!self.open, window, cx); - } + .when_some(self.on_toggle_click, |this, on_toggle_click| { + this.on_click({ + move |_, window, cx| { + on_toggle_click(&!self.open, window, cx); + } + }) }) - }, - ), + }), ) .when(self.open, |this| { this.child(