Skip to main content

native/guppy_nif/src/bridge_view/render_text.rs

use super::{
    events,
    identity::NodeIdentity,
    render_pass::RenderPass,
    style::{apply_div_style, style_ops_to_highlight_style},
};
use crate::ir::{DivStyle, TextRunSegment};
use gpui::{
    AnyElement, HighlightStyle, InteractiveElement, InteractiveText, IntoElement, ParentElement,
    SharedString, StyledText, div,
};
use std::ops::Range;

pub(crate) fn render(
    pass: &mut RenderPass<'_>,
    path: &str,
    id: Option<&str>,
    content: &str,
    runs: &[TextRunSegment],
    style: &DivStyle,
    click: Option<&str>,
) -> AnyElement {
    render_with_view_id(pass.view_id(), path, id, content, runs, style, click)
}

pub(crate) fn render_with_view_id(
    view_id: u64,
    path: &str,
    id: Option<&str>,
    content: &str,
    runs: &[TextRunSegment],
    style: &DivStyle,
    click: Option<&str>,
) -> AnyElement {
    let node_id = NodeIdentity::new(view_id, path, id);
    let interactive_text =
        InteractiveText::new(node_id.to_shared_string(), styled_text(content, runs));

    let element = match click {
        Some(callback_id) if !content.is_empty() => {
            let callback_id = callback_id.to_owned();
            let click_node_id = node_id.to_string();
            let clickable_ranges = std::iter::once(0..content.len()).collect::<Vec<_>>();

            interactive_text
                .on_click(clickable_ranges, move |_, _, _| {
                    events::emit_click(view_id, &click_node_id, &callback_id);
                })
                .into_any_element()
        }
        _ => interactive_text.into_any_element(),
    };

    if style.is_empty() {
        element
    } else {
        apply_div_style(
            div().id(SharedString::from(format!("{}::text_style", node_id))),
            style,
        )
        .child(element)
        .into_any_element()
    }
}

pub(crate) fn styled_text(content: &str, runs: &[TextRunSegment]) -> StyledText {
    let styled = StyledText::new(content.to_owned());

    if runs.is_empty() {
        styled
    } else {
        styled.with_highlights(rich_text_highlights(runs))
    }
}

pub(crate) fn rich_text_highlights(runs: &[TextRunSegment]) -> Vec<(Range<usize>, HighlightStyle)> {
    let mut offset = 0;
    runs.iter()
        .map(|run| {
            let start = offset;
            offset += run.text.len();
            (start..offset, style_ops_to_highlight_style(&run.style))
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::rich_text_highlights;
    use crate::ir::{ColorToken, StyleOp, TextRunSegment};

    #[test]
    fn rich_text_highlights_preserve_utf8_byte_ranges() {
        let runs = vec![
            TextRunSegment {
                text: "Hi ".into(),
                style: vec![StyleOp::FontBold].into(),
            },
            TextRunSegment {
                text: "é".into(),
                style: vec![StyleOp::TextColor(ColorToken::Yellow)].into(),
            },
        ];

        let highlights = rich_text_highlights(&runs);

        assert_eq!(highlights[0].0, 0..3);
        assert_eq!(highlights[1].0, 3..5);
        assert!(highlights[0].1.font_weight.is_some());
        assert!(highlights[1].1.color.is_some());
    }
}