1use std::ops::Range;
2
3use gpui::{
4 App, BorderStyle, Bounds, Corners, Edges, Element, ElementId, GlobalElementId, Hitbox,
5 HitboxBehavior, Hsla, InspectorElementId, IntoElement, LayoutId, PaintQuad, Pixels, Point,
6 SharedString, StyledText, TextStyleRefinement, Window, transparent_black,
7};
8
9use crate::{TextSelection, TextSelectionHandle, TextSelectionRegistration, TextSelectionRun};
10
11pub struct SelectableText {
20 id: ElementId,
21 handle: Option<TextSelectionHandle>,
22 text: SharedString,
23 styled_text: StyledText,
24 document_order: u64,
25 text_style: Option<TextStyleRefinement>,
26 selection_color: Option<Hsla>,
27}
28
29impl SelectableText {
30 pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
32 Self::build(id.into(), None, text.into())
33 }
34
35 pub fn with_handle(
37 id: impl Into<ElementId>,
38 handle: TextSelectionHandle,
39 text: impl Into<SharedString>,
40 ) -> Self {
41 Self::build(id.into(), Some(handle), text.into())
42 }
43
44 fn build(id: ElementId, handle: Option<TextSelectionHandle>, text: SharedString) -> Self {
45 Self {
46 id,
47 handle,
48 styled_text: StyledText::new(text.clone()),
49 text,
50 document_order: 0,
51 text_style: None,
52 selection_color: None,
53 }
54 }
55
56 pub fn document_order(mut self, order: u64) -> Self {
58 self.document_order = order;
59 self
60 }
61
62 pub fn text_style(mut self, style: TextStyleRefinement) -> Self {
64 self.text_style = Some(style);
65 self
66 }
67
68 pub fn selection_color(mut self, color: Hsla) -> Self {
71 self.selection_color = Some(color);
72 self
73 }
74
75 fn paint_selection(
76 layout: &gpui::TextLayout,
77 range: Range<usize>,
78 color: Hsla,
79 window: &mut Window,
80 ) {
81 let (Some(start), Some(end)) = (
82 layout.position_for_index(range.start),
83 layout.position_for_index(range.end),
84 ) else {
85 return;
86 };
87 for bounds in selection_quad_bounds(start, end, layout.bounds(), layout.line_height()) {
88 window.paint_quad(PaintQuad {
89 bounds,
90 background: color.into(),
91 corner_radii: Corners::default(),
92 border_widths: Edges::default(),
93 border_color: transparent_black(),
94 border_style: BorderStyle::default(),
95 });
96 }
97 }
98}
99
100fn selection_quad_bounds(
101 start: Point<Pixels>,
102 end: Point<Pixels>,
103 bounds: Bounds<Pixels>,
104 line_height: Pixels,
105) -> Vec<Bounds<Pixels>> {
106 if start.y == end.y {
107 return vec![Bounds::from_corners(
108 start,
109 Point::new(end.x, end.y + line_height),
110 )];
111 }
112
113 let mut quads = vec![Bounds::from_corners(
114 start,
115 Point::new(bounds.right(), start.y + line_height),
116 )];
117 if end.y > start.y + line_height {
118 quads.push(Bounds::from_corners(
119 Point::new(bounds.left(), start.y + line_height),
120 Point::new(bounds.right(), end.y),
121 ));
122 }
123 quads.push(Bounds::from_corners(
124 Point::new(bounds.left(), end.y),
125 Point::new(end.x, end.y + line_height),
126 ));
127 quads
128}
129
130impl IntoElement for SelectableText {
131 type Element = Self;
132
133 fn into_element(self) -> Self::Element {
134 self
135 }
136}
137
138impl Element for SelectableText {
139 type RequestLayoutState = TextSelectionHandle;
140 type PrepaintState = Hitbox;
141
142 fn id(&self) -> Option<ElementId> {
143 Some(self.id.clone())
144 }
145
146 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
147 None
148 }
149
150 fn request_layout(
151 &mut self,
152 global_id: Option<&GlobalElementId>,
153 inspector_id: Option<&InspectorElementId>,
154 window: &mut Window,
155 cx: &mut App,
156 ) -> (LayoutId, Self::RequestLayoutState) {
157 let handle = self.handle.clone().unwrap_or_else(|| {
158 window.with_element_state(
159 global_id.expect("SelectableText must have a stable element id"),
160 |retained: Option<TextSelectionHandle>, _| {
161 let handle =
162 retained.unwrap_or_else(|| TextSelectionHandle::new(self.text.clone(), cx));
163 (handle.clone(), handle)
164 },
165 )
166 });
167 let (layout_id, ()) = if let Some(style) = self.text_style.clone() {
168 window.with_text_style(Some(style), |window| {
169 self.styled_text
170 .request_layout(global_id, inspector_id, window, cx)
171 })
172 } else {
173 self.styled_text
174 .request_layout(global_id, inspector_id, window, cx)
175 };
176 (layout_id, handle)
177 }
178
179 fn prepaint(
180 &mut self,
181 global_id: Option<&GlobalElementId>,
182 inspector_id: Option<&InspectorElementId>,
183 bounds: Bounds<Pixels>,
184 handle: &mut Self::RequestLayoutState,
185 window: &mut Window,
186 cx: &mut App,
187 ) -> Self::PrepaintState {
188 self.styled_text
189 .prepaint(global_id, inspector_id, bounds, &mut (), window, cx);
190 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
191 handle.register(
192 TextSelectionRegistration::new(hitbox.clone(), bounds)
193 .with_document_order(self.document_order)
194 .with_text_bounds(vec![bounds]),
195 window,
196 cx,
197 );
198 hitbox
199 }
200
201 fn paint(
202 &mut self,
203 global_id: Option<&GlobalElementId>,
204 inspector_id: Option<&InspectorElementId>,
205 bounds: Bounds<Pixels>,
206 handle: &mut Self::RequestLayoutState,
207 _: &mut Self::PrepaintState,
208 window: &mut Window,
209 cx: &mut App,
210 ) {
211 let layout = self.styled_text.layout().clone();
212 let selected_text_before = TextSelection::selected_text(window, cx);
213 let projection = handle.update_runs(
214 &[
215 TextSelectionRun::new(self.text.clone(), layout.clone(), bounds)
216 .with_document_order(self.document_order),
217 ],
218 cx,
219 );
220 if selected_text_before != TextSelection::selected_text(window, cx) {
221 window.refresh();
222 }
223 let color = self
224 .selection_color
225 .unwrap_or_else(|| crate::Theme::global(cx).tokens.colors.selection);
226 for range in projection.ranges().iter().flatten().cloned() {
227 Self::paint_selection(&layout, range, color, window);
228 }
229 self.styled_text.paint(
230 global_id,
231 inspector_id,
232 bounds,
233 &mut (),
234 &mut (),
235 window,
236 cx,
237 );
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use gpui::{
244 Bounds, Context, IntoElement, Modifiers, MouseButton, ParentElement as _, Render,
245 Styled as _, TestAppContext, Window, div, point, px, size,
246 };
247
248 use super::SelectableText;
249 use crate::{TextSelection, TextSelectionHandle, TextSelectionLayer};
250
251 struct SelectableTextTestView;
252
253 impl Render for SelectableTextTestView {
254 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
255 div().size_full().child(TextSelectionLayer).child(
256 div()
257 .w(px(240.))
258 .h(px(32.))
259 .child(SelectableText::new("local", "alpha beta")),
260 )
261 }
262 }
263
264 #[gpui::test]
265 fn explicit_handle_constructor_preserves_document_contract(cx: &mut TestAppContext) {
266 cx.update(|cx| {
267 let handle = TextSelectionHandle::new("alpha beta", cx);
268 let _ = SelectableText::with_handle("plain", handle, "alpha beta").document_order(42);
269 });
270 }
271
272 #[gpui::test]
273 fn local_handle_participates_in_window_selection(cx: &mut TestAppContext) {
274 let (_, cx) = cx.add_window_view(|_, _| SelectableTextTestView);
275 cx.update(|window, cx| {
276 let _ = window.draw(cx);
277 });
278
279 cx.simulate_mouse_down(
280 gpui::point(px(1.), px(12.)),
281 MouseButton::Left,
282 Modifiers::default(),
283 );
284 cx.simulate_mouse_move(
285 gpui::point(px(220.), px(12.)),
286 Some(MouseButton::Left),
287 Modifiers::default(),
288 );
289 cx.simulate_mouse_up(
290 gpui::point(px(220.), px(12.)),
291 MouseButton::Left,
292 Modifiers::default(),
293 );
294 cx.update(|window, cx| {
295 let _ = window.draw(cx);
296 assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
297 });
298 }
299
300 #[test]
301 fn wrapped_selection_paints_full_width_middle_lines() {
302 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(100.)));
303 let quads = super::selection_quad_bounds(
304 point(px(40.), px(20.)),
305 point(px(30.), px(80.)),
306 bounds,
307 px(20.),
308 );
309
310 assert_eq!(
311 quads,
312 vec![
313 Bounds::from_corners(point(px(40.), px(20.)), point(px(110.), px(40.))),
314 Bounds::from_corners(point(px(10.), px(40.)), point(px(110.), px(80.))),
315 Bounds::from_corners(point(px(10.), px(80.)), point(px(30.), px(100.))),
316 ]
317 );
318 }
319}