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 let registration = TextSelectionRegistration::new(hitbox.clone(), bounds)
192 .with_document_order(self.document_order)
193 .with_text_bounds(vec![bounds])
194 .with_rendered_element(handle, window, cx);
195 handle.register(registration, window, cx);
196 hitbox
197 }
198
199 fn paint(
200 &mut self,
201 global_id: Option<&GlobalElementId>,
202 inspector_id: Option<&InspectorElementId>,
203 bounds: Bounds<Pixels>,
204 handle: &mut Self::RequestLayoutState,
205 _: &mut Self::PrepaintState,
206 window: &mut Window,
207 cx: &mut App,
208 ) {
209 let layout = self.styled_text.layout().clone();
210 let selected_text_before = TextSelection::selected_text(window, cx);
211 let projection = handle.update_runs(
212 &[
213 TextSelectionRun::new(self.text.clone(), layout.clone(), bounds)
214 .with_document_order(self.document_order),
215 ],
216 cx,
217 );
218 if selected_text_before != TextSelection::selected_text(window, cx) {
219 window.refresh();
220 }
221 let color = self
222 .selection_color
223 .unwrap_or_else(|| crate::Theme::global(cx).tokens.colors.selection);
224 for range in projection.ranges().iter().flatten().cloned() {
225 Self::paint_selection(&layout, range, color, window);
226 }
227 self.styled_text.paint(
228 global_id,
229 inspector_id,
230 bounds,
231 &mut (),
232 &mut (),
233 window,
234 cx,
235 );
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use gpui::{
242 Bounds, Context, IntoElement, Modifiers, MouseButton, ParentElement as _, Render,
243 Styled as _, TestAppContext, Window, div, point, px, size,
244 };
245
246 use super::SelectableText;
247 use crate::{TextSelection, TextSelectionHandle, TextSelectionLayer};
248
249 struct SelectableTextTestView;
250
251 impl Render for SelectableTextTestView {
252 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
253 div().size_full().child(TextSelectionLayer).child(
254 div()
255 .w(px(240.))
256 .h(px(32.))
257 .child(SelectableText::new("local", "alpha beta")),
258 )
259 }
260 }
261
262 #[gpui::test]
263 fn explicit_handle_constructor_preserves_document_contract(cx: &mut TestAppContext) {
264 cx.update(|cx| {
265 let handle = TextSelectionHandle::new("alpha beta", cx);
266 let _ = SelectableText::with_handle("plain", handle, "alpha beta").document_order(42);
267 });
268 }
269
270 #[gpui::test]
271 fn local_handle_participates_in_window_selection(cx: &mut TestAppContext) {
272 let (_, cx) = cx.add_window_view(|_, _| SelectableTextTestView);
273 cx.update(|window, cx| {
274 let _ = window.draw(cx);
275 });
276
277 cx.simulate_mouse_down(
278 gpui::point(px(1.), px(12.)),
279 MouseButton::Left,
280 Modifiers::default(),
281 );
282 cx.simulate_mouse_move(
283 gpui::point(px(220.), px(12.)),
284 Some(MouseButton::Left),
285 Modifiers::default(),
286 );
287 cx.simulate_mouse_up(
288 gpui::point(px(220.), px(12.)),
289 MouseButton::Left,
290 Modifiers::default(),
291 );
292 cx.update(|window, cx| {
293 let _ = window.draw(cx);
294 assert_eq!(TextSelection::selected_text(window, cx), "alpha beta");
295 });
296 }
297
298 #[test]
299 fn wrapped_selection_paints_full_width_middle_lines() {
300 let bounds = Bounds::new(point(px(10.), px(20.)), size(px(100.), px(100.)));
301 let quads = super::selection_quad_bounds(
302 point(px(40.), px(20.)),
303 point(px(30.), px(80.)),
304 bounds,
305 px(20.),
306 );
307
308 assert_eq!(
309 quads,
310 vec![
311 Bounds::from_corners(point(px(40.), px(20.)), point(px(110.), px(40.))),
312 Bounds::from_corners(point(px(10.), px(40.)), point(px(110.), px(80.))),
313 Bounds::from_corners(point(px(10.), px(80.)), point(px(30.), px(100.))),
314 ]
315 );
316 }
317}