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 | font_weight: FontWeight::NORMAL, | 9423 | font_weight: FontWeight::NORMAL, |
| 9424 | font_style: FontStyle::Normal, | 9424 | font_style: FontStyle::Normal, |
| 9425 | line_height: relative(1.).into(), | 9425 | line_height: relative(1.).into(), |
| 9426 | background_color: None, | ||
| 9426 | underline: None, | 9427 | underline: None, |
| 9427 | white_space: WhiteSpace::Normal, | 9428 | white_space: WhiteSpace::Normal, |
| 9428 | }, | 9429 | }, |
| @@ -9437,6 +9438,7 @@ impl Render for Editor { · 8 unmodified lines | |||
| 9437 | font_weight: FontWeight::NORMAL, | 9438 | font_weight: FontWeight::NORMAL, |
| 9438 | font_style: FontStyle::Normal, | 9439 | font_style: FontStyle::Normal, |
| 9439 | line_height: relative(settings.buffer_line_height.value()), | 9440 | line_height: relative(settings.buffer_line_height.value()), |
| 9441 | background_color: None, | ||
| 9440 | underline: None, | 9442 | underline: None, |
| 9441 | white_space: WhiteSpace::Normal, | 9443 | white_space: WhiteSpace::Normal, |
| 9442 | }, | 9444 | }, |
| @@ -2452,7 +2452,7 @@ impl LineWithInvisibles { · 2451 unmodified lines | |||
| 2452 | len: line_chunk.len(), | 2452 | len: line_chunk.len(), |
| 2453 | font: text_style.font(), | 2453 | font: text_style.font(), |
| 2454 | color: text_style.color, | 2454 | color: text_style.color, |
| 2455 | background_color: None, | 2455 | background_color: text_style.background_color, |
| 2456 | underline: text_style.underline, | 2456 | underline: text_style.underline, |
| 2457 | }); | 2457 | }); |
| 2458 | 2458 | ||
| @@ -1,11 +1,11 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | Bounds, Element, ElementId, IntoElement, LayoutId, Pixels, SharedString, Size, TextRun, | 2 | Bounds, DispatchPhase, Element, ElementId, IntoElement, LayoutId, MouseDownEvent, MouseUpEvent, |
| 3 | WhiteSpace, WindowContext, WrappedLine, | 3 | Pixels, Point, SharedString, Size, TextRun, WhiteSpace, WindowContext, WrappedLine, |
| 4 | }; | 4 | }; |
| 5 | use anyhow::anyhow; | 5 | use anyhow::anyhow; |
| 6 | use parking_lot::{Mutex, MutexGuard}; | 6 | use parking_lot::{Mutex, MutexGuard}; |
| 7 | use smallvec::SmallVec; | 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 | use util::ResultExt; | 9 | use util::ResultExt; |
| 10 | 10 | ||
| 11 | impl Element for &'static str { | 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 | pub struct StyledText { | 77 | pub struct StyledText { |
| 73 | text: SharedString, | 78 | text: SharedString, |
| 74 | runs: Option<Vec<TextRun>>, | 79 | runs: Option<Vec<TextRun>>, |
| 75 | } | 80 | } |
| 76 | 81 | ||
| 77 | impl StyledText { | 82 | impl StyledText { |
| 78 | /// Renders text with runs of different styles. | 83 | pub fn new(text: impl Into<SharedString>) -> Self { |
| 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 { | ||
| 84 | StyledText { | 84 | StyledText { |
| 85 | text, | 85 | text: text.into(), |
| 86 | runs: Some(runs), | 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 | impl Element for StyledText { | 96 | impl Element for StyledText { |
| @@ -226,16 +231,73 @@ impl TextState { · 134 unmodified lines | |||
| 226 | line_origin.y += line.size(line_height).height; | 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 | element_id: ElementId, | 262 | element_id: ElementId, |
| 233 | text: StyledText, | 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 | text_state: TextState, | 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 | impl Element for InteractiveText { | 303 | impl Element for InteractiveText { |
| @@ -247,27 +309,62 @@ impl Element for InteractiveText { · 5 unmodified lines | |||
| 247 | cx: &mut WindowContext, | 309 | cx: &mut WindowContext, |
| 248 | ) -> (LayoutId, Self::State) { | 310 | ) -> (LayoutId, Self::State) { |
| 249 | if let Some(InteractiveTextState { | 311 | if let Some(InteractiveTextState { |
| 250 | text_state, | 312 | mouse_down_index, .. |
| 251 | clicked_range_ixs, | ||
| 252 | }) = state | 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 | let element_state = InteractiveTextState { | 316 | let element_state = InteractiveTextState { |
| 256 | text_state, | 317 | text_state, |
| 257 | clicked_range_ixs, | 318 | mouse_down_index, |
| 258 | }; | 319 | }; |
| 259 | (layout_id, element_state) | 320 | (layout_id, element_state) |
| 260 | } else { | 321 | } else { |
| 261 | let (layout_id, text_state) = self.text.layout(None, cx); | 322 | let (layout_id, text_state) = self.text.layout(None, cx); |
| 262 | let element_state = InteractiveTextState { | 323 | let element_state = InteractiveTextState { |
| 263 | text_state, | 324 | text_state, |
| 264 | clicked_range_ixs: Rc::default(), | 325 | mouse_down_index: Rc::default(), |
| 265 | }; | 326 | }; |
| 266 | (layout_id, element_state) | 327 | (layout_id, element_state) |
| 267 | } | 328 | } |
| 268 | } | 329 | } |
| 269 | 330 | ||
| 270 | fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) { | 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 | self.text.paint(bounds, &mut state.text_state, cx) | 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 | pub line_height: DefiniteLength, | 145 | pub line_height: DefiniteLength, |
| 146 | pub font_weight: FontWeight, | 146 | pub font_weight: FontWeight, |
| 147 | pub font_style: FontStyle, | 147 | pub font_style: FontStyle, |
| 148 | pub background_color: Option<Hsla>, | ||
| 148 | pub underline: Option<UnderlineStyle>, | 149 | pub underline: Option<UnderlineStyle>, |
| 149 | pub white_space: WhiteSpace, | 150 | pub white_space: WhiteSpace, |
| 150 | } | 151 | } |
| @@ -159,6 +160,7 @@ impl Default for TextStyle { · 8 unmodified lines | |||
| 159 | line_height: phi(), | 160 | line_height: phi(), |
| 160 | font_weight: FontWeight::default(), | 161 | font_weight: FontWeight::default(), |
| 161 | font_style: FontStyle::default(), | 162 | font_style: FontStyle::default(), |
| 163 | background_color: None, | ||
| 162 | underline: None, | 164 | underline: None, |
| 163 | white_space: WhiteSpace::Normal, | 165 | white_space: WhiteSpace::Normal, |
| 164 | } | 166 | } |
| @@ -182,6 +184,10 @@ impl TextStyle { · 17 unmodified lines | |||
| 182 | self.color.fade_out(factor); | 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 | if let Some(underline) = style.underline { | 191 | if let Some(underline) = style.underline { |
| 186 | self.underline = Some(underline); | 192 | self.underline = Some(underline); |
| 187 | } | 193 | } |
| @@ -212,7 +218,7 @@ impl TextStyle { · 24 unmodified lines | |||
| 212 | style: self.font_style, | 218 | style: self.font_style, |
| 213 | }, | 219 | }, |
| 214 | color: self.color, | 220 | color: self.color, |
| 215 | background_color: None, | 221 | background_color: self.background_color, |
| 216 | underline: self.underline.clone(), | 222 | underline: self.underline.clone(), |
| 217 | } | 223 | } |
| 218 | } | 224 | } |
| @@ -223,6 +229,7 @@ pub struct HighlightStyle { · 4 unmodified lines | |||
| 223 | pub color: Option<Hsla>, | 229 | pub color: Option<Hsla>, |
| 224 | pub font_weight: Option<FontWeight>, | 230 | pub font_weight: Option<FontWeight>, |
| 225 | pub font_style: Option<FontStyle>, | 231 | pub font_style: Option<FontStyle>, |
| 232 | pub background_color: Option<Hsla>, | ||
| 226 | pub underline: Option<UnderlineStyle>, | 233 | pub underline: Option<UnderlineStyle>, |
| 227 | pub fade_out: Option<f32>, | 234 | pub fade_out: Option<f32>, |
| 228 | } | 235 | } |
| @@ -441,6 +448,7 @@ impl From<&TextStyle> for HighlightStyle { · 212 unmodified lines | |||
| 441 | color: Some(other.color), | 448 | color: Some(other.color), |
| 442 | font_weight: Some(other.font_weight), | 449 | font_weight: Some(other.font_weight), |
| 443 | font_style: Some(other.font_style), | 450 | font_style: Some(other.font_style), |
| 451 | background_color: other.background_color, | ||
| 444 | underline: other.underline.clone(), | 452 | underline: other.underline.clone(), |
| 445 | fade_out: None, | 453 | fade_out: None, |
| 446 | } | 454 | } |
| @@ -467,6 +475,10 @@ impl HighlightStyle { · 20 unmodified lines | |||
| 467 | self.font_style = other.font_style; | 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 | if other.underline.is_some() { | 482 | if other.underline.is_some() { |
| 471 | self.underline = other.underline; | 483 | self.underline = other.underline; |
| 472 | } | 484 | } |
| @@ -361,6 +361,13 @@ pub trait Styled: Sized { · 360 unmodified lines | |||
| 361 | self | 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 | fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self { | 371 | fn text_size(mut self, size: impl Into<AbsoluteLength>) -> Self { |
| 365 | self.text_style() | 372 | self.text_style() |
| 366 | .get_or_insert_with(Default::default) | 373 | .get_or_insert_with(Default::default) |
| @@ -196,7 +196,10 @@ impl TextSystem { · 195 unmodified lines | |||
| 196 | let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); | 196 | let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new(); |
| 197 | for run in runs { | 197 | for run in runs { |
| 198 | if let Some(last_run) = decoration_runs.last_mut() { | 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 | last_run.len += run.len as u32; | 203 | last_run.len += run.len as u32; |
| 201 | continue; | 204 | continue; |
| 202 | } | 205 | } |
| @@ -204,6 +207,7 @@ impl TextSystem { · 1 unmodified line | |||
| 204 | decoration_runs.push(DecorationRun { | 207 | decoration_runs.push(DecorationRun { |
| 205 | len: run.len as u32, | 208 | len: run.len as u32, |
| 206 | color: run.color, | 209 | color: run.color, |
| 210 | background_color: run.background_color, | ||
| 207 | underline: run.underline.clone(), | 211 | underline: run.underline.clone(), |
| 208 | }); | 212 | }); |
| 209 | } | 213 | } |
| @@ -254,13 +258,16 @@ impl TextSystem { · 44 unmodified lines | |||
| 254 | } | 258 | } |
| 255 | 259 | ||
| 256 | if decoration_runs.last().map_or(false, |last_run| { | 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 | decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; | 265 | decoration_runs.last_mut().unwrap().len += run_len_within_line as u32; |
| 260 | } else { | 266 | } else { |
| 261 | decoration_runs.push(DecorationRun { | 267 | decoration_runs.push(DecorationRun { |
| 262 | len: run_len_within_line as u32, | 268 | len: run_len_within_line as u32, |
| 263 | color: run.color, | 269 | color: run.color, |
| 270 | background_color: run.background_color, | ||
| 264 | underline: run.underline.clone(), | 271 | underline: run.underline.clone(), |
| 265 | }); | 272 | }); |
| 266 | } | 273 | } |
| @@ -1,6 +1,7 @@ | |||
| 1 | use crate::{ | 1 | use crate::{ |
| 2 | black, point, px, BorrowWindow, Bounds, Hsla, LineLayout, Pixels, Point, Result, SharedString, | 2 | black, point, px, size, transparent_black, BorrowWindow, Bounds, Corners, Edges, Hsla, |
| 3 | UnderlineStyle, WindowContext, WrapBoundary, WrappedLineLayout, | 3 | LineLayout, Pixels, Point, Result, SharedString, UnderlineStyle, WindowContext, WrapBoundary, |
| 4 | WrappedLineLayout, | ||
| 4 | }; | 5 | }; |
| 5 | use derive_more::{Deref, DerefMut}; | 6 | use derive_more::{Deref, DerefMut}; |
| 6 | use smallvec::SmallVec; | 7 | use smallvec::SmallVec; |
| @@ -10,6 +11,7 @@ use std::sync::Arc; · 3 unmodified lines | |||
| 10 | pub struct DecorationRun { | 11 | pub struct DecorationRun { |
| 11 | pub len: u32, | 12 | pub len: u32, |
| 12 | pub color: Hsla, | 13 | pub color: Hsla, |
| 14 | pub background_color: Option<Hsla>, | ||
| 13 | pub underline: Option<UnderlineStyle>, | 15 | pub underline: Option<UnderlineStyle>, |
| 14 | } | 16 | } |
| 15 | 17 | ||
| @@ -97,6 +99,7 @@ fn paint_line( · 81 unmodified lines | |||
| 97 | let mut run_end = 0; | 99 | let mut run_end = 0; |
| 98 | let mut color = black(); | 100 | let mut color = black(); |
| 99 | let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; | 101 | let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; |
| 102 | let mut current_background: Option<(Point<Pixels>, Hsla)> = None; | ||
| 100 | let text_system = cx.text_system().clone(); | 103 | let text_system = cx.text_system().clone(); |
| 101 | let mut glyph_origin = origin; | 104 | let mut glyph_origin = origin; |
| 102 | let mut prev_glyph_position = Point::default(); | 105 | let mut prev_glyph_position = Point::default(); |
| @@ -110,12 +113,24 @@ fn paint_line( · 7 unmodified lines | |||
| 110 | 113 | ||
| 111 | if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { | 114 | if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) { |
| 112 | wraps.next(); | 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 | if let Some((underline_origin, underline_style)) = current_underline.take() { | 128 | if let Some((underline_origin, underline_style)) = current_underline.take() { |
| 114 | cx.paint_underline( | 129 | cx.paint_underline( |
| 115 | underline_origin, | 130 | underline_origin, |
| 116 | glyph_origin.x - underline_origin.x, | 131 | glyph_origin.x - underline_origin.x, |
| 117 | &underline_style, | 132 | &underline_style, |
| 118 | )?; | 133 | ); |
| 119 | } | 134 | } |
| 120 | 135 | ||
| 121 | glyph_origin.x = origin.x; | 136 | glyph_origin.x = origin.x; |
| @@ -123,9 +138,20 @@ fn paint_line( · 1 unmodified line | |||
| 123 | } | 138 | } |
| 124 | prev_glyph_position = glyph.position; | 139 | prev_glyph_position = glyph.position; |
| 125 | 140 | ||
| 141 | let mut finished_background: Option<(Point<Pixels>, Hsla)> = None; | ||
| 126 | let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; | 142 | let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None; |
| 127 | if glyph.index >= run_end { | 143 | if glyph.index >= run_end { |
| 128 | if let Some(style_run) = decoration_runs.next() { | 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 | if let Some((_, underline_style)) = &mut current_underline { | 155 | if let Some((_, underline_style)) = &mut current_underline { |
| 130 | if style_run.underline.as_ref() != Some(underline_style) { | 156 | if style_run.underline.as_ref() != Some(underline_style) { |
| 131 | finished_underline = current_underline.take(); | 157 | finished_underline = current_underline.take(); |
| @@ -149,16 +175,30 @@ fn paint_line( · 17 unmodified lines | |||
| 149 | color = style_run.color; | 175 | color = style_run.color; |
| 150 | } else { | 176 | } else { |
| 151 | run_end = layout.len; | 177 | run_end = layout.len; |
| 178 | finished_background = current_background.take(); | ||
| 152 | finished_underline = current_underline.take(); | 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 | if let Some((underline_origin, underline_style)) = finished_underline { | 196 | if let Some((underline_origin, underline_style)) = finished_underline { |
| 157 | cx.paint_underline( | 197 | cx.paint_underline( |
| 158 | underline_origin, | 198 | underline_origin, |
| 159 | glyph_origin.x - underline_origin.x, | 199 | glyph_origin.x - underline_origin.x, |
| 160 | &underline_style, | 200 | &underline_style, |
| 161 | )?; | 201 | ); |
| 162 | } | 202 | } |
| 163 | 203 | ||
| 164 | let max_glyph_bounds = Bounds { | 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 | if let Some((underline_start, underline_style)) = current_underline.take() { | 245 | if let Some((underline_start, underline_style)) = current_underline.take() { |
| 192 | let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); | 246 | let line_end_x = origin.x + wrap_width.unwrap_or(Pixels::MAX).min(layout.width); |
| 193 | cx.paint_underline( | 247 | cx.paint_underline( |
| 194 | underline_start, | 248 | underline_start, |
| 195 | line_end_x - underline_start.x, | 249 | line_end_x - underline_start.x, |
| 196 | &underline_style, | 250 | &underline_style, |
| 197 | )?; | 251 | ); |
| 198 | } | 252 | } |
| 199 | 253 | ||
| 200 | Ok(()) | 254 | Ok(()) |
| @@ -198,6 +198,41 @@ impl WrappedLineLayout { · 197 unmodified lines | |||
| 198 | pub fn runs(&self) -> &[ShapedRun] { | 198 | pub fn runs(&self) -> &[ShapedRun] { |
| 199 | &self.unwrapped_layout.runs | 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 | pub(crate) struct LineLayoutCache { | 238 | pub(crate) struct LineLayoutCache { |
| @@ -881,7 +881,7 @@ impl<'a> WindowContext<'a> { · 880 unmodified lines | |||
| 881 | origin: Point<Pixels>, | 881 | origin: Point<Pixels>, |
| 882 | width: Pixels, | 882 | width: Pixels, |
| 883 | style: &UnderlineStyle, | 883 | style: &UnderlineStyle, |
| 884 | ) -> Result<()> { | 884 | ) { |
| 885 | let scale_factor = self.scale_factor(); | 885 | let scale_factor = self.scale_factor(); |
| 886 | let height = if style.wavy { | 886 | let height = if style.wavy { |
| 887 | style.thickness * 3. | 887 | style.thickness * 3. |
| @@ -905,7 +905,6 @@ impl<'a> WindowContext<'a> { · 17 unmodified lines | |||
| 905 | wavy: style.wavy, | 905 | wavy: style.wavy, |
| 906 | }, | 906 | }, |
| 907 | ); | 907 | ); |
| 908 | Ok(()) | ||
| 909 | } | 908 | } |
| 910 | 909 | ||
| 911 | /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. | 910 | /// Paint a monochrome (non-emoji) glyph into the scene for the current frame at the current z-index. |
| @@ -1,5 +1,6 @@ | |||
| 1 | use gpui::{ | 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 | use ui::v_stack; | 5 | use ui::v_stack; |
| 5 | 6 | ||
| @@ -55,6 +56,21 @@ impl Render for TextStory { · 49 unmodified lines | |||
| 55 | "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", | 56 | "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", |
| 56 | "Meanwhile, the lazy dog decided it was time for a change. ", | 57 | "Meanwhile, the lazy dog decided it was time for a change. ", |
| 57 | "He started daily workout routines, ate healthier and became the fastest dog in town.", | 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 | LabelSize::Default => this.text_ui(), | 150 | LabelSize::Default => this.text_ui(), |
| 151 | LabelSize::Small => this.text_ui_sm(), | 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 | ||