1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! The query, filter and result rows shared by searchable pickers.
use std::cell::Cell;
use gpui::{Axis, Context, Entity, Point, ScrollHandle, SharedString, Window, div, prelude::*, px};
use theme::{TextStyle, Theme, Typeset};
use crate::{
icons,
input::{FieldEvent, TextField},
popover, scroll,
};
/// How many results a list shows before it scrolls. A count rather than a
/// height: the rows are what the reader is counting, and a figure in pixels
/// would have to be restated every time their metrics move.
const MAX_ROWS: f32 = 12.0;
pub(crate) struct SearchList {
pub query: Entity<TextField>,
pub filter: popover::Filter,
text: SharedString,
/// The result list's own scroll, so the keyboard can keep the active row
/// in view: past [`MAX_ROWS`] the arrows reach rows the reader cannot see.
scroll: ScrollHandle,
/// The row the list was last scrolled to. A wheel belongs to the reader —
/// only the active row moving is this list's business to follow.
scrolled: Cell<Option<usize>>,
}
impl SearchList {
pub fn new<V: 'static>(
items: Vec<SharedString>,
placeholder: &'static str,
get: fn(&mut V) -> &mut Self,
cx: &mut Context<V>,
) -> Self {
let query = cx.new(|cx| {
TextField::new(cx)
.with_placeholder(placeholder)
.with_frame(false)
});
cx.subscribe(&query, move |view, query, event: &FieldEvent, cx| {
if matches!(event, FieldEvent::Changed) {
let search = get(view);
let text = query.read(cx).content();
if search.text != *text {
search.filter.refilter(text);
search.text = text.clone();
search.rewind();
cx.notify();
}
}
})
.detach();
Self {
query,
filter: popover::Filter::new(items),
text: "".into(),
scroll: ScrollHandle::new(),
scrolled: Cell::new(None),
}
}
pub fn clear<V: 'static>(&mut self, cx: &mut Context<V>) {
self.query.update(cx, |query, cx| query.clear(cx));
self.text = "".into();
self.filter.refilter("");
self.rewind();
}
/// Back to the top, for a list that is about to hold different rows —
/// where the offset it was left at names nothing.
fn rewind(&self) {
self.scroll.set_offset(Point::default());
self.scrolled.set(None);
}
pub fn body<V: 'static>(
&self,
theme: &Theme,
selected: Option<usize>,
get: fn(&mut V) -> &mut Self,
choose: fn(&mut V, usize, &mut Window, &mut Context<V>),
cx: &mut Context<V>,
) -> gpui::Div {
let rows = self
.filter
.filtered()
.iter()
.enumerate()
.map(|(position, &item)| {
popover::menu_row(theme, Some(position) == self.filter.active(), None)
.justify_between()
.id(("search-result", item))
.on_mouse_move(cx.listener(move |view, _, _, cx| {
let filter = &mut get(view).filter;
if filter.active() != Some(position) {
filter.set_active(position);
cx.notify();
}
}))
.on_click(
cx.listener(move |view, _, window, cx| choose(view, item, window, cx)),
)
.child(self.filter.items()[item].clone())
.when(selected == Some(item), |row| {
row.child(
icons::icon(icons::glyph::Check)
.size(px(13.0))
.text_color(theme.text),
)
})
});
// Following the active row, not leading it: the arrows move it without
// knowing what is on screen, and the mouse sets it to a row that is on
// screen by definition.
if self.scrolled.get() != self.filter.active() {
self.scrolled.set(self.filter.active());
if let Some(active) = self.filter.active() {
self.scroll.scroll_to_item(active);
}
}
div()
.flex()
.flex_col()
// The query line stays outside the scroller: it is what the rows
// are an answer to, and a reader who has scrolled away from it has
// lost what they typed.
.child(popover::search_line(
theme,
self.query.clone().into_any_element(),
))
.child(if self.filter.filtered().is_empty() {
div()
.px(px(10.0))
.py(px(8.0))
.text_style(TextStyle::Body)
.text_color(theme.text_muted)
.child("No matches")
.into_any_element()
} else {
scroll::Viewport::new(
"search-results",
div()
.id("search-results-rows")
.max_h(px(MAX_ROWS * popover::menu_row_height()))
.flex()
.flex_col()
.children(rows),
Axis::Vertical,
)
.track_scroll(&self.scroll)
.into_any_element()
})
}
}