Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions crates/component/src/group_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub struct GroupBox {
title: Option<AnyElement>,
content_style: StyleRefinement,
children: SmallVec<[AnyElement; 1]>,
footer: Option<AnyElement>,
}

impl GroupBox {
Expand All @@ -80,6 +81,7 @@ impl GroupBox {
content_style: StyleRefinement::default(),
title: None,
children: SmallVec::new(),
footer: None,
}
}

Expand All @@ -106,6 +108,15 @@ impl GroupBox {
self.content_style = style;
self
}

/// Set supporting content below the group's filled or outlined surface.
///
/// The footer shares the title's leading edge, sits 8 px under the
/// surface, and renders as small muted text like a description.
pub fn footer(mut self, footer: impl IntoElement) -> Self {
self.footer = Some(footer.into_any_element());
self
}
}

impl ParentElement for GroupBox {
Expand Down Expand Up @@ -151,15 +162,29 @@ impl RenderOnce for GroupBox {
)
})
.child(
// The footer sits inside the surface's slot so its 8 px gap is
// independent of the root gap between the title and surface.
v_flex()
.when_some(bg, |this, bg| this.bg(bg))
.when_some(border, |this, border| this.border_color(border).border_1())
.text_color(cx.theme().group_box_foreground)
.when(has_paddings, |this| this.p_4())
.gap_4()
.rounded(cx.theme().radius)
.refine_style(&self.content_style)
.children(self.children),
.gap_2()
.child(
v_flex()
.when_some(bg, |this, bg| this.bg(bg))
.when_some(border, |this, border| this.border_color(border).border_1())
.text_color(cx.theme().group_box_foreground)
.when(has_paddings, |this| this.p_4())
.gap_4()
.rounded(cx.theme().radius)
.refine_style(&self.content_style)
.children(self.children),
)
.when_some(self.footer, |this, footer| {
this.child(
div()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child(footer),
)
}),
)
}
}
Expand Down
26 changes: 24 additions & 2 deletions crates/component/src/setting/group.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use std::rc::Rc;

use gpui::{
App, IntoElement, ParentElement as _, SharedString, StyleRefinement, Styled, Window,
prelude::FluentBuilder as _,
AnyElement, App, IntoElement, ParentElement as _, SharedString, StyleRefinement, Styled,
Window, prelude::FluentBuilder as _,
};

