pub mod parser;mod path_range; use std::collections::HashSet;use std::iter;use std::mem;use std::ops::Range;use std::path::Path;use std::rc::Rc;use std::sync::Arc;use std::time::Duration; use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, point, quad,};use language::{Language, LanguageRegistry, Rope};use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};use pulldown_cmark::Alignment;use sum_tree::TreeMap;use theme::SyntaxTheme;use ui::{Tooltip, prelude::*};use util::{ResultExt, TryFutureExt}; use crate::parser::CodeBlockKind; /// A callback function that can be used to customize the style of links based on the destination URL./// If the callback returns `None`, the default link style will be used.type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>; #[derive(Clone)]pub struct MarkdownStyle { pub base_text_style: TextStyle, pub code_block: StyleRefinement, pub code_block_overflow_x_scroll: bool, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub link_callback: Option<LinkStyleCallback>, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, pub heading: StyleRefinement, pub table_overflow_x_scroll: bool,} impl Default for MarkdownStyle { fn default() -> Self { Self { base_text_style: Default::default(), code_block: Default::default(), code_block_overflow_x_scroll: false, inline_code: Default::default(), block_quote: Default::default(), link: Default::default(), link_callback: None, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: Arc::new(SyntaxTheme::default()), selection_background_color: Default::default(), heading: Default::default(), table_overflow_x_scroll: false, } }} pub struct Markdown { source: SharedString, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, parsed_markdown: ParsedMarkdown, should_reparse: bool, pending_parse: Option<Task<Option<()>>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<String>, options: Options, copied_code_blocks: HashSet<ElementId>,} struct Options { parse_links_only: bool,} pub enum CodeBlockRenderer { Default { copy_button: bool }, Custom { render: CodeBlockRenderFn },} pub type CodeBlockRenderFn = Arc<dyn Fn(usize, &CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>; actions!(markdown, [Copy, CopyAsMarkdown]); impl Markdown { pub fn new( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<String>, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, fallback_code_block_language, options: Options { parse_links_only: false, }, copied_code_blocks: HashSet::new(), }; this.parse(cx); this } pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry: None, fallback_code_block_language: None, options: Options { parse_links_only: true, }, copied_code_blocks: HashSet::new(), }; this.parse(cx); this } pub fn source(&self) -> &str { &self.source } pub fn append(&mut self, text: &str, cx: &mut Context<Self>) { self.source = SharedString::new(self.source.to_string() + text); self.parse(cx); } pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) { if source == self.source() { return; } self.source = source; self.selection = Selection::default(); self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; self.parsed_markdown = ParsedMarkdown::default(); self.parse(cx); } pub fn parsed_markdown(&self) -> &ParsedMarkdown { &self.parsed_markdown } fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) { if self.selection.end <= self.selection.start { return; } let text = text.text_for_range(self.selection.start..self.selection.end); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) { if self.selection.end <= self.selection.start { return; } let text = self.source[self.selection.start..self.selection.end].to_string(); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn parse(&mut self, cx: &mut Context<Self>) { if self.source.is_empty() { return; } if self.pending_parse.is_some() { self.should_reparse = true; return; } let source = self.source.clone(); let parse_text_only = self.options.parse_links_only; let language_registry = self.language_registry.clone(); let fallback = self.fallback_code_block_language.clone(); let parsed = cx.background_spawn(async move { if parse_text_only { return anyhow::Ok(ParsedMarkdown { events: Arc::from(parse_links_only(source.as_ref())), source, languages_by_name: TreeMap::default(), languages_by_path: TreeMap::default(), }); } let (events, language_names, paths) = parse_markdown(&source); let mut languages_by_name = TreeMap::default(); let mut languages_by_path = TreeMap::default(); if let Some(registry) = language_registry.as_ref() { for name in language_names { let language = if !name.is_empty() { registry.language_for_name_or_extension(&name) } else if let Some(fallback) = &fallback { registry.language_for_name_or_extension(fallback) } else { continue; }; if let Ok(language) = language.await { languages_by_name.insert(name, language); } } for path in paths { if let Ok(language) = registry.language_for_file_path(&path).await { languages_by_path.insert(path, language); } } } anyhow::Ok(ParsedMarkdown { source, events: Arc::from(events), languages_by_name, languages_by_path, }) }); self.should_reparse = false; self.pending_parse = Some(cx.spawn(async move |this, cx| { async move { let parsed = parsed.await?; this.update(cx, |this, cx| { this.parsed_markdown = parsed; this.pending_parse.take(); if this.should_reparse { this.parse(cx); } cx.notify(); }) .ok(); anyhow::Ok(()) } .log_err() .await })); }} impl Focusable for Markdown { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() }} #[derive(Copy, Clone, Default, Debug)]struct Selection { start: usize, end: usize, reversed: bool, pending: bool,} impl Selection { fn set_head(&mut self, head: usize) { if head < self.tail() { if !self.reversed { self.end = self.start; self.reversed = true; } self.start = head; } else { if self.reversed { self.start = self.end; self.reversed = false; } self.end = head; } } fn tail(&self) -> usize { if self.reversed { self.end } else { self.start } }} #[derive(Clone, Default)]pub struct ParsedMarkdown { pub source: SharedString, pub events: Arc<[(Range<usize>, MarkdownEvent)]>, pub languages_by_name: TreeMap<SharedString, Arc<Language>>, pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>,} impl ParsedMarkdown { pub fn source(&self) -> &SharedString { &self.source } pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> { &self.events }} pub struct MarkdownElement { markdown: Entity<Markdown>, style: MarkdownStyle, code_block_renderer: CodeBlockRenderer, on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>,} impl MarkdownElement { pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self { Self { markdown, style, code_block_renderer: CodeBlockRenderer::Default { copy_button: true }, on_url_click: None, } } pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self { self.code_block_renderer = variant; self } pub fn on_url_click( mut self, handler: impl Fn(SharedString, &mut Window, &mut App) + 'static, ) -> Self { self.on_url_click = Some(Box::new(handler)); self } fn paint_selection( &self, bounds: Bounds<Pixels>, rendered_text: &RenderedText, window: &mut Window, cx: &mut App, ) { let selection = self.markdown.read(cx).selection; let selection_start = rendered_text.position_for_source_index(selection.start); let selection_end = rendered_text.position_for_source_index(selection.end); if let Some(((start_position, start_line_height), (end_position, end_line_height))) = selection_start.zip(selection_end) { if start_position.y == end_position.y { window.paint_quad(quad( Bounds::from_corners( start_position, point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); } else { window.paint_quad(quad( Bounds::from_corners( start_position, point(bounds.right(), start_position.y + start_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); if end_position.y > start_position.y + start_line_height { window.paint_quad(quad( Bounds::from_corners( point(bounds.left(), start_position.y + start_line_height), point(bounds.right(), end_position.y), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); } window.paint_quad(quad( Bounds::from_corners( point(bounds.left(), end_position.y), point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); } } } fn paint_mouse_listeners( &mut self, hitbox: &Hitbox, rendered_text: &RenderedText, window: &mut Window, cx: &mut App, ) { let is_hovering_link = hitbox.is_hovered(window) && !self.markdown.read(cx).selection.pending && rendered_text .link_for_position(window.mouse_position()) .is_some(); if is_hovering_link { window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox)); } else { window.set_cursor_style(CursorStyle::IBeam, Some(hitbox)); } let on_open_url = self.on_url_click.take(); self.on_mouse_event(window, cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); move |markdown, event: &MouseDownEvent, phase, window, cx| { if hitbox.is_hovered(window) { if phase.bubble() { if let Some(link) = rendered_text.link_for_position(event.position) { markdown.pressed_link = Some(link.clone()); } else { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; let range = if event.click_count == 2 { rendered_text.surrounding_word_range(source_index) } else if event.click_count == 3 { rendered_text.surrounding_line_range(source_index) } else { source_index..source_index }; markdown.selection = Selection { start: range.start, end: range.end, reversed: false, pending: true, }; window.focus(&markdown.focus_handle); window.prevent_default(); } cx.notify(); } } else if phase.capture() { markdown.selection = Selection::default(); markdown.pressed_link = None; cx.notify(); } } }); self.on_mouse_event(window, cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); let was_hovering_link = is_hovering_link; move |markdown, event: &MouseMoveEvent, phase, window, cx| { if phase.capture() { return; } if markdown.selection.pending { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; markdown.selection.set_head(source_index); markdown.autoscroll_request = Some(source_index); cx.notify(); } else { let is_hovering_link = hitbox.is_hovered(window) && rendered_text.link_for_position(event.position).is_some(); if is_hovering_link != was_hovering_link { cx.notify();Lines 1–500 of 1535. Load more
pub mod parser; mod path_range; use std::collections::HashSet; use std::iter; use std::mem; use std::ops::Range; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, point, quad, }; use language::{Language, LanguageRegistry, Rope}; use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown}; use pulldown_cmark::Alignment; use sum_tree::TreeMap; use theme::SyntaxTheme; use ui::{Tooltip, prelude::*}; use util::{ResultExt, TryFutureExt}; use crate::parser::CodeBlockKind; /// A callback function that can be used to customize the style of links based on the destination URL. /// If the callback returns `None`, the default link style will be used. type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>; #[derive(Clone)] pub struct MarkdownStyle { pub base_text_style: TextStyle, pub code_block: StyleRefinement, pub code_block_overflow_x_scroll: bool, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub link_callback: Option<LinkStyleCallback>, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, pub heading: StyleRefinement, pub table_overflow_x_scroll: bool, } impl Default for MarkdownStyle { fn default() -> Self { Self { base_text_style: Default::default(), code_block: Default::default(), code_block_overflow_x_scroll: false, inline_code: Default::default(), block_quote: Default::default(), link: Default::default(), link_callback: None, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: Arc::new(SyntaxTheme::default()), selection_background_color: Default::default(), heading: Default::default(), table_overflow_x_scroll: false, } } } pub struct Markdown { source: SharedString, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, parsed_markdown: ParsedMarkdown, should_reparse: bool, pending_parse: Option<Task<Option<()>>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<String>, options: Options, copied_code_blocks: HashSet<ElementId>, } struct Options { parse_links_only: bool, } pub enum CodeBlockRenderer { Default { copy_button: bool }, Custom { render: CodeBlockRenderFn }, } pub type CodeBlockRenderFn = Arc<dyn Fn(usize, &CodeBlockKind, &ParsedMarkdown, Range<usize>, &mut Window, &App) -> Div>; actions!(markdown, [Copy, CopyAsMarkdown]); impl Markdown { pub fn new( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<String>, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, fallback_code_block_language, options: Options { parse_links_only: false, }, copied_code_blocks: HashSet::new(), }; this.parse(cx); this } pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry: None, fallback_code_block_language: None, options: Options { parse_links_only: true, }, copied_code_blocks: HashSet::new(), }; this.parse(cx); this } pub fn source(&self) -> &str { &self.source } pub fn append(&mut self, text: &str, cx: &mut Context<Self>) { self.source = SharedString::new(self.source.to_string() + text); self.parse(cx); } pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) { if source == self.source() { return; } self.source = source; self.selection = Selection::default(); self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; self.parsed_markdown = ParsedMarkdown::default(); self.parse(cx); } pub fn parsed_markdown(&self) -> &ParsedMarkdown { &self.parsed_markdown } fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) { if self.selection.end <= self.selection.start { return; } let text = text.text_for_range(self.selection.start..self.selection.end); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn copy_as_markdown(&self, _: &mut Window, cx: &mut Context<Self>) { if self.selection.end <= self.selection.start { return; } let text = self.source[self.selection.start..self.selection.end].to_string(); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn parse(&mut self, cx: &mut Context<Self>) { if self.source.is_empty() { return; } if self.pending_parse.is_some() { self.should_reparse = true; return; } let source = self.source.clone(); let parse_text_only = self.options.parse_links_only; let language_registry = self.language_registry.clone(); let fallback = self.fallback_code_block_language.clone(); let parsed = cx.background_spawn(async move { if parse_text_only { return anyhow::Ok(ParsedMarkdown { events: Arc::from(parse_links_only(source.as_ref())), source, languages_by_name: TreeMap::default(), languages_by_path: TreeMap::default(), }); } let (events, language_names, paths) = parse_markdown(&source); let mut languages_by_name = TreeMap::default(); let mut languages_by_path = TreeMap::default(); if let Some(registry) = language_registry.as_ref() { for name in language_names { let language = if !name.is_empty() { registry.language_for_name_or_extension(&name) } else if let Some(fallback) = &fallback { registry.language_for_name_or_extension(fallback) } else { continue; }; if let Ok(language) = language.await { languages_by_name.insert(name, language); } } for path in paths { if let Ok(language) = registry.language_for_file_path(&path).await { languages_by_path.insert(path, language); } } } anyhow::Ok(ParsedMarkdown { source, events: Arc::from(events), languages_by_name, languages_by_path, }) }); self.should_reparse = false; self.pending_parse = Some(cx.spawn(async move |this, cx| { async move { let parsed = parsed.await?; this.update(cx, |this, cx| { this.parsed_markdown = parsed; this.pending_parse.take(); if this.should_reparse { this.parse(cx); } cx.notify(); }) .ok(); anyhow::Ok(()) } .log_err() .await })); } } impl Focusable for Markdown { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } #[derive(Copy, Clone, Default, Debug)] struct Selection { start: usize, end: usize, reversed: bool, pending: bool, } impl Selection { fn set_head(&mut self, head: usize) { if head < self.tail() { if !self.reversed { self.end = self.start; self.reversed = true; } self.start = head; } else { if self.reversed { self.start = self.end; self.reversed = false; } self.end = head; } } fn tail(&self) -> usize { if self.reversed { self.end } else { self.start } } } #[derive(Clone, Default)] pub struct ParsedMarkdown { pub source: SharedString, pub events: Arc<[(Range<usize>, MarkdownEvent)]>, pub languages_by_name: TreeMap<SharedString, Arc<Language>>, pub languages_by_path: TreeMap<Arc<Path>, Arc<Language>>, } impl ParsedMarkdown { pub fn source(&self) -> &SharedString { &self.source } pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> { &self.events } } pub struct MarkdownElement { markdown: Entity<Markdown>, style: MarkdownStyle, code_block_renderer: CodeBlockRenderer, on_url_click: Option<Box<dyn Fn(SharedString, &mut Window, &mut App)>>, } impl MarkdownElement { pub fn new(markdown: Entity<Markdown>, style: MarkdownStyle) -> Self { Self { markdown, style, code_block_renderer: CodeBlockRenderer::Default { copy_button: true }, on_url_click: None, } } pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self { self.code_block_renderer = variant; self } pub fn on_url_click( mut self, handler: impl Fn(SharedString, &mut Window, &mut App) + 'static, ) -> Self { self.on_url_click = Some(Box::new(handler)); self } fn paint_selection( &self, bounds: Bounds<Pixels>, rendered_text: &RenderedText, window: &mut Window, cx: &mut App, ) { let selection = self.markdown.read(cx).selection; let selection_start = rendered_text.position_for_source_index(selection.start); let selection_end = rendered_text.position_for_source_index(selection.end); if let Some(((start_position, start_line_height), (end_position, end_line_height))) = selection_start.zip(selection_end) { if start_position.y == end_position.y { window.paint_quad(quad( Bounds::from_corners( start_position, point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); } else { window.paint_quad(quad( Bounds::from_corners( start_position, point(bounds.right(), start_position.y + start_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); if end_position.y > start_position.y + start_line_height { window.paint_quad(quad( Bounds::from_corners( point(bounds.left(), start_position.y + start_line_height), point(bounds.right(), end_position.y), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); } window.paint_quad(quad( Bounds::from_corners( point(bounds.left(), end_position.y), point(end_position.x, end_position.y + end_line_height), ), Pixels::ZERO, self.style.selection_background_color, Edges::default(), Hsla::transparent_black(), BorderStyle::default(), )); } } } fn paint_mouse_listeners( &mut self, hitbox: &Hitbox, rendered_text: &RenderedText, window: &mut Window, cx: &mut App, ) { let is_hovering_link = hitbox.is_hovered(window) && !self.markdown.read(cx).selection.pending && rendered_text .link_for_position(window.mouse_position()) .is_some(); if is_hovering_link { window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox)); } else { window.set_cursor_style(CursorStyle::IBeam, Some(hitbox)); } let on_open_url = self.on_url_click.take(); self.on_mouse_event(window, cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); move |markdown, event: &MouseDownEvent, phase, window, cx| { if hitbox.is_hovered(window) { if phase.bubble() { if let Some(link) = rendered_text.link_for_position(event.position) { markdown.pressed_link = Some(link.clone()); } else { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; let range = if event.click_count == 2 { rendered_text.surrounding_word_range(source_index) } else if event.click_count == 3 { rendered_text.surrounding_line_range(source_index) } else { source_index..source_index }; markdown.selection = Selection { start: range.start, end: range.end, reversed: false, pending: true, }; window.focus(&markdown.focus_handle); window.prevent_default(); } cx.notify(); } } else if phase.capture() { markdown.selection = Selection::default(); markdown.pressed_link = None; cx.notify(); } } }); self.on_mouse_event(window, cx, { let rendered_text = rendered_text.clone(); let hitbox = hitbox.clone(); let was_hovering_link = is_hovering_link; move |markdown, event: &MouseMoveEvent, phase, window, cx| { if phase.capture() { return; } if markdown.selection.pending { let source_index = match rendered_text.source_index_for_position(event.position) { Ok(ix) | Err(ix) => ix, }; markdown.selection.set_head(source_index); markdown.autoscroll_request = Some(source_index); cx.notify(); } else { let is_hovering_link = hitbox.is_hovered(window) && rendered_text.link_for_position(event.position).is_some(); if is_hovering_link != was_hovering_link { cx.notify();