fluidattacks-designs 0.12.0

Fluid Attacks design system, GPUI variant: desktop widgets over the shared tokens
//! `Markdown` — gpui-component's `TextView`, dressed in the design system.
//!
//! `TextView` parses and draws; this element hands it our type scale and the web
//! code snippet's box and copy button, and turns off the one thing it would do
//! on its own that an app has not asked for: opening a link.

use crate::theme::ActiveTheme as _;
use crate::Button;
use fluidattacks_designs_domain::button::Variant;
use fluidattacks_designs_domain::code_snippet::{
    COPY_ICON, COPY_SIZE, ICON_COLOR, LINE_HEIGHT, PADDING, RADIUS, SURFACE,
};
use fluidattacks_designs_domain::icon::Size as IconSize;
use fluidattacks_designs_domain::markdown::{heading_size, PARAGRAPH_GAP_REM};
use fluidattacks_designs_domain::theme::Mode;
use fluidattacks_designs_domain::typography::{TextSize, FONT_PRIMARY};
use gpui::{
    div, px, rems, rgb, rgb_to_hsla, App, ClipboardItem, ElementId, HighlightStyle, IntoElement,
    ParentElement as _, RenderOnce, SharedString, StyleRefinement, Styled as _, Window,
};
use gpui_component::highlighter::HighlightTheme;
use gpui_component::text::{TextView, TextViewStyle};

#[derive(IntoElement)]
pub struct Markdown {
    id: SharedString,
    source: SharedString,
}

impl Markdown {
    pub fn new(id: impl Into<SharedString>, source: impl Into<SharedString>) -> Self {
        Self {
            id: id.into(),
            source: source.into(),
        }
    }
}

impl RenderOnce for Markdown {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = cx.theme();
        let dark = theme.mode == Mode::Dark;
        let surface = theme.shade(SURFACE);
        let icon = theme.shade(ICON_COLOR);
        let ink = theme.colors.text;
        let body = TextSize::default();
        let view = TextView::markdown(ElementId::Name(self.id), self.source)
            .style(style(dark, surface))
            .selectable(true)
            .on_link_click(|_, _, _, _| {})
            .code_block_actions(move |block, _, _| {
                let code = block.code();
                Button::new("copy", "")
                    .variant(Variant::Ghost)
                    .icon(COPY_ICON)
                    .icon_color(icon)
                    .icon_size(IconSize::Lg)
                    .width(COPY_SIZE)
                    .on_click(move |_, _, app| {
                        app.write_to_clipboard(ClipboardItem::new_string(code.to_string()));
                    })
            });
        div()
            .min_w_0()
            .font_family(FONT_PRIMARY)
            .text_size(px(body.px()))
            .line_height(px(body.line_height_px()))
            .text_color(rgb(ink))
            .child(view)
    }
}

fn style(dark: bool, surface: u32) -> TextViewStyle {
    let highlight_theme = if dark {
        HighlightTheme::default_dark()
    } else {
        HighlightTheme::default_light()
    };
    let code_block = StyleRefinement::default()
        .p(px(PADDING))
        .rounded(px(RADIUS))
        .line_height(px(LINE_HEIGHT));
    TextViewStyle {
        is_dark: dark,
        highlight_theme,
        ..TextViewStyle::default()
    }
    .paragraph_gap(rems(PARAGRAPH_GAP_REM))
    .heading_font_size(|depth, _| px(heading_size(depth).px()))
    .code_block(code_block)
    .inline_code(HighlightStyle {
        background_color: Some(rgb_to_hsla(rgb(surface))),
        ..HighlightStyle::default()
    })
}