use crate::{
Expand All @@ -15,6 +17,7 @@ use crate::{
#[derive(Clone)]
pub struct SettingGroup {
style: StyleRefinement,
footer: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>,

pub(super) title: Option<SharedString>,
pub(super) description: Option<SharedString>,
Expand All @@ -32,6 +35,7 @@ impl SettingGroup {
pub fn new() -> Self {
Self {
style: StyleRefinement::default(),
footer: None,
title: None,
description: None,
items: Vec::new(),
Expand All @@ -50,6 +54,23 @@ impl SettingGroup {
self
}

/// Render supporting content below, and outside, the group's surface.
///
/// The footer aligns with the group title and renders as small muted text,
/// like a description. It scrolls with the group and follows its search
/// visibility; it does not add an independently searchable item or a
/// sidebar entry, and a group needs at least one item to be shown.
pub fn footer<F, E>(mut self, footer: F) -> Self
where
E: IntoElement,
F: Fn(&mut Window, &mut App) -> E + 'static,
{
self.footer = Some(Rc::new(move |window, cx| {
footer(window, cx).into_any_element()
}));
self
}

/// Add a setting item to the group.
pub fn item(mut self, item: SettingItem) -> Self {
self.items.push(item);
Expand Down Expand Up @@ -109,6 +130,7 @@ impl SettingGroup {
None
}
}))
.when_some(self.footer, |this, footer| this.footer(footer(window, cx)))
.refine_style(&self.style)
}

Expand Down
27 changes: 27 additions & 0 deletions crates/component/src/setting/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,33 @@ fn search_preserves_group_and_item_identity(cx: &mut TestAppContext) {
assert!(cx.debug_bounds("setting-1-1-0").is_some());
}

#[gpui::test]
fn footer_follows_group_search_visibility(cx: &mut TestAppContext) {
let (host, cx) = setup(cx);
cx.update(|_, cx| {
host.update(cx, |host, cx| {
host.pages[1].groups[2] = host.pages[1].groups[2].clone().footer(|_, _| {
div()
.child("Changes apply to this device only.")
.debug_selector(|| "font-footer".into())
});
cx.notify();
});
});
search(&host, "font", cx);
assert!(cx.debug_bounds("setting-1-2-1").is_some());
assert!(cx.debug_bounds("font-footer").is_some());

// Footer copy does not independently make a group match the query.
search(&host, "colors", cx);
assert!(cx.debug_bounds("setting-1-1-0").is_some());
assert!(cx.debug_bounds("font-footer").is_none());

search(&host, "font", cx);
assert!(cx.debug_bounds("font-footer").is_some());
assert_eq!(selection(&host, cx), (1, None));
}

#[gpui::test]
fn resetting_search_results_leaves_hidden_settings_unchanged(cx: &mut TestAppContext) {
use std::{cell::Cell, rc::Rc};
Expand Down
1 change: 1 addition & 0 deletions crates/story/src/stories/group_box_story.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ impl Render for GroupBoxStory {
.id("activity")
.fill()
.title("Contributions & activity")
.footer("Private contributions never reveal repository names.")
.child(
h_flex()
.justify_between()
Expand Down
1 change: 1 addition & 0 deletions crates/story/src/stories/settings_story.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ impl SettingsStory {
]),
SettingGroup::new()
.title("Font")
.footer(|_, _| "Font preferences apply to this story only.")
.item(
SettingItem::new(
"Font Family",
Expand Down
14 changes: 14 additions & 0 deletions website/component/group-box.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ GroupBox::new()
.child(Button::new("save").primary().label("Save Changes"))
```

### Footer outside the surface

Use `footer` for supporting content below the filled background or outline, not
inside the content area. It shares the title's leading edge, sits 8 px under
the surface, and renders as small muted text like a description, so plain text
is enough. `content_style` only changes the body.

```rust
GroupBox::new()
.fill()
.child("Update preferences")
.footer("Changes apply to this device only.")
```

### Custom ID

```rust
Expand Down
18 changes: 18 additions & 0 deletions website/component/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,24 @@ SettingGroup::new()
.items(vec![...])
```

### Footer outside the group surface

Use `footer` to render supporting content below the group's background or
border. It aligns with the group title and renders as small muted text like a
description, so plain text is enough; the callback receives the current window
and application context for richer content. It scrolls and is filtered with
the group; it is not an independently searchable setting or a sidebar entry,
and a group still needs at least one item to be shown.

```rust
SettingGroup::new()
.item(SettingItem::new(
"Update source",
SettingField::render(|_, _, _| "GitHub Releases"),
))
.footer(|_, _| "Changes apply to this device only.")
```

## Setting Item

### Basic Item
Expand Down
13 changes: 13 additions & 0 deletions website/zh-CN/component/group-box.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ GroupBox::new()
.child(Button::new("save").primary().label("Save Changes"))
```

### 表面外的底部说明

用 `footer` 在填充背景或边框下方放置辅助内容,而不是将其放进内容区域。
它与标题左对齐,位于表面下方 8 px 处,并像描述文字一样以小号 muted 文本渲染,直接传入纯文本即可。
`content_style` 只影响主体内容。

```rust
GroupBox::new()
.fill()
.child("Update preferences")
.footer("Changes apply to this device only.")
```

### 自定义 ID

```rust
Expand Down
13 changes: 13 additions & 0 deletions website/zh-CN/component/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,19 @@ SettingGroup::new()
.items(vec![...])
```

### 分组表面外的底部说明

用 `footer` 在分组的背景或边框下方渲染辅助内容。它与分组标题左对齐,并像描述文字一样以小号 muted 文本渲染,直接传入纯文本即可;闭包接收当前窗口和应用上下文,可用于更复杂的内容。它随分组一起滚动和过滤,不会成为独立的可搜索设置项,也不会新增侧栏入口;分组仍需至少一个设置项才会显示。

```rust
SettingGroup::new()
.item(SettingItem::new(
"Update source",
SettingField::render(|_, _, _| "GitHub Releases"),
))
.footer(|_, _| "Changes apply to this device only.")
```

## Setting Item

### 基础设置项
Expand Down
Loading