Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/formatted_text_element.rs
StratoSDK / crates / strato-ui-core / src / elements / formatted_text_element.rs
1use super::{Highlight, ListNumbering, Selection};
2use crate::elements::{
3 ClickableCharRange, CornerRadius, Fill, HighlightedRange, HoverableCharRange, MouseStateHandle,
4 PartialClickableElement, Radius, SecretRange, SelectableElement, SelectionFragment,
5 SmartSelectFn, ZIndex, SELECTED_HIGHLIGHT_COLOR,
6};
7use crate::event::ModifiersState;
8use crate::fonts::Weight;
9use crate::formatted_text::{FormattedText, FormattedTextFragment, FormattedTextLine, Hyperlink};
10use crate::geometry::rect::RectF;
11use crate::platform::Cursor;
12use crate::text::word_boundaries::WordBoundariesPolicy;
13use crate::text::{
14 char_slice, count_chars_up_to_byte, BlockHeaderSize, IsRect, SelectionDirection, SelectionType,
15 TextBuffer,
16};
17use crate::text_layout::{ClipConfig, TextAlignment, DEFAULT_TOP_BOTTOM_RATIO};
18use crate::text_offsets::{ByteOffset, CharOffset};
19use crate::Event;
20use crate::{
21 elements::{Axis, Point},
22 event::DispatchedEvent,
23 fonts::{FamilyId, Properties, Style},
24 platform::LineStyle,
25 text_layout::{StyleAndFont, TextFrame, TextStyle},
26 AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext,
27 SizeConstraint,
28};
29use itertools::Itertools;
30use pathfinder_color::ColorU;
31use pathfinder_geometry::vector::{vec2f, Vector2F};
32use std::borrow::Cow;
33use std::cell::RefCell;
34use std::cmp::Reverse;
35use std::default::Default;
36use std::ops::Range;
37use std::rc::Rc;
38use std::sync::Arc;
39use std::sync::Mutex;
40use std::sync::Once;
41use vec1::vec1;
42#[derive(Debug, Clone, PartialEq)]
43pub struct HeadingFontSizeMultipliers {
44 pub h1: f32,
45 pub h2: f32,
46 pub h3: f32,
47 pub h4: f32,
48 pub h5: f32,
49 pub h6: f32,
50}
51 
52impl Default for HeadingFontSizeMultipliers {
53 fn default() -> Self {
54 Self {
55 h1: BlockHeaderSize::Header1.font_size_multiplication_ratio(),
56 h2: BlockHeaderSize::Header2.font_size_multiplication_ratio(),
57 h3: BlockHeaderSize::Header3.font_size_multiplication_ratio(),
58 h4: BlockHeaderSize::Header4.font_size_multiplication_ratio(),
59 h5: BlockHeaderSize::Header5.font_size_multiplication_ratio(),
60 h6: BlockHeaderSize::Header6.font_size_multiplication_ratio(),
61 }
62 }
63}
64 
65impl HeadingFontSizeMultipliers {
66 pub fn get_multiplier(&self, heading_level: usize) -> f32 {
67 match heading_level {
68 1 => self.h1,
69 2 => self.h2,
70 3 => self.h3,
71 4 => self.h4,
72 5 => self.h5,
73 6 => self.h6,
74 _ => 1.0, // Default to normal font size for invalid heading levels
75 }
76 }
77}
78 
79pub type HighlightedHyperlink = Arc<Mutex<Option<HyperlinkPosition>>>;
80 
81const CODE_BLOCK_OFFSET: usize = 1;
82 
83// TODO: We should think about whether line height applies to notebooks as well.
84// Consider whether this element really needs a different default than DEFAULT_UI_LINE_HEIGHT_RATIO
85// used by the Text element.
86pub const DEFAULT_LINE_HEIGHT_RATIO: f32 = 1.4;
87const FRAME_SPACER_HEIGHT: f32 = 4.;
88const LINE_BREAK_HEIGHT: f32 = 13.;
89 
90const FULL_BULLET: &str = "•";
91const EMPTY_BULLET: &str = "◦";
92const SQUARE_BULLET: &str = "▪";
93 
94// Background color for the code block.
95const CODE_BLOCK_BACKGROUND: u32 = 0x00000055;
96const DEFAULT_HYPERLINK_COLOR: u32 = 0x7aa6daff;
97 
98#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct HyperlinkUrl {
100 pub url: String,
101}
102 
103impl<'a> From<&'a Hyperlink> for HyperlinkLens<'a> {
104 fn from(url_or_action: &'a Hyperlink) -> Self {
105 match url_or_action {
106 Hyperlink::Url(url) => HyperlinkLens::Url(url.as_str()),
107 Hyperlink::Action(action) => HyperlinkLens::Action(action.as_ref()),
108 }
109 }
110}
111 
112/// A lens into a formatted text hyperlink.
113pub enum HyperlinkLens<'a> {
114 Url(&'a str),
115 Action(&'a dyn crate::Action),
116}
117 
118#[derive(Clone, Default, PartialEq, Eq, Debug)]
119pub struct HyperlinkPosition {
120 frame_index: usize,
121 link_range: Range<usize>,
122}
123 
124struct HyperlinkSupport {
125 /// The highlighted hyperlink index.
126 highlighted_hyperlink: HighlightedHyperlink,
127 /// The highlighted hyperlink index.
128 hyperlink_font_color: ColorU,
129}
130 
131#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
132pub struct FormattedTextSelectionLocation {
133 pub frame_index: usize,
134 pub row_index: usize,
135 pub glyph_index: usize,
136}
137 
138enum SavedGlyphPosition {
139 FormattedTextLinePosition(FormattedTextSelectionLocation),
140 LaidOutTextFramePosition(FormattedTextSelectionLocation),
141}
142 
143struct SavedGlyphPositionIds {
144 position: SavedGlyphPosition,
145 position_id: String,
146}
147 
148pub struct FormattedTextElement {
149 formatted_text: Arc<FormattedText>,
150 family_id: FamilyId,
151 code_block_family_id: FamilyId,
152 font_size: f32,
153 line_height_ratio: f32,
154 heading_to_font_size_multipliers: HeadingFontSizeMultipliers,
155 size: Option<Vector2F>,
156 origin: Option<Point>,
157 text_color: ColorU,
158 text_selection_color: ColorU,
159 laid_out_text: Vec<LaidOutTextFrame>,
160 text_frame_mouse_handlers: Vec<Rc<RefCell<FrameMouseHandlers>>>,
161 alignment: TextAlignment,
162 inline_code_font_color: Option<ColorU>,
163 inline_code_bg_color: Option<ColorU>,
164 hyperlink_support: HyperlinkSupport,
165 saved_glyph_positions: Vec<SavedGlyphPositionIds>,
166 is_selectable: bool,
167 is_mouse_interaction_disabled: bool,
168 disable_text_wrapping: bool,
169 clip_config: Option<ClipConfig>,
170 #[cfg(debug_assertions)]
171 /// Captures the location of the constructor call site. This is used for debugging purposes.
172 constructor_location: Option<&'static std::panic::Location<'static>>,
173}
174 
175impl FormattedTextElement {
176 #[cfg_attr(debug_assertions, track_caller)]
177 fn internal_constructor(
178 formatted_text: Arc<FormattedText>,
179 font_size: f32,
180 family_id: FamilyId,
181 code_block_family_id: FamilyId,
182 text_color: ColorU,
183 text_selection_color: ColorU,
184 hyperlink_support: HyperlinkSupport,
185 ) -> Self {
186 Self {
187 formatted_text,
188 family_id,
189 code_block_family_id,
190 font_size,
191 line_height_ratio: DEFAULT_LINE_HEIGHT_RATIO,
192 heading_to_font_size_multipliers: HeadingFontSizeMultipliers::default(),
193 text_color,
194 text_selection_color,
195 size: None,
196 origin: None,
197 laid_out_text: vec![],
198 text_frame_mouse_handlers: vec![],
199 inline_code_font_color: None,
200 inline_code_bg_color: None,
201 alignment: Default::default(),
202 hyperlink_support,
203 saved_glyph_positions: vec![],
204 is_selectable: false,
205 is_mouse_interaction_disabled: false,
206 disable_text_wrapping: false,
207 clip_config: None,
208 #[cfg(debug_assertions)]
209 constructor_location: Some(std::panic::Location::caller()),
210 }
211 }
212 
213 /// Creates a new FormattedTextElement. Allows multiple [FormattedTextLine]s to be passed in.
214 /// This enables features like in-line hyperlinks and code blocks. If this is not needed,
215 /// consider the simpler [FormattedTextElement::from_str] constructor.
216 #[cfg_attr(debug_assertions, track_caller)]
217 pub fn new(
218 formatted_text: FormattedText,
219 font_size: f32,
220 family_id: FamilyId,
221 code_block_family_id: FamilyId,
222 text_color: ColorU,
223 highlight_index: HighlightedHyperlink,
224 ) -> Self {
225 Self::new_arc(
226 Arc::new(formatted_text),
227 font_size,
228 family_id,
229 code_block_family_id,
230 text_color,
231 highlight_index,
232 )
233 }
234 
235 /// Like [`FormattedTextElement::new`], but accepts an already-allocated [`Arc<FormattedText>`]
236 /// so callers that have a cached Arc can avoid an extra deep clone.
237 #[cfg_attr(debug_assertions, track_caller)]
238 pub fn new_arc(
239 formatted_text: Arc<FormattedText>,
240 font_size: f32,
241 family_id: FamilyId,
242 code_block_family_id: FamilyId,
243 text_color: ColorU,
244 highlight_index: HighlightedHyperlink,
245 ) -> Self {
246 Self::internal_constructor(
247 formatted_text,
248 font_size,
249 family_id,
250 code_block_family_id,
251 text_color,
252 *SELECTED_HIGHLIGHT_COLOR,
253 HyperlinkSupport {
254 highlighted_hyperlink: highlight_index,
255 hyperlink_font_color: ColorU::from_u32(DEFAULT_HYPERLINK_COLOR),
256 },
257 )
258 }
259 
260 /// Creates a new FormattedTextElement from a single `str`. Use this method similar to how you'd use
261 /// [Text::new], though currently FormattedTextElement is missing many features from `Text`. If you
262 /// find yourself needing multiple styles throughout the text body, or need to make use of hyperlinks,
263 /// consider using [FormattedTextElement::new] instead. The FormattedTextElement created will have mouse
264 /// interactions disabled by default
265 #[cfg_attr(debug_assertions, track_caller)]
266 pub fn from_str(
267 text: impl Into<Cow<'static, str>>,
268 family_id: FamilyId,
269 font_size: f32,
270 ) -> Self {
271 Self::internal_constructor(
272 Arc::new(FormattedText::new([FormattedTextLine::Line(vec![
273 FormattedTextFragment::plain_text(text.into()),
274 ])])),
275 font_size,
276 family_id,
277 family_id,
278 ColorU::white(),
279 *SELECTED_HIGHLIGHT_COLOR,
280 HyperlinkSupport {
281 highlighted_hyperlink: Default::default(),
282 hyperlink_font_color: ColorU::from_u32(DEFAULT_HYPERLINK_COLOR),
283 },
284 )
285 .disable_mouse_interaction()
286 }
287 
288 /// TODO (roland): There are cases where we need to set the line height to the UI default
289 /// of 1.2 used by the Text element so that a FormattedTextElement and Text element in the same
290 /// row are laid out with the same behavior and occupy the same height. If the row has height restrictions,
291 /// the larger line height of FormattedTextElement can cause the text to not render due to CoreText
292 /// thinking there isn't enough vertical space.
293 /// Consider whether this element really needs a different default of 1.4.
294 pub fn with_line_height_ratio(mut self, line_height_ratio: f32) -> Self {
295 self.line_height_ratio = line_height_ratio;
296 self
297 }
298 
299 pub fn with_heading_to_font_size_multipliers(
300 mut self,
301 heading_to_font_size_multipliers: HeadingFontSizeMultipliers,
302 ) -> Self {
303 self.heading_to_font_size_multipliers = heading_to_font_size_multipliers;
304 self
305 }
306 
307 pub fn with_color(mut self, color: ColorU) -> Self {
308 self.text_color = color;
309 self
310 }
311 
312 pub fn with_selection_color(mut self, color: ColorU) -> Self {
313 self.text_selection_color = color;
314 self
315 }
316 
317 pub fn with_weight(mut self, weight: Weight) -> Self {
318 let ft = Arc::make_mut(&mut self.formatted_text);
319 let lines = std::mem::take(&mut ft.lines);
320 ft.lines = lines
321 .into_iter()
322 .map(|mut line| {
323 line.set_weight(weight.to_custom_weight());
324 line
325 })
326 .collect();
327 self
328 }
329 
330 #[allow(dead_code)]
331 pub fn with_alignment(mut self, alignment: TextAlignment) -> Self {
332 self.alignment = alignment;
333 self
334 }
335 
336 /// Register a default handler for all _URL_ hyperlinks detected by the parser.
337 /// Note that this will clear all existing registered handlers!
338 /// For a version with support for actions, see [FormattedTextElement::register_default_click_handlers_with_actions].
339 pub fn register_default_click_handlers<S>(mut self, default_click_handler: S) -> Self
340 where
341 S: 'static + Fn(HyperlinkUrl, &mut EventContext, &AppContext),
342 {
343 self.text_frame_mouse_handlers.clear();
344 
345 let callback = Rc::new(default_click_handler);
346 self.register_handlers(|frame_mouse_handlers, (_index, line)| {
347 line.hyperlinks(false).into_iter().fold(
348 frame_mouse_handlers,
349 |mut frame_mouse_handlers, (range, link)| {
350 let callback = callback.clone();
351 frame_mouse_handlers = frame_mouse_handlers.with_hoverable_char_range(
352 range.clone(),
353 MouseStateHandle::default(),
354 Some(Cursor::PointingHand),
355 // Default hover handler does nothing. TODO: highlighting
356 move |_is_hovering, _ctx, _app| {},
357 );
358 frame_mouse_handlers = frame_mouse_handlers.with_clickable_char_range(
359 range,
360 move |_modifiers, ctx, app| {
361 if let Hyperlink::Url(url) = &link {
362 callback(HyperlinkUrl { url: url.clone() }, ctx, app);
363 }
364 },
365 );
366 frame_mouse_handlers
367 },
368 )
369 });
370 
371 let highlighted_link_pos = self
372 .hyperlink_support
373 .highlighted_hyperlink
374 .lock()
375 .expect("Failed to acquire lock on highlighted_hyperlink")
376 .clone();
377 
378 for (line_index, line) in self.formatted_text.lines.iter().enumerate() {
379 let style_ranges =
380 line.hyperlinks(false)
381 .into_iter()
382 .filter_map(|(range, _)| {
383 let mut text_style = TextStyle::new()
384 .with_foreground_color(self.hyperlink_support.hyperlink_font_color);
385 if highlighted_link_pos.as_ref().is_some_and(|pos| {
386 pos.frame_index == line_index && pos.link_range == range
387 }) {
388 text_style = text_style
389 .with_underline_color(self.hyperlink_support.hyperlink_font_color);
390 }
391 
392 let highlight_indices = range.clone().collect_vec();
393 if highlight_indices.is_empty() {
394 None
395 } else {
396 Some(HighlightedRange {
397 highlight_indices,
398 highlight: Highlight::new().with_text_style(text_style),
399 })
400 }
401 })
402 .collect_vec();
403 
404 let merged_range = HighlightedRange::merge_overlapping_ranges(style_ranges);
405 let sorted_range = merged_range
406 .into_iter()
407 .sorted_by_key(|range| range.highlight_indices[0]);
408 
409 if let Some(handler) = self.text_frame_mouse_handlers.get(line_index) {
410 let mut handler = handler.borrow_mut();
411 sorted_range.for_each(|style| {
412 handler.add_style(style);
413 });
414 }
415 }
416 
417 self
418 }
419 
420 /// Register a default handler for all hyperlinks detected by the parser.
421 /// Note that this will clear all existing registered handlers!
422 pub fn register_default_click_handlers_with_action_support<S>(
423 mut self,
424 default_click_handler: S,
425 ) -> Self
426 where
427 S: 'static + Fn(HyperlinkLens, &mut EventContext, &AppContext),
428 {
429 self.text_frame_mouse_handlers.clear();
430 
431 let callback = Rc::new(default_click_handler);
432 self.register_handlers(|frame_mouse_handlers, (_index, line)| {
433 line.hyperlinks(false).into_iter().fold(
434 frame_mouse_handlers,
435 |mut frame_mouse_handlers, (range, link)| {
436 let callback = callback.clone();
437 frame_mouse_handlers = frame_mouse_handlers.with_hoverable_char_range(
438 range.clone(),
439 MouseStateHandle::default(),
440 Some(Cursor::PointingHand),
441 // Default hover handler does nothing. TODO: highlighting
442 move |_is_hovering, _ctx, _app| {},
443 );
444 frame_mouse_handlers = frame_mouse_handlers.with_clickable_char_range(
445 range,
446 move |_modifiers, ctx, app| {
447 callback(HyperlinkLens::from(&link), ctx, app);
448 },
449 );
450 frame_mouse_handlers
451 },
452 )
453 });
454 
455 let highlighted_link_pos = self
456 .hyperlink_support
457 .highlighted_hyperlink
458 .lock()
459 .expect("Failed to acquire lock on highlighted_hyperlink")
460 .clone();
461 
462 for (line_index, line) in self.formatted_text.lines.iter().enumerate() {
463 let style_ranges =
464 line.hyperlinks(false)
465 .into_iter()
466 .filter_map(|(range, _)| {
467 let mut text_style = TextStyle::new()
468 .with_foreground_color(self.hyperlink_support.hyperlink_font_color);
469 if highlighted_link_pos.as_ref().is_some_and(|pos| {
470 pos.frame_index == line_index && pos.link_range == range
471 }) {
472 text_style = text_style
473 .with_underline_color(self.hyperlink_support.hyperlink_font_color);
474 }
475 
476 let highlight_indices = range.clone().collect_vec();
477 if highlight_indices.is_empty() {
478 None
479 } else {
480 Some(HighlightedRange {
481 highlight_indices,
482 highlight: Highlight::new().with_text_style(text_style),
483 })
484 }
485 })
486 .collect_vec();
487 
488 let merged_range = HighlightedRange::merge_overlapping_ranges(style_ranges);
489 let sorted_range = merged_range
490 .into_iter()
491 .sorted_by_key(|range| range.highlight_indices[0]);
492 
493 if let Some(handler) = self.text_frame_mouse_handlers.get(line_index) {
494 let mut handler = handler.borrow_mut();
495 sorted_range.for_each(|style| {
496 handler.add_style(style);
497 });
498 }
499 }
500 
501 self
502 }
503 
504 pub fn with_inline_code_properties(
505 mut self,
506 inline_code_font_color: Option<ColorU>,
507 inline_code_bg_color: Option<ColorU>,
508 ) -> Self {
509 self.inline_code_font_color = inline_code_font_color;
510 self.inline_code_bg_color = inline_code_bg_color;
511 self
512 }
513 
514 pub fn with_hyperlink_font_color(mut self, hyperlink_font_color: ColorU) -> Self {
515 self.hyperlink_support.hyperlink_font_color = hyperlink_font_color;
516 self
517 }
518 
519 pub fn set_selectable(mut self, selectable: bool) -> Self {
520 self.is_selectable = selectable;
521 self
522 }
523 
524 pub fn disable_mouse_interaction(mut self) -> Self {
525 self.is_mouse_interaction_disabled = true;
526 self
527 }
528 
529 /// Disables text wrapping within the element. When text doesn't fit within the available
530 /// width, the element will extend beyond its constraints rather than wrapping the text.
531 /// This forces the parent container to handle overflow by moving the element to a new line.
532 pub fn with_no_text_wrapping(mut self) -> Self {
533 self.disable_text_wrapping = true;
534 self
535 }
536 
537 /// Sets the clip configuration for text that doesn't fit within the available width.
538 /// This automatically disables text wrapping, as clipping only applies to single-line text.
539 pub fn with_clip(mut self, clip_config: ClipConfig) -> Self {
540 self.disable_text_wrapping = true;
541 self.clip_config = Some(clip_config);
542 self
543 }
544 
545 /// Given an absolute point, returns the frame, row, and glyph indexes that makes the
546 /// most sense for a caret position.
547 /// For example, with example text "here is example text", if the mouse point is over
548 /// |i|, the index could be either 5 or 6 depending which side of the |i| the mouse is
549 /// closest to.
550 ///
551 /// # Parameters
552 /// - `snapping_policy`: Defines how the function should behave when the point is not in the bounds
553 /// of any frame. The default is to snap to the beginning or the end of the frame if the point is to the
554 /// left or right of the text, but not snap to a frame if it's in between 2 frames.
555 fn position_for_point(
556 &self,
557 absolute_point: Vector2F,
558 snapping_policy: SnappingPolicy,
559 ) -> Option<FormattedTextSelectionLocation> {
560 let (Some(origin), Some(size)) = (self.origin(), self.size()) else {
561 return None;
562 };
563 
564 let relative_point = absolute_point - origin.xy;
565 
566 // Snap to first/last character if above/below text.
567 if relative_point.y() < 0. {
568 return (!self.laid_out_text.is_empty()).then_some(FormattedTextSelectionLocation {
569 frame_index: 0,
570 row_index: 0,
571 glyph_index: 0,
572 });
573 } else if relative_point.y() > size.y() {
574 // Get the last valid frame and its last character index.
575 let frame = self.laid_out_text.last()?;
576 let (row_index, glyph_index) = frame
577 .get_last_row_and_glyph_index(!snapping_policy.should_adjust_to_char_indices)?;
578 return Some(FormattedTextSelectionLocation {
579 frame_index: self.laid_out_text.len() - 1,
580 row_index,
581 glyph_index,
582 });
583 }
584 
585 // Find which frame contains the point and which row and glyph index it is in.
586 // Keep track of whether the last frame was before or after the point to check if the point
587 // landed in a gap between frames.
588 let mut point_after_last_frame = false;
589 for (frame_index, frame) in self.laid_out_text.iter().enumerate() {
590 let frame_bounds = frame.get_frame_bounds();
591 
592 let point_before_current_frame = absolute_point.y() < frame_bounds.min_y();
593 let point_after_current_frame = absolute_point.y() > frame_bounds.max_y();
594 if point_before_current_frame || point_after_current_frame {
595 if snapping_policy.should_snap_on_gap
596 && point_after_last_frame
597 && point_before_current_frame
598 {
599 // Snap to the beginning of the current frame
600 return Some(FormattedTextSelectionLocation {
601 frame_index,
602 row_index: 0,
603 glyph_index: 0,
604 });
605 }
606 
607 point_after_last_frame = point_after_current_frame;
608 continue;
609 }
610 
611 return match frame {
612 LaidOutTextFrame::Text { text_frame, .. }
613 | LaidOutTextFrame::CodeBlock { text_frame, .. }
614 | LaidOutTextFrame::Indented { text_frame, .. } => {
615 let relative_y_within_frame = absolute_point.y() - frame_bounds.min_y();
616 
617 // Find the line that the point is in.
618 let mut row_index = 0;
619 let mut line_height = 0.;
620 for line in text_frame.lines() {
621 line_height += line.height();
622 if relative_y_within_frame > line_height {
623 row_index += 1;
624 } else {
625 break;
626 }
627 }
628 if row_index >= text_frame.lines().len() {
629 row_index = text_frame.lines().len().saturating_sub(1);
630 }
631 let line = text_frame.lines().get(row_index)?;
632 
633 // Compute the relative x position within the frame.
634 let relative_x_within_frame =
635 absolute_point.x() - frame_bounds.min_x() - text_frame.line_x_offset(line);
636 
637 let mut glyph_index = if snapping_policy.should_snap_to_ends {
638 line.caret_index_for_x_unbounded(relative_x_within_frame)
639 } else {
640 line.caret_index_for_x(relative_x_within_frame)?
641 };
642 
643 if glyph_index == line.end_index()
644 && snapping_policy.should_adjust_to_char_indices
645 {
646 // If the point is at the end of the line, we should adjust to the last character
647 // index if the snapping policy forces it.
648 glyph_index = line.end_index() - 1;
649 }
650 
651 Some(FormattedTextSelectionLocation {
652 frame_index,
653 row_index,
654 glyph_index,
655 })
656 }
657 LaidOutTextFrame::LineBreak { .. } => {
658 // Treat line breaks as a single newline character
659 Some(FormattedTextSelectionLocation {
660 frame_index,
661 row_index: 0,
662 glyph_index: 0,
663 })
664 }
665 };
666 }
667 None
668 }
669 
670 /// Determines rendering boundaries for drawing the given selection.
671 /// Assumes that [`selection_start`] comes before [`selection_end`].
672 fn calculate_selection_bounds(
673 &self,
674 selection_start: FormattedTextSelectionLocation,
675 selection_end: FormattedTextSelectionLocation,
676 ) -> Vec<RectF> {
677 let start_frame_idx = selection_start.frame_index;
678 let start_row_idx = selection_start.row_index;
679 let end_frame_idx = selection_end.frame_index;
680 let end_row_idx = selection_end.row_index;
681 
682 let mut selection_bounds = Vec::new();
683 
684 for (frame_index, frame) in self
685 .laid_out_text
686 .iter()
687 .enumerate()
688 .skip(start_frame_idx)
689 .take(end_frame_idx - start_frame_idx + 1)
690 {
691 match frame {
692 LaidOutTextFrame::Text { text_frame, .. }
693 | LaidOutTextFrame::CodeBlock { text_frame, .. }
694 | LaidOutTextFrame::Indented { text_frame, .. } => {
695 let frame_bounds = frame.get_frame_bounds();
696 let mut frame_y = frame_bounds.min_y();
697 
698 let is_starting_frame = frame_index == start_frame_idx;
699 let is_ending_frame = frame_index == end_frame_idx;
700 let line_count = text_frame.lines().len();
701 
702 // Start drawing on the starting row if the current frame is the starting frame;
703 // otherwise, start from the first row.
704 let row_start = if is_starting_frame { start_row_idx } else { 0 };
705 
706 // Start drawing on the ending row if the current frame is the ending frame;
707 // otherwise, draw all rows.
708 let row_end = if is_ending_frame {
709 end_row_idx
710 } else {
711 line_count.saturating_sub(1)
712 };
713 
714 for row in text_frame.lines().iter().take(row_start) {
715 frame_y += row.height();
716 }
717 
718 for row_index in row_start..=row_end {
719 let line = match text_frame.lines().get(row_index) {
720 Some(line) => line,
721 None => continue,
722 };
723 let line_height = line.height();
724 let line_origin_x = frame_bounds.min_x() + text_frame.line_x_offset(line);
725 
726 // Draw highlight on the entire line if it's not the starting or the ending row;
727 // otherwise, draw highlight that starts/ends on the selection bound.
728 let start_x = if is_starting_frame && row_index == start_row_idx {
729 line.x_for_index(selection_start.glyph_index)
730 } else {
731 0.
732 };
733 let end_x = if is_ending_frame && row_index == end_row_idx {
734 line.x_for_index(selection_end.glyph_index)
735 } else {
736 line.width
737 };
738 let rect_origin = vec2f(line_origin_x + start_x, frame_y);
739 let rect_size = vec2f(end_x - start_x, line_height);
740 selection_bounds.push(RectF::new(rect_origin, rect_size));
741 frame_y += line_height;
742 }
743 }
744 LaidOutTextFrame::LineBreak { .. } => (),
745 }
746 }
747 selection_bounds
748 }
749 
750 /// Assumes that [`selection_start`] comes before [`selection_end`].
751 fn draw_selection(
752 &self,
753 selection_start: FormattedTextSelectionLocation,
754 selection_end: FormattedTextSelectionLocation,
755 ctx: &mut PaintContext,
756 ) {
757 if !self.is_selectable {
758 return;
759 }
760 for rect in self.calculate_selection_bounds(selection_start, selection_end) {
761 ctx.scene
762 .draw_rect_without_hit_recording(rect)
763 .with_background(Fill::Solid(self.text_selection_color));
764 }
765 }
766 
767 fn translate_selection_bound_to_line_bound(
768 &self,
769 bound: FormattedTextSelectionLocation,
770 line_start: bool,
771 ) -> Option<Vector2F> {
772 let origin = self.origin()?;
773 
774 let base_y_offset = self.laid_out_text[..bound.frame_index]
775 .iter()
776 .map(|f| f.calculate_frame_height())
777 .sum::<f32>();
778 let frame = self.laid_out_text.get(bound.frame_index)?;
779 
780 let (relative_x, y_offset) = match frame {
781 LaidOutTextFrame::Text { text_frame, .. }
782 | LaidOutTextFrame::CodeBlock { text_frame, .. }
783 | LaidOutTextFrame::Indented { text_frame, .. } => {
784 let frame_bounds = frame.get_frame_bounds();
785 let mut y_offset = frame_bounds.min_y() - origin.y();
786 // add the height of all lines before the selected line
787 y_offset += text_frame
788 .lines()
789 .iter()
790 .take(bound.row_index)
791 .map(|line| line.height())
792 .sum::<f32>();
793 // use the y-pos of the middle of the line to be selected
794 y_offset += text_frame.lines().get(bound.row_index)?.height() / 2.;
795 
796 let line = text_frame.lines().get(bound.row_index)?;
797 let relative_x = frame_bounds.min_x() - origin.x()
798 + text_frame.line_x_offset(line)
799 + if line_start { 0. } else { line.width };
800 
801 (relative_x, y_offset)
802 }
803 LaidOutTextFrame::LineBreak { frame_bounds } => {
804 // use the y-pos of the middle of the frame to select
805 (0., base_y_offset + frame_bounds.height() / 2.)
806 }
807 };
808 
809 Some(origin.xy + vec2f(relative_x, y_offset))
810 }
811 
812 pub fn register_handlers<F>(&mut self, register: F)
813 where
814 F: Fn(FrameMouseHandlers, (usize, &FormattedTextLine)) -> FrameMouseHandlers,
815 {
816 self.text_frame_mouse_handlers = self
817 .formatted_text
818 .lines
819 .iter()
820 .enumerate()
821 .map(|line| Rc::new(RefCell::new(register(FrameMouseHandlers::default(), line))))
822 .collect();
823 }
824 
825 /// Save a position_id in the position cache for a given glyph and frame in the text.
826 /// This can be used to position other elements relative to a char in the text element.
827 pub fn with_saved_glyph_position(
828 mut self,
829 glyph_index: usize,
830 frame_index: usize,
831 position_id: String,
832 ) -> Self {
833 self.saved_glyph_positions.push(SavedGlyphPositionIds {
834 position: SavedGlyphPosition::FormattedTextLinePosition(
835 FormattedTextSelectionLocation {
836 frame_index,
837 row_index: 0,
838 glyph_index,
839 },
840 ),
841 position_id,
842 });
843 self
844 }
845 
846 pub fn add_styles(
847 &mut self,
848 frame_index: usize,
849 sorted_styles: impl IntoIterator<Item = HighlightedRange>,
850 ) {
851 if let Some(handler) = self.text_frame_mouse_handlers.get(frame_index) {
852 let mut handler = handler.borrow_mut();
853 sorted_styles.into_iter().for_each(|style| {
854 handler.add_style(style);
855 });
856 }
857 }
858 
859 fn handle_mouse_moved(
860 &mut self,
861 position: Vector2F,
862 z_index: ZIndex,
863 ctx: &mut EventContext,
864 app: &AppContext,
865 ) -> bool {
866 if self.is_mouse_interaction_disabled {
867 return false;
868 }
869 
870 let is_covered = ctx.is_covered(Point::from_vec2f(position, z_index));
871 let mut handled = false;
872 
873 let mut highlighted_link = self
874 .hyperlink_support
875 .highlighted_hyperlink
876 .lock()
877 .expect("Failed to acquire lock on highlighted_hyperlink");
878 // Set to None. If all checks passed, we will set it to the hovered link.
879 *highlighted_link = None;
880 
881 if let Some(bounds) = self.bounds() {
882 // If the mouse is moving outside the bounds, ignore the event; otherwise,
883 // reset the cursor and clear all hover states.
884 if !bounds.contains_point(position) {
885 // If there was a link hovered, consider mouse to be moving from within the element to outside.
886 let was_hovered = self
887 .text_frame_mouse_handlers
888 .iter()
889 .any(|handlers| handlers.borrow_mut().unhover_all(ctx, app));
890 if was_hovered {
891 ctx.reset_cursor();
892 }
893 return false;
894 }
895 } else {
896 return false;
897 };
898 
899 let Some(text_pos) =
900 self.position_for_point(position, SnappingPolicy::precise_char_range())
901 else {
902 // If the mouse is within the bound of the element but not on any text frame,
903 // reset the cursor and clear all hover states.
904 for handlers in self.text_frame_mouse_handlers.iter() {
905 handlers.borrow_mut().unhover_all(ctx, app);
906 }
907 ctx.reset_cursor();
908 return true;
909 };
910 
911 let handlers = match self.laid_out_text.get(text_pos.frame_index) {
912 Some(
913 LaidOutTextFrame::Text { mouse_handlers, .. }
914 | LaidOutTextFrame::Indented { mouse_handlers, .. },
915 ) => mouse_handlers,
916 _ => return false,
917 };
918 
919 // Note that these are all char indices!
920 let mut handlers = handlers.borrow_mut();
921 let glyph_offset = handlers.glyph_offset;
922 
923 // Unhover all other frames.
924 let was_hovered = self
925 .laid_out_text
926 .iter()
927 .enumerate()
928 .any(|(i, laid_out_frame)| {
929 if i == text_pos.frame_index {
930 return false;
931 }
932 let handlers = match laid_out_frame {
933 LaidOutTextFrame::Text { mouse_handlers, .. }
934 | LaidOutTextFrame::Indented { mouse_handlers, .. } => mouse_handlers,
935 _ => return false,
936 };
937 handlers.borrow_mut().unhover_all(ctx, app)
938 });
939 if was_hovered {
940 ctx.reset_cursor();
941 }
942 
943 // If the mouse is on a frame without any hover handlers, reset the cursor
944 if handlers.hover_handlers.is_empty() {
945 ctx.reset_cursor();
946 return true;
947 }
948 
949 handlers
950 .hover_handlers
951 .iter_mut()
952 .for_each(|hoverable_range| {
953 let was_hovered = hoverable_range.mouse_state().is_hovered();
954 let adjusted_range = hoverable_range.char_range.start + glyph_offset.as_usize()
955 ..hoverable_range.char_range.end + glyph_offset.as_usize();
956 let is_hovered = !is_covered && adjusted_range.contains(&text_pos.glyph_index);
957 
958 if is_hovered != was_hovered {
959 hoverable_range.mouse_state().is_hovered = is_hovered;
960 let handler = hoverable_range.hover_handler.as_mut();
961 handler(is_hovered, ctx, app);
962 handled = true;
963 if let Some(cursor_on_hover) = hoverable_range.cursor_on_hover {
964 if is_hovered {
965 ctx.set_cursor(cursor_on_hover, z_index)
966 } else {
967 ctx.reset_cursor()
968 }
969 }
970 }
971 
972 if is_hovered {
973 *highlighted_link = Some(HyperlinkPosition {
974 // formatted text line index, not laid-out frame index
975 frame_index: self.frame_index_to_line_index(text_pos.frame_index),
976 link_range: hoverable_range.char_range.clone(),
977 });
978 }
979 });
980 handled
981 }
982 
983 fn handle_mouse_down(
984 &mut self,
985 position: Vector2F,
986 z_index: ZIndex,
987 modifiers: &ModifiersState,
988 ctx: &mut EventContext,
989 app: &AppContext,
990 ) -> bool {
991 if self.is_mouse_interaction_disabled
992 || ctx.is_covered(Point::from_vec2f(position, z_index))
993 {
994 return false;
995 }
996 
997 if !self
998 .bounds()
999 .is_some_and(|bounds| bounds.contains_point(position))
1000 {
1001 return false;
1002 }
1003 
1004 let Some(click_pos) =
1005 self.position_for_point(position, SnappingPolicy::precise_char_range())
1006 else {
1007 return false;
1008 };
1009 let handlers = match self.laid_out_text.get(click_pos.frame_index) {
1010 Some(
1011 LaidOutTextFrame::Text { mouse_handlers, .. }
1012 | LaidOutTextFrame::Indented { mouse_handlers, .. },
1013 ) => mouse_handlers,
1014 _ => return false,
1015 };
1016 
1017 let mut handlers = handlers.borrow_mut();
1018 let glyph_offset = handlers.glyph_offset;
1019 
1020 let mut handled = false;
1021 handlers
1022 .click_handlers
1023 .iter_mut()
1024 .for_each(|clickable_range| {
1025 let handler_char_range = (clickable_range.char_range.start
1026 + glyph_offset.as_usize())
1027 ..(clickable_range.char_range.end + glyph_offset.as_usize());
1028 if handler_char_range.contains(&click_pos.glyph_index) {
1029 let handler = clickable_range.click_handler.as_mut();
1030 handler(modifiers, ctx, app);
1031 handled = true;
1032 }
1033 });
1034 handled
1035 }
1036 
1037 fn frame_index_to_line_index(&self, frame_index: usize) -> usize {
1038 // Render text line-by-line.
1039 let mut lines = self.formatted_text.lines.iter().enumerate().peekable();
1040 // We don't use the frame_index from lines.next() because this creates inconsistencies in the frame index when we have lists
1041 let mut curr_frame_index = 0;
1042 let mut last_line_was_list_item = false;
1043 while let Some((line_index, line)) = lines.next() {
1044 if curr_frame_index == frame_index {
1045 return line_index;
1046 }
1047 
1048 let line_type = LineType::from(line);
1049 // Since list items are flattened into multiple lines rather than a single list segment,
1050 // this ends up causing line breaks between each individual list item. To avoid this,
1051 // we only show the line break if we already started a list and there isn't a following list item.
1052 let curr_line_is_line_break = matches!(line_type, LineType::LineBreak);
1053 let next_line_is_list_item = matches!(
1054 lines.peek(),
1055 Some((_, FormattedTextLine::UnorderedList(_)))
1056 | Some((_, FormattedTextLine::OrderedList(_)))
1057 );
1058 if last_line_was_list_item && curr_line_is_line_break && next_line_is_list_item {
1059 continue;
1060 }
1061 
1062 last_line_was_list_item =
1063 matches!(line_type, LineType::OrderedList | LineType::UnorderedList);
1064 
1065 curr_frame_index += 1;
1066 }
1067 
1068 curr_frame_index
1069 }
1070 
1071 /// Guarantees that any returned ranges are non-inverted (i.e. start before end)
1072 fn calculate_point_ranges(
1073 &self,
1074 current_selection: Option<Selection>,
1075 ) -> Option<
1076 Vec<(
1077 FormattedTextSelectionLocation,
1078 FormattedTextSelectionLocation,
1079 )>,
1080 > {
1081 let current_selection = current_selection?;
1082 let ((start_bound, start_pos), (end_bound, end_pos)) = {
1083 let start_point = self.position_for_point(
1084 current_selection.start,
1085 SnappingPolicy::default().snap_on_gap(),
1086 )?;
1087 let end_point = self.position_for_point(
1088 current_selection.end,
1089 SnappingPolicy::default().snap_on_gap(),
1090 )?;
1091 match start_point.frame_index.cmp(&end_point.frame_index) {
1092 std::cmp::Ordering::Equal => {
1093 if end_point.glyph_index >= start_point.glyph_index {
1094 (
1095 (start_point, current_selection.start),
1096 (end_point, current_selection.end),
1097 )
1098 } else {
1099 (
1100 (end_point, current_selection.end),
1101 (start_point, current_selection.start),
1102 )
1103 }
1104 }
1105 std::cmp::Ordering::Less => (
1106 (start_point, current_selection.start),
1107 (end_point, current_selection.end),
1108 ),
1109 std::cmp::Ordering::Greater => (
1110 (end_point, current_selection.end),
1111 (start_point, current_selection.start),
1112 ),
1113 }
1114 };
1115 
1116 match current_selection.is_rect {
1117 IsRect::True => self.compute_rect_selection_bounds(
1118 start_bound,
1119 end_bound,
1120 start_pos.x(),
1121 end_pos.x(),
1122 ),
1123 IsRect::False => Some(vec![(start_bound, end_bound)]),
1124 }
1125 }
1126 
1127 fn compute_rect_selection_bounds(
1128 &self,
1129 start_bound: FormattedTextSelectionLocation,
1130 end_bound: FormattedTextSelectionLocation,
1131 selection_start_x: f32,
1132 selection_end_x: f32,
1133 ) -> Option<
1134 Vec<(
1135 FormattedTextSelectionLocation,
1136 FormattedTextSelectionLocation,
1137 )>,
1138 > {
1139 if start_bound == end_bound {
1140 return None;
1141 }
1142 let mut rows_bounds = Vec::new();
1143 
1144 for (frame_index, frame) in self
1145 .laid_out_text
1146 .iter()
1147 .enumerate()
1148 .skip(start_bound.frame_index)
1149 .take(end_bound.frame_index - start_bound.frame_index + 1)
1150 {
1151 let text_frame = match frame {
1152 LaidOutTextFrame::Text { text_frame, .. }
1153 | LaidOutTextFrame::CodeBlock { text_frame, .. }
1154 | LaidOutTextFrame::Indented { text_frame, .. } => text_frame,
1155 LaidOutTextFrame::LineBreak { .. } => {
1156 continue;
1157 }
1158 };
1159 let lines = text_frame.lines();
1160 
1161 let start_row_index = if frame_index == start_bound.frame_index {
1162 start_bound.row_index
1163 } else {
1164 0
1165 };
1166 let end_row_index = if frame_index == end_bound.frame_index {
1167 end_bound.row_index
1168 } else {
1169 lines.len().saturating_sub(1)
1170 };
1171 
1172 for (row_index, line) in lines
1173 .iter()
1174 .enumerate()
1175 .skip(start_row_index)
1176 .take(end_row_index - start_row_index + 1)
1177 {
1178 let line_origin_x =
1179 frame.get_frame_bounds().min_x() + text_frame.line_x_offset(line);
1180 let start_caret_index =
1181 line.caret_index_for_x_unbounded(selection_start_x - line_origin_x);
1182 let end_caret_index =
1183 line.caret_index_for_x_unbounded(selection_end_x - line_origin_x);
1184 rows_bounds.push((
1185 FormattedTextSelectionLocation {
1186 frame_index,
1187 row_index,
1188 glyph_index: start_caret_index,
1189 },
1190 FormattedTextSelectionLocation {
1191 frame_index,
1192 row_index,
1193 glyph_index: end_caret_index,
1194 },
1195 ));
1196 }
1197 }
1198 
1199 Some(rows_bounds)
1200 }
1201 
1202 fn build_regular_selection_text(
1203 &self,
1204 start_bound: FormattedTextSelectionLocation,
1205 end_bound: FormattedTextSelectionLocation,
1206 ) -> Option<String> {
1207 // Handle selection within a single frame
1208 if start_bound.frame_index == end_bound.frame_index {
1209 let frame = self.laid_out_text.get(start_bound.frame_index)?;
1210 let text = frame.get_raw_text();
1211 
1212 let start_glyph_index = start_bound.glyph_index;
1213 let end_glyph_index = end_bound.glyph_index.min(text.chars().count());
1214 
1215 Some(char_slice(text, start_glyph_index, end_glyph_index)?.to_owned())
1216 } else {
1217 // Handle selection across multiple frames
1218 let mut result = String::new();
1219 
1220 // Handle start frame
1221 let start_frame = self.laid_out_text.get(start_bound.frame_index)?;
1222 let start_text = start_frame.get_raw_text();
1223 
1224 // The `if let` is necessary because `glyph_index` might point to the end of the line.
1225 // In such cases, `get_selection()` should ignore the starting line and resume from the next line.
1226 if let Some((start_byte_index, _)) =
1227 start_text.char_indices().nth(start_bound.glyph_index)
1228 {
1229 result.push_str(&start_text[start_byte_index..]);
1230 }
1231 
1232 // Handle middle frames
1233 for frame in self
1234 .laid_out_text
1235 .iter()
1236 .skip(start_bound.frame_index + 1)
1237 .take(end_bound.frame_index - start_bound.frame_index - 1)
1238 {
1239 let text = frame.get_raw_text();
1240 result.push('\n');
1241 result.push_str(text);
1242 }
1243 
1244 // Handle end frame
1245 let end_frame = self.laid_out_text.get(end_bound.frame_index)?;
1246 let end_text = end_frame.get_raw_text();
1247 let end_glyph_index = end_bound.glyph_index.min(end_text.chars().count());
1248 
1249 // This is to prevent slicing an empty string (i.e. a newline frame) and returning a None.
1250 if let Some(text) = char_slice(end_text, 0, end_glyph_index) {
1251 if !text.is_empty() {
1252 result.push('\n');
1253 result.push_str(text);
1254 }
1255 }
1256 
1257 Some(result)
1258 }
1259 }
1260 
1261 /// Returns a reference to the formatted text content of this element.
1262 pub fn formatted_text(&self) -> &FormattedText {
1263 &self.formatted_text
1264 }
1265}
1266 
1267enum LaidOutTextFrame {
1268 Text {
1269 text_frame: Arc<TextFrame>,
1270 frame_bounds: RectF,
1271 bottom_padding: f32,
1272 raw_text: String,
1273 mouse_handlers: Rc<RefCell<FrameMouseHandlers>>,
1274 },
1275 CodeBlock {
1276 text_frame: Arc<TextFrame>,
1277 frame_bounds: RectF,
1278 bottom_padding: f32,
1279 raw_text: String,
1280 },
1281 Indented {
1282 text_frame: Arc<TextFrame>,
1283 indent: usize,
1284 frame_bounds: RectF,
1285 top_padding: f32,
1286 bottom_padding: f32,
1287 left_padding: f32,
1288 raw_text: String,
1289 mouse_handlers: Rc<RefCell<FrameMouseHandlers>>,
1290 },
1291 LineBreak {
1292 frame_bounds: RectF,
1293 },
1294}
1295 
1296impl LaidOutTextFrame {
1297 /// Returns if a frame has a certain position inside of it.
1298 #[allow(dead_code)]
1299 fn contains(&self, position: Vector2F) -> bool {
1300 self.get_frame_bounds().contains_point(position)
1301 }
1302 
1303 fn get_frame_bounds(&self) -> &RectF {
1304 match self {
1305 LaidOutTextFrame::Text { frame_bounds, .. }
1306 | LaidOutTextFrame::CodeBlock { frame_bounds, .. }
1307 | LaidOutTextFrame::Indented { frame_bounds, .. }
1308 | LaidOutTextFrame::LineBreak { frame_bounds, .. } => frame_bounds,
1309 }
1310 }
1311 
1312 /// Helper function to calculate the x ofset of a text frame.
1313 pub fn calculate_x_offset(
1314 &self,
1315 font_size: f32,
1316 x_origin: f32,
1317 alignment: TextAlignment,
1318 frame_width: f32,
1319 ) -> f32 {
1320 let indent = match self {
1321 LaidOutTextFrame::Text { .. } | LaidOutTextFrame::LineBreak { .. } => 0,
1322 LaidOutTextFrame::CodeBlock { .. } => CODE_BLOCK_OFFSET,
1323 LaidOutTextFrame::Indented { indent, .. } => *indent,
1324 };
1325 
1326 match alignment {
1327 TextAlignment::Left => x_origin + font_size * indent as f32,
1328 TextAlignment::Right => x_origin + frame_width - self.width(),
1329 TextAlignment::Center => x_origin + (frame_width - self.width()) / 2.,
1330 }
1331 }
1332 
1333 pub fn width(&self) -> f32 {
1334 match self {
1335 LaidOutTextFrame::CodeBlock { text_frame, .. }
1336 | LaidOutTextFrame::Indented { text_frame, .. }
1337 | LaidOutTextFrame::Text { text_frame, .. } => text_frame.max_width(),
1338 LaidOutTextFrame::LineBreak { .. } => 0.,
1339 }
1340 }
1341 
1342 /// Helper function to get the frame height.
1343 pub fn calculate_frame_height(&self) -> f32 {
1344 match self {
1345 LaidOutTextFrame::Text {
1346 text_frame,
1347 bottom_padding,
1348 ..
1349 }
1350 | LaidOutTextFrame::CodeBlock {
1351 text_frame,
1352 bottom_padding,
1353 ..
1354 } => text_frame.height() + bottom_padding,
1355 LaidOutTextFrame::Indented {
1356 text_frame,
1357 top_padding,
1358 bottom_padding,
1359 ..
1360 } => text_frame.height() + bottom_padding + top_padding,
1361 LaidOutTextFrame::LineBreak { .. } => LINE_BREAK_HEIGHT,
1362 }
1363 }
1364 
1365 /// Returns the last row index and glyph index as `(row_index, glyph_index)` if there is at least one Line;
1366 /// otherwise returns `None`.
1367 /// # Parameters
1368 /// - `use_end_index`: If true, returns the end index of the last line; otherwise, returns the index of the last character.
1369 pub fn get_last_row_and_glyph_index(&self, use_end_index: bool) -> Option<(usize, usize)> {
1370 match self {
1371 LaidOutTextFrame::Text { text_frame, .. }
1372 | LaidOutTextFrame::CodeBlock { text_frame, .. }
1373 | LaidOutTextFrame::Indented { text_frame, .. } => {
1374 text_frame.lines().last().map(|line| {
1375 (
1376 text_frame.lines().len() - 1,
1377 if use_end_index {
1378 line.end_index()
1379 } else {
1380 line.last_index()
1381 },
1382 )
1383 })
1384 }
1385 LaidOutTextFrame::LineBreak { .. } => {
1386 // Treat line breaks as a single newline character
1387 Some((0, 0))
1388 }
1389 }
1390 }
1391 
1392 pub fn get_raw_text(&self) -> &str {
1393 match self {
1394 LaidOutTextFrame::Text { raw_text, .. }
1395 | LaidOutTextFrame::CodeBlock { raw_text, .. }
1396 | LaidOutTextFrame::Indented { raw_text, .. } => raw_text,
1397 LaidOutTextFrame::LineBreak { .. } => "",
1398 }
1399 }
1400}
1401 
1402#[derive(Default)]
1403pub struct FrameMouseHandlers {
1404 // contains the clickable char ranges and the corresponding click
1405 // handler for each char range
1406 click_handlers: Vec<ClickableCharRange>,
1407 // contains the hoverable char ranges and the corresponding hover
1408 // handler for each char range
1409 hover_handlers: Vec<HoverableCharRange>,
1410 glyph_offset: CharOffset,
1411 byte_offset: ByteOffset,
1412 secret_replacement: Vec<(SecretRange, Cow<'static, str>)>,
1413 styles: Vec<HighlightedRange>,
1414}
1415 
1416impl FrameMouseHandlers {
1417 fn add_offset(&mut self, offset: CharOffset, byte_offset: ByteOffset) {
1418 self.glyph_offset = offset;
1419 self.byte_offset = byte_offset;
1420 }
1421 
1422 /// Returns `true` if any of the hoverable ranges was hovered.
1423 fn unhover_all(&mut self, ctx: &mut EventContext, app: &AppContext) -> bool {
1424 let mut any_hovered = false;
1425 self.hover_handlers.iter_mut().for_each(|hoverable_range| {
1426 let hovered = hoverable_range.mouse_state().is_hovered();
1427 any_hovered |= hovered;
1428 if hovered {
1429 let handler = hoverable_range.hover_handler.as_mut();
1430 handler(false, ctx, app);
1431 hoverable_range.mouse_state().is_hovered = false;
1432 }
1433 });
1434 any_hovered
1435 }
1436 
1437 fn add_style(&mut self, style: HighlightedRange) {
1438 self.styles.push(style);
1439 }
1440}
1441 
1442static SECRET_REPLACEMENT_OOB_ONCE: Once = Once::new();
1443 
1444impl PartialClickableElement for FrameMouseHandlers {
1445 fn with_clickable_char_range<F>(
1446 mut self,
1447 clickable_char_range: Range<usize>,
1448 callback: F,
1449 ) -> Self
1450 where
1451 F: 'static + FnMut(&ModifiersState, &mut EventContext, &AppContext),
1452 {
1453 self.click_handlers.push(ClickableCharRange {
1454 char_range: clickable_char_range,
1455 click_handler: Box::new(callback),
1456 });
1457 self
1458 }
1459 
1460 fn with_hoverable_char_range<F>(
1461 mut self,
1462 hoverable_char_range: Range<usize>,
1463 mouse_state: MouseStateHandle,
1464 cursor_on_hover: Option<Cursor>,
1465 callback: F,
1466 ) -> Self
1467 where
1468 F: 'static + FnMut(bool, &mut EventContext, &AppContext),
1469 {
1470 self.hover_handlers.push(HoverableCharRange {
1471 char_range: hoverable_char_range,
1472 hover_handler: Box::new(callback),
1473 cursor_on_hover,
1474 mouse_state,
1475 });
1476 self
1477 }
1478 
1479 fn replace_text_range(&mut self, range: SecretRange, replacement: Cow<'static, str>) {
1480 self.secret_replacement.push((range, replacement));
1481 }
1482}
1483 
1484/// Applies secret replacements to the given text using char indices adjusted by glyph_offset.
1485/// Replacements are applied in descending order of start position to avoid shifting ranges.
1486fn apply_secret_replacements(
1487 text: &mut String,
1488 glyph_offset: usize,
1489 secret_replacements: &[(SecretRange, Cow<'static, str>)],
1490) {
1491 let mut replacements = secret_replacements.to_vec();
1492 replacements.sort_by_key(|(range, _)| Reverse(range.char_range.start));
1493 
1494 let total_chars = text.chars().count();
1495 let mut out_of_bound_message: Option<String> = None;
1496 
1497 for (range, replacement) in replacements.iter() {
1498 let start_char = range.char_range.start + glyph_offset;
1499 let end_char = range.char_range.end + glyph_offset;
1500 
1501 if start_char >= end_char {
1502 continue;
1503 }
1504 if start_char > total_chars {
1505 out_of_bound_message = Some(format!(
1506 "Secret redaction OOB: char start beyond length. range={:?}, start_char={}, end_char={}, total_chars={}, byte_len={}",
1507 start_char..end_char,
1508 start_char,
1509 end_char,
1510 total_chars,
1511 text.len()
1512 ));
1513 continue;
1514 }
1515 
1516 let start_byte = if start_char == total_chars {
1517 text.len()
1518 } else if let Some((byte_idx, _)) = text.char_indices().nth(start_char) {
1519 byte_idx
1520 } else {
1521 out_of_bound_message = Some(format!(
1522 "Secret redaction OOB: failed to map start_char to byte index. range={:?}, start_char={}, end_char={}, total_chars={}, byte_len={}",
1523 start_char..end_char,
1524 start_char,
1525 end_char,
1526 total_chars,
1527 text.len()
1528 ));
1529 continue;
1530 };
1531 
1532 let end_byte = if end_char >= total_chars {
1533 text.len()
1534 } else if let Some((byte_idx, _)) = text.char_indices().nth(end_char) {
1535 byte_idx
1536 } else {
1537 out_of_bound_message = Some(format!(
1538 "Secret redaction OOB: failed to map end_char to byte index. range={:?}, start_char={}, end_char={}, total_chars={}, byte_len={}",
1539 start_char..end_char,
1540 start_char,
1541 end_char,
1542 total_chars,
1543 text.len()
1544 ));
1545 continue;
1546 };
1547 
1548 if start_byte > end_byte || end_byte > text.len() {
1549 out_of_bound_message = Some(format!(
1550 "Secret redaction OOB: byte range invalid. start_byte={}, end_byte={}, byte_len={}, start_char={}, end_char={}, total_chars={}",
1551 start_byte,
1552 end_byte,
1553 text.len(),
1554 start_char,
1555 end_char,
1556 total_chars
1557 ));
1558 continue;
1559 }
1560 
1561 text.replace_range(start_byte..end_byte, replacement);
1562 }
1563 
1564 if let Some(msg) = out_of_bound_message {
1565 SECRET_REPLACEMENT_OOB_ONCE.call_once(|| log::error!("{msg}"));
1566 }
1567}
1568 
1569#[derive(Debug, Clone, Eq, PartialEq)]
1570enum LineType {
1571 OrderedList,
1572 UnorderedList,
1573 FormattedLine,
1574 CodeBlock,
1575 LineBreak,
1576}
1577 
1578impl From<&FormattedTextLine> for LineType {
1579 fn from(line: &FormattedTextLine) -> Self {
1580 match line {
1581 FormattedTextLine::OrderedList(_) => LineType::OrderedList,
1582 FormattedTextLine::UnorderedList(_) => LineType::UnorderedList,
1583 FormattedTextLine::Heading(_)
1584 | FormattedTextLine::Line(_)
1585 | FormattedTextLine::TaskList(_)
1586 | FormattedTextLine::Table(_) => LineType::FormattedLine,
1587 FormattedTextLine::CodeBlock(_) => LineType::CodeBlock,
1588 FormattedTextLine::LineBreak
1589 | FormattedTextLine::HorizontalRule
1590 | FormattedTextLine::Embedded(_)
1591 | FormattedTextLine::Image(_) => LineType::LineBreak,
1592 }
1593 }
1594}
1595 
1596impl Element for FormattedTextElement {
1597 fn layout(
1598 &mut self,
1599 constraint: SizeConstraint,
1600 ctx: &mut LayoutContext,
1601 app: &AppContext,
1602 ) -> Vector2F {
1603 self.laid_out_text = vec![];
1604 let max_width = constraint.max_along(Axis::Horizontal);
1605 let max_height = constraint.max_along(Axis::Vertical);
1606 
1607 // Frame width should at least be the minimum constraint width.
1608 let mut frame_width = constraint.min.x();
1609 let mut frame_height = 0.;
1610 let mut should_expand_to_max_width = false;
1611 
1612 // Render text line-by-line.
1613 let mut lines = self.formatted_text.lines.iter().enumerate().peekable();
1614 let mut last_line_was_list_item = false;
1615 let mut list_numbering = ListNumbering::new();
1616 
1617 // We don't use the frame_index from lines.next() because this creates inconsistencies in the frame index when we have lists
1618 let mut frame_index = 0;
1619 while let Some((line_index, line)) = lines.next() {
1620 let mut res = Vec::new();
1621 let (font_size, texts, indent, line_type) = match line {
1622 FormattedTextLine::Heading(header) => (
1623 self.heading_to_font_size_multipliers
1624 .get_multiplier(header.heading_size.into())
1625 * self.font_size,
1626 &header.text,
1627 0,
1628 LineType::FormattedLine,
1629 ),
1630 FormattedTextLine::Line(texts) => {
1631 (self.font_size, texts, 0, LineType::FormattedLine)
1632 }
1633 // TODO: Update when we support task lists.
1634 FormattedTextLine::TaskList(list) => {
1635 (self.font_size, &list.text, 0, LineType::FormattedLine)
1636 }
1637 // Increment indent_level by 1 since even if no indent is given, still need to format list to the right.
1638 FormattedTextLine::OrderedList(texts) => (
1639 self.font_size,
1640 &texts.indented_text.text,
1641 texts.indented_text.indent_level + 1,
1642 LineType::OrderedList,
1643 ),
1644 FormattedTextLine::UnorderedList(texts) => (
1645 self.font_size,
1646 &texts.text,
1647 texts.indent_level + 1,
1648 LineType::UnorderedList,
1649 ),
1650 FormattedTextLine::CodeBlock(texts) => {
1651 // If there are any code block lines, this element should expand to max width.
1652 // This is because we want the code block background to take up the full width even
1653 // if the text itself is very short.
1654 should_expand_to_max_width = true;
1655 // Add a line of padding before and after the code body.
1656 let new_line = "\n".to_owned();
1657 let formatted = new_line.clone() + &texts.code + &new_line;
1658 
1659 // TODO: In the future, can use the first parameter (the lang field) to modify the actual style of the text fragment.
1660 res.push(FormattedTextFragment::plain_text(formatted));
1661 
1662 (self.font_size, &res, 1, LineType::CodeBlock)
1663 }
1664 FormattedTextLine::Table(table) => {
1665 res.push(FormattedTextFragment::plain_text(table.to_plain_text()));
1666 (self.font_size, &res, 0, LineType::FormattedLine)
1667 }
1668 FormattedTextLine::LineBreak
1669 | FormattedTextLine::HorizontalRule
1670 | FormattedTextLine::Embedded(_)
1671 | FormattedTextLine::Image(_) => (self.font_size, &res, 0, LineType::LineBreak),
1672 };
1673 
1674 // Appends either the number or bullet type in the case of list based on the indent.
1675 let mut text = match line {
1676 FormattedTextLine::OrderedList(texts) => {
1677 format!(
1678 "{}. ",
1679 list_numbering
1680 // Subtracting by 1 as `indent` of lists starts at 1,
1681 // but list_numbering expects it to start at 0.
1682 .advance(indent.saturating_sub(1), Some(texts.number))
1683 .display_label
1684 )
1685 }
1686 
1687 FormattedTextLine::UnorderedList(_) => {
1688 let bullet = match indent {
1689 1 => FULL_BULLET,
1690 2 => EMPTY_BULLET,
1691 _ => SQUARE_BULLET,
1692 };
1693 // Align with the ordered list - assuming the ordered list indices are only single digits.
1694 format!("{bullet:<3}")
1695 }
1696 _ => String::new(),
1697 };
1698 // Offset to account for the bullet or number.
1699 let glyph_offset = text.chars().count();
1700 let byte_offset = text.len();
1701 
1702 // Reset ordered list numbering if this item isn't an ordered list.
1703 if !matches!(line, FormattedTextLine::OrderedList(_)) {
1704 list_numbering.reset();
1705 }
1706 
1707 // Keep a vec of running styles and the range of indices they are decorating.
1708 let mut styles = vec![];
1709 
1710 // If there is a prefix (e.g. bullet points, numbers, etc), accounts for the style of it which will be default.
1711 if !text.is_empty() {
1712 let style = match line {
1713 // Round bullets are a bit small comparing to the square bullet, so we bold them to increase their size.
1714 FormattedTextLine::UnorderedList(_) if indent == 1 || indent == 2 => {
1715 Properties::default().weight(Weight::Bold)
1716 }
1717 _ => Properties::default(),
1718 };
1719 styles.push((
1720 0..glyph_offset,
1721 StyleAndFont::new(self.family_id, style, TextStyle::new()),
1722 ));
1723 }
1724 
1725 // Scope to contain a borrow of the mouse handlers.
1726 {
1727 // Preserves formatting for the innner text in case of list.
1728 let mut prev_index = glyph_offset;
1729 
1730 let borrowed_handler = self
1731 .text_frame_mouse_handlers
1732 .get(line_index)
1733 .map(|handler| handler.borrow());
1734 let mut link_styles_iter = if let Some(borrowed_handler) = &borrowed_handler {
1735 borrowed_handler.styles.iter()
1736 } else {
1737 [].iter()
1738 }
1739 .peekable();
1740 
1741 let mut current_link_style: Option<HighlightedRange> = None;
1742 
1743 for inline in texts {
1744 let fragment_char_count = inline.text.chars().count();
1745 let mut character_count = 0;
1746 
1747 while character_count < fragment_char_count {
1748 let mut style = Properties::default();
1749 let mut text_style = TextStyle::default();
1750 
1751 if let Some(style) = link_styles_iter.peek() {
1752 if style.highlight_indices[0] + glyph_offset
1753 == prev_index + character_count
1754 {
1755 current_link_style = Some((*style).clone());
1756 link_styles_iter.next();
1757 }
1758 }
1759 
1760 let start_char_index = prev_index + character_count;
1761 let style_char_len;
1762 
1763 if let Some(link_style) = &current_link_style {
1764 text_style = link_style.highlight.text_style;
1765 let end_char_index = *link_style.highlight_indices.last().unwrap_or(&0)
1766 + 1
1767 + glyph_offset;
1768 if end_char_index - start_char_index
1769 <= fragment_char_count - character_count
1770 {
1771 // If the length of the currently registered link style is less than the remaining length of the current fragment,
1772 // then the style should be applied to the link only.
1773 style_char_len = end_char_index - start_char_index;
1774 // Reset the current link style since it has been fully applied.
1775 current_link_style = None;
1776 } else {
1777 // If the length of the currently registered link style is greater than the remaining length of the current fragment,
1778 // then the style should be applied to the entire fragment.
1779 style_char_len = fragment_char_count - character_count;
1780 // We keep the current_link_style, as it might overflow to the next fragment.
1781 }
1782 } else if let Some(style) = link_styles_iter.peek() {
1783 // If we are not currently working with a link style, then the length of the current style should be the minimum
1784 // of the remaining length of the fragment or the remaining length until the next link style within the fragment.
1785 style_char_len = (style.highlight_indices[0] + glyph_offset
1786 - start_char_index)
1787 .min(fragment_char_count - character_count);
1788 } else {
1789 // If there's no more link styles, then the length of the current style should be the remaining length of the fragment.
1790 style_char_len = fragment_char_count - character_count;
1791 }
1792 
1793 if inline.styles.weight.is_some() {
1794 style.weight = Weight::from_custom_weight(inline.styles.weight);
1795 }
1796 if inline.styles.italic {
1797 style.style = Style::Italic;
1798 }
1799 if inline.styles.strikethrough {
1800 text_style = text_style.with_show_strikethrough(true);
1801 }
1802 if inline.styles.underline {
1803 let underline_color =
1804 text_style.foreground_color.unwrap_or(self.text_color);
1805 text_style = text_style.with_underline_color(underline_color)
1806 }
1807 if inline.styles.inline_code {
1808 // If we have existing background and foreground highlighting from, for example,
1809 // a link or a search, we don't want to override it.
1810 if let Some(font_color) = self.inline_code_font_color {
1811 if text_style.foreground_color.is_none() {
1812 text_style.foreground_color = Some(font_color);
1813 }
1814 }
1815 if let Some(bg_color) = self.inline_code_bg_color {
1816 if text_style.background_color.is_none() {
1817 text_style.background_color = Some(bg_color);
1818 }
1819 }
1820 }
1821 
1822 let font_family_id = if matches!(line, FormattedTextLine::CodeBlock(_))
1823 || inline.styles.inline_code
1824 {
1825 self.code_block_family_id
1826 } else {
1827 self.family_id
1828 };
1829 
1830 styles.push((
1831 start_char_index..start_char_index + style_char_len,
1832 StyleAndFont::new(font_family_id, style, text_style),
1833 ));
1834 
1835 character_count += style_char_len;
1836 }
1837 prev_index += character_count;
1838 text.push_str(&inline.text);
1839 // TODO: ensure the constructed text is the same as `line.raw_text()` (test?)
1840 }
1841 }
1842 
1843 // Since list items are flattened into multiple lines rather than a single list segment,
1844 // this ends up causing line breaks between each individual list item. To avoid this,
1845 // we only show the line break if we already started a list and there isn't a following list item.
1846 let curr_line_is_line_break = matches!(line_type, LineType::LineBreak);
1847 let next_line_is_list_item = matches!(
1848 lines.peek(),
1849 Some((_, FormattedTextLine::UnorderedList(_)))
1850 | Some((_, FormattedTextLine::OrderedList(_)))
1851 );
1852 if last_line_was_list_item && curr_line_is_line_break && next_line_is_list_item {
1853 continue;
1854 }
1855 
1856 if let Some(handler) = self.text_frame_mouse_handlers.get(line_index) {
1857 let mut handler = handler.borrow_mut();
1858 if glyph_offset > 0 {
1859 handler.add_offset(glyph_offset.into(), byte_offset.into());
1860 }
1861 apply_secret_replacements(
1862 &mut text,
1863 glyph_offset,
1864 handler.secret_replacement.as_slice(),
1865 );
1866 }
1867 
1868 // Indent should only be considered with left text alignment. This matches the behavior
1869 // of other text editors (Google Docs).
1870 let should_layout_with_indent = line_type != LineType::FormattedLine
1871 && matches!(self.alignment, TextAlignment::Left);
1872 
1873 let text_frame_width = if self.disable_text_wrapping && self.clip_config.is_none() {
1874 // Use a very large width to prevent text wrapping.
1875 f32::MAX
1876 } else if should_layout_with_indent {
1877 (max_width - font_size * indent as f32).max(0.)
1878 } else {
1879 max_width
1880 };
1881 
1882 // If clip_config is set we layout a line, otherwise we use layout_text which uses a TextFrame for soft-wrapping.
1883 let text_frame = if let Some(clip_config) = self.clip_config {
1884 let line = ctx.text_layout_cache.layout_line(
1885 &text,
1886 LineStyle {
1887 font_size,
1888 line_height_ratio: self.line_height_ratio,
1889 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
1890 fixed_width_tab_size: matches!(line, FormattedTextLine::CodeBlock(_))
1891 .then_some(4),
1892 },
1893 styles.as_slice(),
1894 text_frame_width,
1895 clip_config,
1896 &app.font_cache().text_layout_system(),
1897 );
1898 
1899 Arc::new(TextFrame::new(
1900 vec1![(*line).clone()],
1901 text_frame_width,
1902 self.alignment,
1903 ))
1904 } else {
1905 ctx.text_layout_cache.layout_text(
1906 &text,
1907 LineStyle {
1908 font_size,
1909 line_height_ratio: self.line_height_ratio,
1910 baseline_ratio: DEFAULT_TOP_BOTTOM_RATIO,
1911 fixed_width_tab_size: matches!(line, FormattedTextLine::CodeBlock(_))
1912 .then_some(4),
1913 },
1914 styles.as_slice(),
1915 text_frame_width,
1916 max_height,
1917 self.alignment,
1918 None,
1919 &app.font_cache().text_layout_system(),
1920 )
1921 };
1922 
1923 // Adjust frame width if there is an indent.
1924 frame_width = frame_width.max(if should_layout_with_indent {
1925 text_frame.max_width() + font_size * indent as f32
1926 } else {
1927 text_frame.max_width()
1928 });
1929 
1930 // Give certain lines additional breathing room underneath. This padding is only added
1931 // if there's more formatted text below, and omitted for the last line of text.
1932 let bottom_padding = if let Some((_, next_line)) = lines.peek() {
1933 match line {
1934 FormattedTextLine::Heading(_) => match next_line {
1935 // If the heading is followed by a line break, don't add any
1936 // additional padding (as the line break will take care of it).
1937 FormattedTextLine::LineBreak => 0.,
1938 _ => LINE_BREAK_HEIGHT,
1939 },
1940 FormattedTextLine::OrderedList(_) | FormattedTextLine::UnorderedList(_)
1941 if !next_line_is_list_item =>
1942 {
1943 LINE_BREAK_HEIGHT
1944 }
1945 FormattedTextLine::CodeBlock(_) => LINE_BREAK_HEIGHT,
1946 _ => 0.,
1947 }
1948 } else {
1949 0.
1950 };
1951 
1952 // The saved_glyph_positions vector should be relatively small, so the performance should be acceptable.
1953 self.saved_glyph_positions
1954 .iter_mut()
1955 .for_each(|saved_glyph_position| {
1956 if let SavedGlyphPosition::FormattedTextLinePosition(pos) =
1957 saved_glyph_position.position
1958 {
1959 if pos.frame_index != line_index {
1960 return;
1961 }
1962 
1963 let mut row_index = 0;
1964 let mut glyph_accum = 0;
1965 for row in text_frame.lines() {
1966 if row.end_index() > (pos.glyph_index + glyph_offset) - glyph_accum {
1967 break;
1968 }
1969 row_index += 1;
1970 glyph_accum += row.end_index();
1971 }
1972 
1973 saved_glyph_position.position =
1974 SavedGlyphPosition::LaidOutTextFramePosition(
1975 FormattedTextSelectionLocation {
1976 frame_index,
1977 row_index,
1978 glyph_index: pos.glyph_index + glyph_offset,
1979 },
1980 );
1981 }
1982 });
1983 
1984 let laid_out_frame = match line_type {
1985 LineType::FormattedLine => LaidOutTextFrame::Text {
1986 text_frame,
1987 frame_bounds: RectF::default(),
1988 bottom_padding,
1989 raw_text: text,
1990 mouse_handlers: self
1991 .text_frame_mouse_handlers
1992 .get(line_index)
1993 .cloned()
1994 .unwrap_or(Rc::new(RefCell::new(FrameMouseHandlers::default()))),
1995 },
1996 LineType::CodeBlock => LaidOutTextFrame::CodeBlock {
1997 text_frame,
1998 frame_bounds: RectF::default(),
1999 bottom_padding,
2000 raw_text: text,
2001 },
2002 LineType::OrderedList | LineType::UnorderedList => LaidOutTextFrame::Indented {
2003 text_frame,
2004 indent,
2005 frame_bounds: RectF::default(),
2006 top_padding: FRAME_SPACER_HEIGHT,
2007 bottom_padding,
2008 left_padding: font_size * indent as f32,
2009 raw_text: text,
2010 mouse_handlers: self
2011 .text_frame_mouse_handlers
2012 .get(line_index)
2013 .cloned()
2014 .unwrap_or(Rc::new(RefCell::new(FrameMouseHandlers::default()))),
2015 },
2016 LineType::LineBreak => LaidOutTextFrame::LineBreak {
2017 frame_bounds: RectF::default(),
2018 },
2019 };
2020 
2021 frame_height += laid_out_frame.calculate_frame_height();
2022 self.laid_out_text.push(laid_out_frame);
2023 
2024 last_line_was_list_item =
2025 matches!(line_type, LineType::OrderedList | LineType::UnorderedList);
2026 
2027 frame_index += 1;
2028 }
2029 
2030 let mut size = vec2f(frame_width, frame_height);
2031 if should_expand_to_max_width {
2032 if max_width.is_infinite() {
2033 panic!("The max width was infinite when stretch set to true");
2034 }
2035 size.set_x(max_width);
2036 }
2037 self.size = Some(size);
2038 size
2039 }
2040 
2041 fn after_layout(&mut self, _: &mut AfterLayoutContext, _: &AppContext) {}
2042 
2043 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, _: &AppContext) {
2044 self.origin = Some(Point::from_vec2f(origin, ctx.scene.z_index()));
2045 let mut mut_origin = origin;
2046 let size = self.size().expect("Expected size to not be none");
2047 
2048 for saved_glyph_position in self.saved_glyph_positions.iter() {
2049 let SavedGlyphPosition::LaidOutTextFramePosition(pos) = saved_glyph_position.position
2050 else {
2051 continue;
2052 };
2053 let Some(laid_out_frame) = self.laid_out_text.get(pos.frame_index) else {
2054 continue;
2055 };
2056 
2057 let frame = match laid_out_frame {
2058 LaidOutTextFrame::Text { text_frame, .. }
2059 | LaidOutTextFrame::Indented { text_frame, .. }
2060 | LaidOutTextFrame::CodeBlock { text_frame, .. } => text_frame,
2061 _ => continue,
2062 };
2063 let Some(line) = frame.lines().get(pos.row_index) else {
2064 continue;
2065 };
2066 
2067 let Some(glyph_width) = line.width_for_index(pos.glyph_index) else {
2068 continue;
2069 };
2070 
2071 let x_offset = if let LaidOutTextFrame::Indented { left_padding, .. } = laid_out_frame {
2072 *left_padding
2073 } else {
2074 0.
2075 } + line.x_for_index(pos.glyph_index);
2076 
2077 let y_offset = self
2078 .laid_out_text
2079 .iter()
2080 .take(pos.frame_index)
2081 .map(|frame| frame.calculate_frame_height())
2082 .sum::<f32>()
2083 + frame
2084 .lines()
2085 .iter()
2086 .take(pos.row_index)
2087 .map(|line| line.height())
2088 .sum::<f32>();
2089 
2090 ctx.position_cache.cache_position_indefinitely(
2091 saved_glyph_position.position_id.clone(),
2092 RectF::new(
2093 origin + vec2f(x_offset, y_offset),
2094 vec2f(glyph_width, line.height()),
2095 ),
2096 );
2097 }
2098 
2099 // Add indent by moving origin point of the frame.
2100 for laid_out_frame in &mut self.laid_out_text {
2101 // Get the x-offset for this laid out frame.
2102 let x_offset = laid_out_frame.calculate_x_offset(
2103 self.font_size,
2104 mut_origin.x(),
2105 self.alignment,
2106 size.x(),
2107 );
2108 let frame_height = laid_out_frame.calculate_frame_height();
2109 
2110 let (frame, frame_height, bounds) = match laid_out_frame {
2111 LaidOutTextFrame::Text {
2112 text_frame,
2113 frame_bounds,
2114 bottom_padding,
2115 ..
2116 } => {
2117 let curr_origin = vec2f(x_offset, mut_origin.y());
2118 *frame_bounds = RectF::new(
2119 curr_origin,
2120 Vector2F::new(size.x(), frame_height - *bottom_padding),
2121 );
2122 (Some(text_frame), frame_height, *frame_bounds)
2123 }
2124 LaidOutTextFrame::Indented {
2125 text_frame,
2126 frame_bounds,
2127 top_padding,
2128 bottom_padding,
2129 ..
2130 } => {
2131 let curr_origin = vec2f(x_offset, mut_origin.y() + *top_padding);
2132 *frame_bounds = RectF::new(
2133 curr_origin,
2134 Vector2F::new(size.x(), frame_height - *top_padding - *bottom_padding),
2135 );
2136 (Some(text_frame), frame_height, *frame_bounds)
2137 }
2138 LaidOutTextFrame::CodeBlock {
2139 text_frame,
2140 frame_bounds,
2141 bottom_padding,
2142 ..
2143 } => {
2144 let curr_origin = vec2f(x_offset, mut_origin.y());
2145 
2146 #[cfg(debug_assertions)]
2147 ctx.scene
2148 .set_location_for_panic_logging(self.constructor_location);
2149 
2150 // Draw code block rectangle.
2151 ctx.scene
2152 .draw_rect_with_hit_recording(RectF::new(
2153 curr_origin,
2154 vec2f(size.x(), frame_height - *bottom_padding),
2155 ))
2156 .with_background(Fill::Solid(ColorU::from_u32(CODE_BLOCK_BACKGROUND)))
2157 .with_corner_radius(CornerRadius::with_all(Radius::Pixels(10.)));
2158 
2159 *frame_bounds = RectF::new(
2160 curr_origin,
2161 Vector2F::new(size.x(), frame_height - *bottom_padding),
2162 );
2163 (Some(text_frame), frame_height, *frame_bounds)
2164 }
2165 LaidOutTextFrame::LineBreak { frame_bounds } => {
2166 let curr_origin = vec2f(x_offset, mut_origin.y());
2167 *frame_bounds = RectF::new(curr_origin, Vector2F::new(size.x(), frame_height));
2168 (None, LINE_BREAK_HEIGHT, *frame_bounds)
2169 }
2170 };
2171 
2172 if let Some(frame) = frame {
2173 frame.paint(
2174 bounds,
2175 &Default::default(),
2176 self.text_color,
2177 ctx.scene,
2178 ctx.font_cache,
2179 );
2180 }
2181 mut_origin += vec2f(0., frame_height);
2182 }
2183 
2184 // Draw selection if there is one
2185 if let Some(point_ranges) = self.calculate_point_ranges(ctx.current_selection) {
2186 for (start, end) in point_ranges {
2187 self.draw_selection(start, end, ctx);
2188 }
2189 }
2190 }
2191 
2192 fn size(&self) -> Option<Vector2F> {
2193 self.size
2194 }
2195 
2196 fn dispatch_event(
2197 &mut self,
2198 event: &DispatchedEvent,
2199 ctx: &mut EventContext,
2200 app: &AppContext,
2201 ) -> bool {
2202 let Some(z_index) = self.z_index() else {
2203 return false;
2204 };
2205 match event.at_z_index(z_index, ctx) {
2206 Some(Event::MouseMoved { position, .. }) => {
2207 let link_pos_before = self
2208 .hyperlink_support
2209 .highlighted_hyperlink
2210 .lock()
2211 .expect("Failed to acquire lock on highlighted_hyperlink")
2212 .clone();
2213 let result = self.handle_mouse_moved(*position, z_index, ctx, app);
2214 let link_pos_after = self
2215 .hyperlink_support
2216 .highlighted_hyperlink
2217 .lock()
2218 .expect("Failed to acquire lock on highlighted_hyperlink")
2219 .clone();
2220 
2221 if link_pos_before != link_pos_after {
2222 ctx.notify();
2223 }
2224 result
2225 }
2226 Some(Event::LeftMouseDown {
2227 position,
2228 modifiers,
2229 ..
2230 }) => {
2231 self.handle_mouse_down(*position, z_index, modifiers, ctx, app);
2232 false
2233 }
2234 _ => false,
2235 }
2236 
2237 // false
2238 }
2239 
2240 fn origin(&self) -> Option<Point> {
2241 self.origin
2242 }
2243 
2244 fn as_selectable_element(&self) -> Option<&dyn SelectableElement> {
2245 if self.is_selectable {
2246 Some(self)
2247 } else {
2248 None
2249 }
2250 }
2251}
2252 
2253impl SelectableElement for FormattedTextElement {
2254 fn get_selection(
2255 &self,
2256 selection_start: Vector2F,
2257 selection_end: Vector2F,
2258 is_rect: IsRect,
2259 ) -> Option<Vec<SelectionFragment>> {
2260 let mut start_bound =
2261 self.position_for_point(selection_start, SnappingPolicy::default().snap_on_gap())?;
2262 let mut end_bound =
2263 self.position_for_point(selection_end, SnappingPolicy::default().snap_on_gap())?;
2264 
2265 // If the start and end are at the same position, selection is empty.
2266 if start_bound.frame_index == end_bound.frame_index
2267 && start_bound.glyph_index == end_bound.glyph_index
2268 {
2269 return None;
2270 }
2271 
2272 // Ensure start comes before end
2273 let (selection_start, selection_end) = if start_bound.frame_index > end_bound.frame_index
2274 || (start_bound.frame_index == end_bound.frame_index
2275 && start_bound.glyph_index > end_bound.glyph_index)
2276 {
2277 std::mem::swap(&mut start_bound, &mut end_bound);
2278 (selection_end, selection_start)
2279 } else {
2280 (selection_start, selection_end)
2281 };
2282 
2283 let text = match is_rect {
2284 IsRect::True => {
2285 let selection_bounds = self.compute_rect_selection_bounds(
2286 start_bound,
2287 end_bound,
2288 selection_start.x(),
2289 selection_end.x(),
2290 )?;
2291 selection_bounds
2292 .into_iter()
2293 .filter_map(|(start_bound, end_bound)| {
2294 let frame = self.laid_out_text.get(start_bound.frame_index)?;
2295 char_slice(
2296 frame.get_raw_text(),
2297 start_bound.glyph_index,
2298 end_bound.glyph_index,
2299 )
2300 })
2301 .join("\n")
2302 }
2303 IsRect::False => self.build_regular_selection_text(start_bound, end_bound)?,
2304 };
2305 
2306 Some(vec![SelectionFragment {
2307 text,
2308 origin: self.origin?,
2309 }])
2310 }
2311 
2312 fn expand_selection(
2313 &self,
2314 absolute_point: Vector2F,
2315 direction: SelectionDirection,
2316 unit: SelectionType,
2317 word_boundaries_policy: &WordBoundariesPolicy,
2318 ) -> Option<Vector2F> {
2319 if matches!(unit, SelectionType::Simple | SelectionType::Rect) {
2320 return None;
2321 }
2322 
2323 let bound = self.bounds()?;
2324 
2325 // Handle points above/below bounds
2326 if absolute_point.y() < bound.min_y() {
2327 return match direction {
2328 SelectionDirection::Backward => Some(bound.origin()),
2329 SelectionDirection::Forward => None,
2330 };
2331 }
2332 if absolute_point.y() > bound.max_y() {
2333 return match direction {
2334 SelectionDirection::Backward => None,
2335 SelectionDirection::Forward => Some(absolute_point),
2336 };
2337 }
2338 
2339 match unit {
2340 SelectionType::Simple | SelectionType::Rect => None,
2341 SelectionType::Semantic => {
2342 let text_selection_bound = self
2343 .position_for_point(absolute_point, SnappingPolicy::default().snap_on_gap())?;
2344 let frame = self.laid_out_text.get(text_selection_bound.frame_index)?;
2345 
2346 match frame {
2347 LaidOutTextFrame::Text {
2348 text_frame,
2349 raw_text,
2350 ..
2351 }
2352 | LaidOutTextFrame::CodeBlock {
2353 text_frame,
2354 raw_text,
2355 ..
2356 }
2357 | LaidOutTextFrame::Indented {
2358 text_frame,
2359 raw_text,
2360 ..
2361 } => {
2362 let frame_bounds = frame.get_frame_bounds();
2363 let inner_point = if matches!(direction, SelectionDirection::Backward) {
2364 raw_text.word_starts_backward_from_offset_exclusive(CharOffset::from(
2365 text_selection_bound.glyph_index,
2366 ))
2367 } else {
2368 raw_text.word_ends_from_offset_exclusive(CharOffset::from(
2369 text_selection_bound.glyph_index,
2370 ))
2371 }
2372 .ok()?
2373 .with_policy(word_boundaries_policy)
2374 .next()?;
2375 
2376 let offset = raw_text.to_offset(inner_point).ok()?.as_usize();
2377 
2378 let line = text_frame.lines().get(text_selection_bound.row_index)?;
2379 let first_glyph = line.first_glyph()?;
2380 let last_glyph = line.last_glyph()?;
2381 let relative_x =
2382 if first_glyph.index <= offset && offset <= last_glyph.index + 1 {
2383 line.x_for_index(offset)
2384 } else if matches!(direction, SelectionDirection::Backward) {
2385 0.
2386 } else {
2387 line.width
2388 };
2389 let absolute_x =
2390 frame_bounds.min_x() + text_frame.line_x_offset(line) + relative_x;
2391 
2392 Some(vec2f(absolute_x, absolute_point.y()))
2393 }
2394 LaidOutTextFrame::LineBreak { .. } => None,
2395 }
2396 }
2397 SelectionType::Lines => {
2398 let text_selection_bound =
2399 self.position_for_point(absolute_point, SnappingPolicy::default())?;
2400 let snap_to = self.translate_selection_bound_to_line_bound(
2401 text_selection_bound,
2402 matches!(direction, SelectionDirection::Backward),
2403 )?;
2404 Some(snap_to)
2405 }
2406 }
2407 }
2408 
2409 fn is_point_semantically_before(
2410 &self,
2411 absolute_point_1: Vector2F,
2412 absolute_point_2: Vector2F,
2413 ) -> Option<bool> {
2414 let bounds = self.bounds()?;
2415 match (
2416 bounds.contains_point(absolute_point_1),
2417 bounds.contains_point(absolute_point_2),
2418 ) {
2419 (false, false) => None,
2420 (false, true) => {
2421 if absolute_point_1.y() < bounds.min_y() {
2422 Some(true)
2423 } else if absolute_point_1.y() > bounds.max_y() {
2424 Some(false)
2425 } else {
2426 Some(absolute_point_1.x() < bounds.min_x())
2427 }
2428 }
2429 (true, false) => {
2430 if absolute_point_2.y() < bounds.min_y() {
2431 Some(false)
2432 } else if absolute_point_2.y() > bounds.max_y() {
2433 Some(true)
2434 } else {
2435 Some(absolute_point_2.x() > bounds.max_x())
2436 }
2437 }
2438 (true, true) => {
2439 let point_1 = self.position_for_point(
2440 absolute_point_1,
2441 SnappingPolicy::default().snap_on_gap(),
2442 )?;
2443 let point_2 = self.position_for_point(
2444 absolute_point_2,
2445 SnappingPolicy::default().snap_on_gap(),
2446 )?;
2447 Some(
2448 (
2449 point_1.frame_index,
2450 point_1.row_index,
2451 point_1.glyph_index,
2452 absolute_point_1.x(),
2453 ) < (
2454 point_2.frame_index,
2455 point_2.row_index,
2456 point_2.glyph_index,
2457 absolute_point_2.x(),
2458 ),
2459 )
2460 }
2461 }
2462 }
2463 
2464 fn smart_select(
2465 &self,
2466 absolute_point: Vector2F,
2467 smart_select_fn: SmartSelectFn,
2468 ) -> Option<(Vector2F, Vector2F)> {
2469 let bound = self.bounds()?;
2470 if absolute_point.y() < bound.min_y() || absolute_point.y() > bound.max_y() {
2471 return None;
2472 }
2473 if absolute_point.x() < bound.min_x() || absolute_point.x() > bound.max_x() {
2474 return None;
2475 }
2476 
2477 let text_selection_bound =
2478 self.position_for_point(absolute_point, SnappingPolicy::default())?;
2479 let frame = self.laid_out_text.get(text_selection_bound.frame_index)?;
2480 
2481 match frame {
2482 LaidOutTextFrame::Text {
2483 text_frame,
2484 raw_text,
2485 ..
2486 }
2487 | LaidOutTextFrame::CodeBlock {
2488 text_frame,
2489 raw_text,
2490 ..
2491 }
2492 | LaidOutTextFrame::Indented {
2493 text_frame,
2494 raw_text,
2495 ..
2496 } => {
2497 // Get row that we initially clicked on.
2498 // We need to do this because the same char offset in the raw text buffer
2499 // could be either the end of one line or the start of the next line.
2500 let line = text_frame.lines().get(text_selection_bound.row_index)?;
2501 let first_glyph = line.first_glyph()?;
2502 let last_glyph = line.last_glyph()?;
2503 // If we clicked to the right of a line, the text_selection_bound's glyph index would be one larger than the last glyph's index.
2504 // Snap it within the line's index range so we do smart selection as if we clicked on the last glyph in the line.
2505 let char_offset = text_selection_bound
2506 .glyph_index
2507 .clamp(first_glyph.index, last_glyph.index);
2508 let byte_offset = raw_text.char_indices().nth(char_offset)?.0.into();
2509 
2510 let smart_select_range = smart_select_fn(raw_text, byte_offset)?;
2511 // convert the byte offset to glyph index
2512 let smart_select_start =
2513 count_chars_up_to_byte(raw_text, smart_select_range.start)?.as_usize();
2514 let smart_select_end =
2515 count_chars_up_to_byte(raw_text, smart_select_range.end)?.as_usize();
2516 // After smart selection, the start and end offsets can be on different lines if a word wrapped.
2517 // Find the lines for the start and end offsets.
2518 let start_row = text_frame.row_within_frame(smart_select_start, false);
2519 let end_row = text_frame.row_within_frame(smart_select_end, true);
2520 let start_line = text_frame.lines().get(start_row)?;
2521 let end_line = text_frame.lines().get(end_row)?;
2522 
2523 let start_relative_x = start_line.x_for_index(smart_select_start);
2524 let end_relative_x = end_line.x_for_index(smart_select_end);
2525 // We subtract half the line's height to put the position in the center of the line.
2526 let start_relative_y =
2527 text_frame.height_up_to_row(start_row) - start_line.height() / 2.;
2528 let end_relative_y = text_frame.height_up_to_row(end_row) - end_line.height() / 2.;
2529 let frame_bounds = frame.get_frame_bounds();
2530 
2531 Some((
2532 vec2f(
2533 frame_bounds.min_x()
2534 + text_frame.line_x_offset(start_line)
2535 + start_relative_x,
2536 frame_bounds.min_y() + start_relative_y,
2537 ),
2538 vec2f(
2539 frame_bounds.min_x() + text_frame.line_x_offset(end_line) + end_relative_x,
2540 frame_bounds.min_y() + end_relative_y,
2541 ),
2542 ))
2543 }
2544 LaidOutTextFrame::LineBreak { .. } => None,
2545 }
2546 }
2547 
2548 fn calculate_clickable_bounds(&self, current_selection: Option<Selection>) -> Vec<RectF> {
2549 if self.origin.is_none() {
2550 return vec![];
2551 }
2552 
2553 self.calculate_point_ranges(current_selection)
2554 .unwrap_or_default()
2555 .iter()
2556 .flat_map(|(start_point, end_point)| {
2557 self.calculate_selection_bounds(*start_point, *end_point)
2558 })
2559 .collect()
2560 }
2561}
2562 
2563/// A policy that determines how the snapping should behave when converting a point position
2564/// to a position of a glyph.
2565#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2566struct SnappingPolicy {
2567 /// If true, the snapping will snap to the beginning of the next frame if the point is in
2568 /// the gap between two frames; otherwise, it will return `None`.
2569 should_snap_on_gap: bool,
2570 /// If true, the snapping will snap to the beginning or the end of a frame if the point is
2571 /// outside the frame bounds; otherwise, it will return `None`.
2572 should_snap_to_ends: bool,
2573 /// If true, the returned result will not exceed the last char index. The last caret index
2574 /// will simply be reduced to the last char index.
2575 should_adjust_to_char_indices: bool,
2576}
2577 
2578impl SnappingPolicy {
2579 fn snap_on_gap(mut self) -> Self {
2580 self.should_snap_on_gap = true;
2581 self
2582 }
2583 
2584 fn precise_char_range() -> Self {
2585 Self {
2586 should_snap_on_gap: false,
2587 should_snap_to_ends: false,
2588 should_adjust_to_char_indices: true,
2589 }
2590 }
2591}
2592 
2593impl Default for SnappingPolicy {
2594 fn default() -> Self {
2595 Self {
2596 should_snap_on_gap: false,
2597 should_snap_to_ends: true,
2598 should_adjust_to_char_indices: false,
2599 }
2600 }
2601}
2602 
2603#[cfg(test)]
2604#[path = "formatted_text_element_tests.rs"]
2605mod tests;
2606