Skip to content
Open
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
147 changes: 145 additions & 2 deletions crates/ui/src/combobox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,30 @@ where
self.state.selection()
}

/// Replace the entire selection set by item values.
///
/// Values are resolved through the current delegate. Values that cannot be resolved are
/// ignored. This updates the committed selection and snapshot without emitting a
/// [`ComboboxEvent`].
pub fn set_selected_values(
&mut self,
values: &[<D::Item as SearchableListItem>::Value],
window: &mut Window,
cx: &mut Context<Self>,
) {
let selected_indices = {
let list = self.state.list.read(cx);
let delegate = &list.delegate().delegate;

values
.iter()
.filter_map(|value| delegate.position(value))
.collect::<Vec<_>>()
};

self.set_selected_indices(selected_indices, window, cx);
}

/// Replace the entire selection set.
pub fn set_selected_indices(
&mut self,
Expand Down Expand Up @@ -1019,17 +1043,45 @@ fn render_popup_shell<D: SearchableListDelegate + 'static>(

#[cfg(test)]
mod tests {
use gpui::{AppContext as _, TestAppContext};
use std::{cell::Cell, rc::Rc};

use gpui::{AppContext as _, Context, Entity, Subscription, TestAppContext};

use crate::{
IndexPath,
combobox::{Combobox, ComboboxState},
combobox::{Combobox, ComboboxEvent, ComboboxState},
searchable_list::{
SearchableListChange, SearchableListDelegate, SearchableListItem, SearchableListState,
SearchableVec,
},
};

struct TestComboboxEventCollector {
event_count: Rc<Cell<usize>>,
_subscription: Subscription,
}

impl TestComboboxEventCollector {
fn new(
state: &Entity<ComboboxState<SearchableVec<&'static str>>>,
cx: &mut Context<Self>,
) -> Self {
let event_count = Rc::new(Cell::new(0));
let event_count_for_subscription = event_count.clone();
let _subscription = cx.subscribe(
state,
move |_, _, _: &ComboboxEvent<SearchableVec<&'static str>>, _| {
event_count_for_subscription.set(event_count_for_subscription.get() + 1);
},
);

Self {
event_count,
_subscription,
}
}
}

#[gpui::test]
fn test_combo_box_builder(cx: &mut TestAppContext) {
cx.update(crate::init);
Expand Down Expand Up @@ -1111,6 +1163,97 @@ mod tests {
});
}

#[gpui::test]
fn test_combo_box_set_selected_values_uses_current_delegate(cx: &mut TestAppContext) {
cx.update(crate::init);
let cx = cx.add_empty_window();
cx.update(|window, cx| {
let items = SearchableVec::new(vec!["React", "Vue", "Angular"]);
let state = cx.new(|cx| ComboboxState::new(items, vec![], window, cx).multiple(true));

state.update(cx, |state, cx| {
state.set_selected_values(&["Vue", "Missing"], window, cx);

assert_eq!(state.selected_values(), vec!["Vue"]);
assert_eq!(
state
.selection()
.iter()
.map(|(index, _)| *index)
.collect::<Vec<_>>(),
vec![IndexPath::new(1)],
);
assert_eq!(
state
.state
.list
.read(cx)
.delegate()
.selection_snapshot
.as_slice(),
state.selection(),
);

state.set_items(SearchableVec::new(vec!["Vue", "Rust", "Go"]), window, cx);
state.set_selected_values(&["Go", "Vue"], window, cx);

assert_eq!(state.selected_values(), vec!["Go", "Vue"]);
assert_eq!(
state
.selection()
.iter()
.map(|(index, _)| *index)
.collect::<Vec<_>>(),
vec![IndexPath::new(2), IndexPath::new(0)],
);
assert_eq!(
state
.state
.list
.read(cx)
.delegate()
.selection_snapshot
.as_slice(),
state.selection(),
);

state.set_selected_values(&[], window, cx);

assert!(state.selection().is_empty());
assert!(
state
.state
.list
.read(cx)
.delegate()
.selection_snapshot
.is_empty()
);
});
});
}

#[gpui::test]
fn test_combo_box_set_selected_values_does_not_emit_events(cx: &mut TestAppContext) {
cx.update(crate::init);
let cx = cx.add_empty_window();
let state = cx.update(|window, cx| {
let items = SearchableVec::new(vec!["React", "Vue", "Angular"]);
cx.new(|cx| ComboboxState::new(items, vec![], window, cx).multiple(true))
});
let collector = cx.update(|_, cx| cx.new(|cx| TestComboboxEventCollector::new(&state, cx)));

cx.update(|window, cx| {
state.update(cx, |state, cx| {
state.set_selected_values(&["React", "Vue"], window, cx);
});
});

cx.update(|_, cx| {
assert_eq!(collector.read(cx).event_count.get(), 0);
});
}

#[gpui::test]
fn test_combo_box_initial_selection_seeds_cursor(cx: &mut TestAppContext) {
cx.update(crate::init);
Expand Down
9 changes: 8 additions & 1 deletion docs/docs/components/combobox.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,15 @@ cx.subscribe_in(&state, window, |view, _, event, window, cx| {

### Mutating Programmatically

Values are resolved through the current delegate. Values that cannot be found are ignored.

```rust
// Replace the entire selection
// Replace the entire selection by value
state.update(cx, |s, cx| {
s.set_selected_values(&["React", "Angular"], window, cx);
});

// Replace the entire selection by index path
state.update(cx, |s, cx| {
s.set_selected_indices(vec![IndexPath::new(0), IndexPath::new(2)], window, cx);
});
Expand Down
9 changes: 8 additions & 1 deletion docs/zh-CN/docs/components/combobox.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,15 @@ cx.subscribe_in(&state, window, |view, _, event, window, cx| {

### 程序化操控

值会通过当前 delegate 解析,无法找到的值会被忽略。

```rust
// 替换整个选中集合
// 按值替换整个选中集合
state.update(cx, |s, cx| {
s.set_selected_values(&["React", "Angular"], window, cx);
});

// 按索引路径替换整个选中集合
state.update(cx, |s, cx| {
s.set_selected_indices(vec![IndexPath::new(0), IndexPath::new(2)], window, cx);
});
Expand Down
Loading