This new element will let us react to click events on arbitrary ranges of some rendered text, e.g.: ```rs InteractiveText::new( "element-id", StyledText::new("Hello world, how is it going?").with_runs(vec![ cx.text_style().to_run(6), TextRun { background_color: Some(green()), ..cx.text_style().to_run(5) }, cx.text_style().to_run(18), ]), ) .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| { println!("Clicked range {range_ix}"); }) ``` As part of this, I also added the ability to give text runs a background color. Release Notes: - N/A
Unified · Split
| @@ -9423,6 +9423,7 @@ impl Render for Editor { · 9422 unmodified lines | ||
| 9423 | 9423 | font_weight: FontWeight::NORMAL, |
| 9424 | 9424 | font_style: FontStyle::Normal, |
| 9425 | 9425 | line_height: relative(1.).into(), |
| 9426 | + background_color: None, | |
| 9426 | 9427 | underline: None, |
| 9427 | 9428 | white_space: WhiteSpace::Normal, |
| 9428 | 9429 | }, |
| @@ -9437,6 +9438,7 @@ impl Render for Editor { · 8 unmodified lines | ||
| 9437 | 9438 | font_weight: FontWeight::NORMAL, |
| 9438 | 9439 | font_style: FontStyle::Normal, |
| 9439 | 9440 | line_height: relative(settings.buffer_line_height.value()), |
| 9441 | + background_color: None, | |
| 9440 | 9442 | underline: None, |
| 9441 | 9443 | white_space: WhiteSpace::Normal, |
| 9442 | 9444 | }, |
| @@ -2452,7 +2452,7 @@ impl LineWithInvisibles { · 2451 unmodified lines | ||
| 2452 | 2452 | len: line_chunk.len(), |
| 2453 | 2453 | font: text_style.font(), |
| 2454 | 2454 | color: text_style.color, |
| 2455 | − background_color: None, | |
| 2455 | + background_color: text_style.background_color, | |
| 2456 | 2456 | underline: text_style.underline, |
| 2457 | 2457 | }); |
| 2458 | 2458 | |
| @@ -1,11 +1,11 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − Bounds, Element, ElementId, IntoElement, LayoutId, Pixels, SharedString, Size, TextRun, | |
| 3 | − WhiteSpace, WindowContext, WrappedLine, | |
| 2 | + Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent, | |
| 3 | + Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine, | |
| 4 | 4 | }; |
| 5 | 5 | use anyhow::anyhow; |
| 6 | 6 | use parking_lot::{Mutex, MutexGuard}; |
| 7 | 7 | use smallvec::SmallVec; |
| 8 | −use std::{cell::Cell, rc::Rc, sync::Arc}; | |
| 8 | +use std::{cell::Cell, ops::Range, rc::Rc, sync::Arc}; | |
| 9 | 9 | use util::ResultExt; |
| 10 | 10 | |
| 11 | 11 | impl Element for &'static str { |
| @@ -69,23 +69,28 @@ impl IntoElement for SharedString { · 57 unmodified lines | ||
| 69 | 69 | } |
| 70 | 70 | } |
| 71 | 71 | |
| 72 | +/// Renders text with runs of different styles. | |
| 73 | +/// | |
| 74 | +/// Callers are responsible for setting the correct style for each run. | |
| 75 | +/// For text with a uniform style, you can usually avoid calling this constructor | |
| 76 | +/// and just pass text directly. | |
| 72 | 77 | pub struct StyledText { |
| 73 | 78 | text: SharedString, |
| 74 | 79 | runs: Option<Vec<TextRun>>, |
| 75 | 80 | } |
| 76 | 81 | |
| 77 | 82 | impl StyledText { |
| 78 | − /// Renders text with runs of different styles. | |
| 79 | − /// | |
| 80 | − /// Callers are responsible for setting the correct style for each run. | |
| 81 | − /// For text with a uniform style, you can usually avoid calling this constructor | |
| 82 | − /// and just pass text directly. | |
| 83 | − pub fn new(text: SharedString, runs: Vec<TextRun>) -> Self { | |
| 83 | + pub fn new(text: impl Into<SharedString>) -> Self { | |
| 84 | 84 | StyledText { |
| 85 | − text, | |
| 86 | − runs: Some(runs), | |
| 85 | + text: text.into(), | |
| 86 | + runs: None, | |
| 87 | 87 | } |
| 88 | 88 | } |
| 89 | + | |
| 90 | + pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self { | |
| 91 | + self.runs = Some(runs); | |
| 92 | + self | |
| 93 | + } | |
| 89 | 94 | } |
| 90 | 95 | |
| 91 | 96 | impl Element for StyledText { |
| @@ -226,16 +231,73 @@ impl TextState { · 134 unmodified lines | ||
| 226 | 231 | line_origin.y += line.size(line_height).height; |
| 227 | 232 | } |
| 228 | 233 | } |
| 234 | + | |
| 235 | + fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> { | |
| 236 | + if !bounds.contains_point(&position) { | |
| 237 | + return None; | |
| 238 | + } | |
| 239 | + | |
| 240 | + let element_state = self.lock(); | |
| 241 | + let element_state = element_state | |
| 242 | + .as_ref() | |
| 243 | + .expect("measurement has not been performed"); | |
| 244 | + | |
| 245 | + let line_height = element_state.line_height; | |
| 246 | + let mut line_origin = bounds.origin; | |
| 247 | + for line in &element_state.lines { | |
| 248 | + let line_bottom = line_origin.y + line.size(line_height).height; | |
| 249 | + if position.y > line_bottom { | |
| 250 | + line_origin.y = line_bottom; | |
| 251 | + } else { | |
| 252 | + let position_within_line = position - line_origin; | |
| 253 | + return line.index_for_position(position_within_line, line_height); | |
| 254 | + } | |
| 255 | + } | |
| 256 | + | |
| 257 | + None | |
| 258 | + } | |
| 229 | 259 | } |
| 230 | 260 | |
| 231 | −struct InteractiveText { | |
| 261 | +pub struct InteractiveText { | |
| 232 | 262 | element_id: ElementId, |
| 233 | 263 | text: StyledText, |
| 264 | + click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>, | |
| 234 | 265 | } |
| 235 | 266 | |
| 236 | −struct InteractiveTextState { | |
| 267 | +struct InteractiveTextClickEvent { | |
| 268 | + mouse_down_index: usize, | |
| 269 | + mouse_up_index: usize, | |
| 270 | +} | |
| 271 | + | |
| 272 | +pub struct InteractiveTextState { | |
| 237 | 273 | text_state: TextState, |
| 238 | − clicked_range_ixs: Rc<Cell<SmallVec<[usize; 1]>>>, | |
| 274 | + mouse_down_index: Rc<Cell<Option<usize>>>, | |
| 275 | +} | |
| 276 | + | |
| 277 | +impl InteractiveText { | |
| 278 | + pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self { | |
| 279 | + Self { | |
| 280 | + element_id: id.into(), | |
| 281 | + text, | |
| 282 | + click_listener: None, | |
| 283 | + } | |
| 284 | + } | |
| 285 | + | |
| 286 | + pub fn on_click( | |
| 287 | + mut self, | |
| 288 | + ranges: Vec<Range<usize>>, | |
| 289 | + listener: impl Fn(usize, &mut WindowContext<'_>) + 'static, | |
| 290 | + ) -> Self { | |
| 291 | + self.click_listener = Some(Box::new(move |event, cx| { | |
| 292 | + for (range_ix, range) in ranges.iter().enumerate() { | |
| 293 | + if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index) | |
| 294 | + { | |
| 295 | + listener(range_ix, cx); | |
| 296 | + } | |
| 297 | + } | |
| 298 | + })); | |
| 299 | + self | |
| 300 | + } | |
| 239 | 301 | } |
| 240 | 302 | |
| 241 | 303 | impl Element for InteractiveText { |
| @@ -247,27 +309,62 @@ impl Element for InteractiveText { · 5 unmodified lines | ||
| 247 | 309 | cx: &mut WindowContext, |
| 248 | 310 | ) -> (LayoutId, Self::State) { |
| 249 | 311 | if let Some(InteractiveTextState { |
| 250 | − text_state, | |
| 251 | − clicked_range_ixs, | |
| 312 | + mouse_down_index, .. | |
| 252 | 313 | }) = state |
| 253 | 314 | { |
| 254 | − let (layout_id, text_state) = self.text.layout(Some(text_state), cx); | |
| 315 | + let (layout_id, text_state) = self.text.layout(None, cx); | |
| 255 | 316 | let element_state = InteractiveTextState { |
| 256 | 317 | text_state, |
| 257 | − clicked_range_ixs, | |
| 318 | + mouse_down_index, | |
| 258 | 319 | }; |
| 259 | 320 | (layout_id, element_state) |
| 260 | 321 | } else { |
| 261 | 322 | let (layout_id, text_state) = self.text.layout(None, cx); |
| 262 | 323 | let element_state = InteractiveTextState { |
| 263 | 324 | text_state, |
| 264 | − clicked_range_ixs: Rc::default(), | |
| 325 | + mouse_down_index: Rc::default(), | |
| 265 | 326 | }; |
| 266 | 327 | (layout_id, element_state) |
| 267 | 328 | } |
| 268 | 329 | } |
| 269 | 330 | |
| 270 | 331 | fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { |
| 332 | + if let Some(click_listener) = self.click_listener { | |
| 333 | + let text_state = state.text_state.clone(); | |
| 334 | + let mouse_down = state.mouse_down_index.clone(); | |
| 335 | + if let Some(mouse_down_index) = mouse_down.get() { | |
| 336 | + cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| { | |
| 337 | + if phase == DispatchPhase::Bubble { | |
| 338 | + if let Some(mouse_up_index) = | |
| 339 | + text_state.index_for_position(bounds, event.position) | |
| 340 | + { | |
| 341 | + click_listener( | |
| 342 | + InteractiveTextClickEvent { | |
| 343 | + mouse_down_index, | |
| 344 | + mouse_up_index, | |
| 345 | + }, | |
| 346 | + cx, | |
| 347 | + ) | |
| 348 | + } | |
| 349 | + | |
| 350 | + mouse_down.take(); | |
| 351 | + cx.notify(); | |
| 352 | + } | |
| 353 | + }); | |
| 354 | + } else { | |
| 355 | + cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| { | |
| 356 | + if phase == DispatchPhase::Bubble { | |
| 357 | + if let Some(mouse_down_index) = | |
| 358 | + text_state.index_for_position(bounds, event.position) | |
| 359 | + { | |
| 360 | + mouse_down.set(Some(mouse_down_index)); | |
| 361 | + cx.notify(); | |
| 362 | + } | |
| 363 | + } | |
| 364 | + }); | |
| 365 | + } | |
| 366 | + } | |
| 367 | + | |
| 271 | 368 | self.text.paint(bounds, &mut state.text_state, cx) |
| 272 | 369 | } |
| 273 | 370 | } |
| @@ -145,6 +145,7 @@ pub struct TextStyle { · 144 unmodified lines | ||
| 145 | 145 | pub line_height: DefiniteLength, |
| 146 | 146 | pub font_weight: FontWeight, |
| 147 | 147 | pub font_style: FontStyle, |
| 148 | + pub background_color: Option<Hsla>, | |
| 148 | 149 | pub underline: Option<UnderlineStyle>, |
| 149 | 150 | pub white_space: WhiteSpace, |
| 150 | 151 | } |
| @@ -159,6 +160,7 @@ impl Default for TextStyle { · 8 unmodified lines | ||
| 159 | 160 | line_height: phi(), |
| 160 | 161 | font_weight: FontWeight::default(), |
| 161 | 162 | font_style: FontStyle::default(), |
| 163 | + background_color: None, | |
| 162 | 164 | underline: None, |
| 163 | 165 | white_space: WhiteSpace::Normal, |
| 164 | 166 | } |
| @@ -182,6 +184,10 @@ impl TextStyle { · 17 unmodified lines | ||
| 182 | 184 | self.color.fade_out(factor); |
| 183 | 185 | } |
| 184 | 186 | |
| 187 | + if let Some(background_color) = style.background_color { | |
| 188 | + self.background_color = Some(background_color); | |
| 189 | + } | |
| 190 | + | |
| 185 | 191 | if let Some(underline) = style.underline { |
| 186 | 192 | self.underline = Some(underline); |
| 187 | 193 | } |
| @@ -212,7 +218,7 @@ impl TextStyle { · 24 unmodified lines | ||
| 212 | 218 | style: self.font_style, |
| 213 | 219 | }, |
| 214 | 220 | color: self.color, |
| 215 | − background_color: None, | |
| 221 | + background_color: self.background_color, | |
| 216 | 222 | underline: self.underline.clone(), |
| 217 | 223 | } |
| 218 | 224 | } |
| @@ -223,6 +229,7 @@ pub struct HighlightStyle { · 4 unmodified lines | ||
| 223 | 229 | pub color: Option<Hsla>, |
| 224 | 230 | pub font_weight: Option<FontWeight>, |
| 225 | 231 | pub font_style: Option<FontStyle>, |
| 232 | + pub background_color: Option<Hsla>, | |
| 226 | 233 | pub underline: Option<UnderlineStyle>, |
| 227 | 234 | pub fade_out: Option<f32>, |
| 228 | 235 | } |
| @@ -441,6 +448,7 @@ impl From<&TextStyle> for HighlightStyle { · 212 unmodified lines | ||
| 441 | 448 | color: Some(other.color), |
| 442 | 449 | font_weight: Some(other.font_weight), |
| 443 | 450 | font_style: Some(other.font_style), |
| 451 | + background_color: other.background_color, | |
| 444 | 452 | underline: other.underline.clone(), |
| 445 | 453 | fade_out: None, |
| 446 | 454 | } |
| @@ -467,6 +475,10 @@ impl HighlightStyle { · 20 unmodified lines | ||
| 467 | 475 | self.font_style = other.font_style; |
| 468 | 476 | } |
| 469 | 477 | |
| 478 | + if other.background_color.is_some() { | |
| 479 | + self.background_color = other.background_color; | |
| 480 | + } | |
| 481 | + | |
| 470 | 482 | if other.underline.is_some() { |
| 471 | 483 | self.underline = other.underline; |
| 472 | 484 | } |
| @@ -361,6 +361,13 @@ pub trait Styled: Sized { · 360 unmodified lines | ||
| 361 | 361 | self |
| 362 | 362 | } |
| 363 | 363 | |
| 364 | + fn text_bg(mut self, bg: impl Into<Hsla>) -> Self { | |
| 365 | + self.text_style() | |
| 366 | + .get_or_insert_with(Default::default) | |
| 367 | + .background_color = Some(bg.into()); | |
| 368 | + self | |
| 369 | + } | |
| 370 | + | |
| 364 | 371 | fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self { |
| 365 | 372 | self.text_style() |
| 366 | 373 | .get_or_insert_with(Default::default) |
| @@ -196,7 +196,10 @@ impl TextSystem { · 195 unmodified lines | ||
| 196 | 196 | let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); |
| 197 | 197 | for run in runs { |
| 198 | 198 | if let Some(last_run) = decoration_runs.last_mut() { |
| 199 | − if last_run.color == run.color && last_run.underline == run.underline { | |
| 199 | + if last_run.color == run.color | |
| 200 | + && last_run.underline == run.underline | |
| 201 | + && last_run.background_color == run.background_color | |
| 202 | + { | |
| 200 | 203 | last_run.len += run.len as u32; |
| 201 | 204 | continue; |
| 202 | 205 | } |
| @@ -204,6 +207,7 @@ impl TextSystem { · 1 unmodified line | ||
| 204 | 207 | decoration_runs.push(DecorationRun { |
| 205 | 208 | len: run.len as u32, |
| 206 | 209 | color: run.color, |
| 210 | + background_color: run.background_color, | |
| 207 | 211 | underline: run.underline.clone(), |
| 208 | 212 | }); |
| 209 | 213 | } |
| @@ -254,13 +258,16 @@ impl TextSystem { · 44 unmodified lines | ||
| 254 | 258 | } |
| 255 | 259 | |
| 256 | 260 | if decoration_runs.last().map_or(false, |last_run| { |
| 257 | − last_run.color == run.color && last_run.underline == run.underline | |
| 261 | + last_run.color == run.color | |
| 262 | + && last_run.underline == run.underline | |
| 263 | + && last_run.background_color == run.background_color | |
| 258 | 264 | }) { |
| 259 | 265 | decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; |
| 260 | 266 | } else { |
| 261 | 267 | decoration_runs.push(DecorationRun { |
| 262 | 268 | len: run_len_within_line as u32, |
| 263 | 269 | color: run.color, |
| 270 | + background_color: run.background_color, | |
| 264 | 271 | underline: run.underline.clone(), |
| 265 | 272 | }); |
| 266 | 273 | } |
| @@ -1,6 +1,7 @@ | ||
| 1 | 1 | use crate::{ |
| 2 | − black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, | |
| 3 | − UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, | |
| 2 | + black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, | |
| 3 | + LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, | |
| 4 | + WrappedLineLayout, | |
| 4 | 5 | }; |
| 5 | 6 | use derive_more::{Deref, DerefMut}; |
| 6 | 7 | use smallvec::SmallVec; |
| @@ -10,6 +11,7 @@ use std::sync::Arc; · 3 unmodified lines | ||
| 10 | 11 | pub struct DecorationRun { |
| 11 | 12 | pub len: u32, |
| 12 | 13 | pub color: Hsla, |
| 14 | + pub background_color: Option<Hsla>, | |
| 13 | 15 | pub underline: Option<UnderlineStyle>, |
| 14 | 16 | } |
| 15 | 17 | |
| @@ -97,6 +99,7 @@ fn paint_line( · 81 unmodified lines | ||
| 97 | 99 | let mut run_end = 0; |
| 98 | 100 | let mut color = black(); |
| 99 | 101 | let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; |
| 102 | + let mut current_background: Option<(Point<Pixels>, Hsla)> = None; | |
| 100 | 103 | let text_system = cx.text_system().clone(); |
| 101 | 104 | let mut glyph_origin = origin; |
| 102 | 105 | let mut prev_glyph_position = Point::default(); |
| @@ -110,12 +113,24 @@ fn paint_line( · 7 unmodified lines | ||
| 110 | 113 | |
| 111 | 114 | if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { |
| 112 | 115 | wraps.next(); |
| 116 | + if let Some((background_origin, background_color)) = current_background.take() { | |
| 117 | + cx.paint_quad( | |
| 118 | + Bounds { | |
| 119 | + origin: background_origin, | |
| 120 | + size: size(glyph_origin.x - background_origin.x, line_height), | |
| 121 | + }, | |
| 122 | + Corners::default(), | |
| 123 | + background_color, | |
| 124 | + Edges::default(), | |
| 125 | + transparent_black(), | |
| 126 | + ); | |
| 127 | + } | |
| 113 | 128 | if let Some((underline_origin, underline_style)) = current_underline.take() { |
| 114 | 129 | cx.paint_underline( |
| 115 | 130 | underline_origin, |
| 116 | 131 | glyph_origin.x - underline_origin.x, |
| 117 | 132 | &underline_style, |
| 118 | − )?; | |
| 133 | + ); | |
| 119 | 134 | } |
| 120 | 135 | |
| 121 | 136 | glyph_origin.x = origin.x; |
| @@ -123,9 +138,20 @@ fn paint_line( · 1 unmodified line | ||
| 123 | 138 | } |
| 124 | 139 | prev_glyph_position = glyph.position; |
| 125 | 140 | |
| 141 | + let mut finished_background: Option<(Point<Pixels>, Hsla)> = None; | |
| 126 | 142 | let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; |
| 127 | 143 | if glyph.index >= run_end { |
| 128 | 144 | if let Some(style_run) = decoration_runs.next() { |
| 145 | + if let Some((_, background_color)) = &mut current_background { | |
| 146 | + if style_run.background_color.as_ref() != Some(background_color) { | |
| 147 | + finished_background = current_background.take(); | |
| 148 | + } | |
| 149 | + } | |
| 150 | + if let Some(run_background) = style_run.background_color { | |
| 151 | + current_background | |
| 152 | + .get_or_insert((point(glyph_origin.x, origin.y), run_background)); | |
| 153 | + } | |
| 154 | + | |
| 129 | 155 | if let Some((_, underline_style)) = &mut current_underline { |
| 130 | 156 | if style_run.underline.as_ref() != Some(underline_style) { |
| 131 | 157 | finished_underline = current_underline.take(); |
| @@ -149,16 +175,30 @@ fn paint_line( · 17 unmodified lines | ||
| 149 | 175 | color = style_run.color; |
| 150 | 176 | } else { |
| 151 | 177 | run_end = layout.len; |
| 178 | + finished_background = current_background.take(); | |
| 152 | 179 | finished_underline = current_underline.take(); |
| 153 | 180 | } |
| 154 | 181 | } |
| 155 | 182 | |
| 183 | + if let Some((background_origin, background_color)) = finished_background { | |
| 184 | + cx.paint_quad( | |
| 185 | + Bounds { | |
| 186 | + origin: background_origin, | |
| 187 | + size: size(glyph_origin.x - background_origin.x, line_height), | |
| 188 | + }, | |
| 189 | + Corners::default(), | |
| 190 | + background_color, | |
| 191 | + Edges::default(), | |
| 192 | + transparent_black(), | |
| 193 | + ); | |
| 194 | + } | |
| 195 | + | |
| 156 | 196 | if let Some((underline_origin, underline_style)) = finished_underline { |
| 157 | 197 | cx.paint_underline( |
| 158 | 198 | underline_origin, |
| 159 | 199 | glyph_origin.x - underline_origin.x, |
| 160 | 200 | &underline_style, |
| 161 | − )?; | |
| 201 | + ); | |
| 162 | 202 | } |
| 163 | 203 | |
| 164 | 204 | let max_glyph_bounds = Bounds { |
| @@ -188,13 +228,27 @@ fn paint_line( · 23 unmodified lines | ||
| 188 | 228 | } |
| 189 | 229 | } |
| 190 | 230 | |
| 231 | + if let Some((background_origin, background_color)) = current_background.take() { | |
| 232 | + let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); | |
| 233 | + cx.paint_quad( | |
| 234 | + Bounds { | |
| 235 | + origin: background_origin, | |
| 236 | + size: size(line_end_x - background_origin.x, line_height), | |
| 237 | + }, | |
| 238 | + Corners::default(), | |
| 239 | + background_color, | |
| 240 | + Edges::default(), | |
| 241 | + transparent_black(), | |
| 242 | + ); | |
| 243 | + } | |
| 244 | + | |
| 191 | 245 | if let Some((underline_start, underline_style)) = current_underline.take() { |
| 192 | 246 | let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); |
| 193 | 247 | cx.paint_underline( |
| 194 | 248 | underline_start, |
| 195 | 249 | line_end_x - underline_start.x, |
| 196 | 250 | &underline_style, |
| 197 | − )?; | |
| 251 | + ); | |
| 198 | 252 | } |
| 199 | 253 | |
| 200 | 254 | Ok(()) |
| @@ -198,6 +198,41 @@ impl WrappedLineLayout { · 197 unmodified lines | ||
| 198 | 198 | pub fn runs(&self) -> &[ShapedRun] { |
| 199 | 199 | &self.unwrapped_layout.runs |
| 200 | 200 | } |
| 201 | + | |
| 202 | + pub fn index_for_position( | |
| 203 | + &self, | |
| 204 | + position: Point<Pixels>, | |
| 205 | + line_height: Pixels, | |
| 206 | + ) -> Option<usize> { | |
| 207 | + let wrapped_line_ix = (position.y / line_height) as usize; | |
| 208 | + | |
| 209 | + let wrapped_line_start_x = if wrapped_line_ix > 0 { | |
| 210 | + let wrap_boundary_ix = wrapped_line_ix - 1; | |
| 211 | + let wrap_boundary = self.wrap_boundaries[wrap_boundary_ix]; | |
| 212 | + let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix]; | |
| 213 | + run.glyphs[wrap_boundary.glyph_ix].position.x | |
| 214 | + } else { | |
| 215 | + Pixels::ZERO | |
| 216 | + }; | |
| 217 | + | |
| 218 | + let wrapped_line_end_x = if wrapped_line_ix < self.wrap_boundaries.len() { | |
| 219 | + let next_wrap_boundary_ix = wrapped_line_ix; | |
| 220 | + let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix]; | |
| 221 | + let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix]; | |
| 222 | + run.glyphs[next_wrap_boundary.glyph_ix].position.x | |
| 223 | + } else { | |
| 224 | + self.unwrapped_layout.width | |
| 225 | + }; | |
| 226 | + | |
| 227 | + let mut position_in_unwrapped_line = position; | |
| 228 | + position_in_unwrapped_line.x += wrapped_line_start_x; | |
| 229 | + if position_in_unwrapped_line.x > wrapped_line_end_x { | |
| 230 | + None | |
| 231 | + } else { | |
| 232 | + self.unwrapped_layout | |
| 233 | + .index_for_x(position_in_unwrapped_line.x) | |
| 234 | + } | |
| 235 | + } | |
| 201 | 236 | } |
| 202 | 237 | |
| 203 | 238 | pub(crate) struct LineLayoutCache { |
| @@ -881,7 +881,7 @@ impl<'a> WindowContext<'a> { · 880 unmodified lines | ||
| 881 | 881 | origin: Point<Pixels>, |
| 882 | 882 | width: Pixels, |
| 883 | 883 | style: &UnderlineStyle, |
| 884 | − ) -> Result<()> { | |
| 884 | + ) { | |
| 885 | 885 | let scale_factor = self.scale_factor(); |
| 886 | 886 | let height = if style.wavy { |
| 887 | 887 | style.thickness * 3. |
| @@ -905,7 +905,6 @@ impl<'a> WindowContext<'a> { · 17 unmodified lines | ||
| 905 | 905 | wavy: style.wavy, |
| 906 | 906 | }, |
| 907 | 907 | ); |
| 908 | − Ok(()) | |
| 909 | 908 | } |
| 910 | 909 | |
| 911 | 910 | /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | use gpui::{ |
| 2 | − blue, div, red, white, Div, ParentElement, Render, Styled, View, VisualContext, WindowContext, | |
| 2 | + blue, div, green, red, white, Div, InteractiveText, ParentElement, Render, Styled, StyledText, | |
| 3 | + TextRun, View, VisualContext, WindowContext, | |
| 3 | 4 | }; |
| 4 | 5 | use ui::v_stack; |
| 5 | 6 | |
| @@ -55,6 +56,21 @@ impl Render for TextStory { · 49 unmodified lines | ||
| 55 | 56 | "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", |
| 56 | 57 | "Meanwhile, the lazy dog decided it was time for a change. ", |
| 57 | 58 | "He started daily workout routines, ate healthier and became the fastest dog in town.", |
| 58 | − ))) | |
| 59 | + ))).child( | |
| 60 | + InteractiveText::new( | |
| 61 | + "interactive", | |
| 62 | + StyledText::new("Hello world, how is it going?").with_runs(vec![ | |
| 63 | + cx.text_style().to_run(6), | |
| 64 | + TextRun { | |
| 65 | + background_color: Some(green()), | |
| 66 | + ..cx.text_style().to_run(5) | |
| 67 | + }, | |
| 68 | + cx.text_style().to_run(18), | |
| 69 | + ]), | |
| 70 | + ) | |
| 71 | + .on_click(vec![2..4, 1..3, 7..9], |range_ix, cx| { | |
| 72 | + println!("Clicked range {range_ix}"); | |
| 73 | + }) | |
| 74 | + ) | |
| 59 | 75 | } |
| 60 | 76 | } |
| @@ -150,7 +150,7 @@ impl RenderOnce for HighlightedLabel { · 149 unmodified lines | ||
| 150 | 150 | LabelSize::Default => this.text_ui(), |
| 151 | 151 | LabelSize::Small => this.text_ui_sm(), |
| 152 | 152 | }) |
| 153 | − .child(StyledText::new(self.label, runs)) | |
| 153 | + .child(StyledText::new(self.label).with_runs(runs)) | |
| 154 | 154 | } |
| 155 | 155 | } |
| 156 | 156 | |