guise-ui 1.5.3

A component library for gpui, Zed's GPU-accelerated UI framework: a themed palette, sizing tokens, 130+ composable components, a reactive state layer, and an in-app Safari-style inspector.
Documentation
//! `Markdown` — read-only markdown, rendered.
//!
//! [`MarkdownEditor`](super::MarkdownEditor) exists to *edit* markdown, so it
//! carries a caret, a scroll model, and hand-rolled glyph layout. Displaying
//! markdown needs none of that, and asking for an editor to show a paragraph
//! is the wrong shape. This walks the same three pure passes the editor does —
//! [`block::classify`], then [`layout::plan`] with reveal off — and hands each
//! line to gpui's `StyledText`, which wraps it for us.
//!
//! It is what an assistant's reply is drawn with, and it is a plain
//! `RenderOnce` builder, so it can appear anywhere text can.
//!
//! ```ignore
//! div().child(Markdown::new("# Notes\n\n- **bold** and `code`"))
//! ```

use gpui::prelude::*;
use gpui::{
  div, px, App, Font, FontStyle, FontWeight, IntoElement, SharedString, StrikethroughStyle,
  StyledText, TextRun, UnderlineStyle, Window,
};

use super::block::{classify, DocState};
use super::layout::{metrics, plan, RowKind, RowPlan};
use crate::devtools::Probed;
use crate::style::MONO_FAMILY;
use crate::theme::{theme, ColorName, Size};

/// Read-only markdown. Create with [`Markdown::new`] and drop it anywhere.
#[derive(IntoElement)]
pub struct Markdown {
  source: SharedString,
  size: Size,
  /// Colors links with the theme's primary and underlines them.
  accent: Option<ColorName>,
  /// Cap on how much of the source is rendered, in lines. Streaming replies
  /// can get long, and the caller may want a preview.
  max_lines: Option<usize>,
}

impl Markdown {
  pub fn new(source: impl Into<SharedString>) -> Self {
    Markdown {
      source: source.into(),
      size: Size::Sm,
      accent: None,
      max_lines: None,
    }
  }

  /// Base text size; headings and code scale from it.
  pub fn size(mut self, size: Size) -> Self {
    self.size = size;
    self
  }

  /// Draw links in this palette color rather than the theme's primary.
  pub fn accent(mut self, accent: ColorName) -> Self {
    self.accent = Some(accent);
    self
  }

  /// Render at most `lines` source lines.
  pub fn max_lines(mut self, lines: usize) -> Self {
    self.max_lines = Some(lines);
    self
  }
}

impl RenderOnce for Markdown {
  fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
    let t = theme(cx);
    // Start from the font actually in effect, so bold and italic are that
    // family's own faces. Building a `Font` from scratch with an empty
    // family name resolves to nothing and loses the weight with it.
    let prose = window.text_style().font();
    let base = t.font_size(self.size);
    let text_color = t.text().hsla();
    let dimmed = t.dimmed().hsla();
    let border = t.border().hsla();
    let accent = self
      .accent
      .map_or_else(|| t.primary(), |name| t.color(name, 6))
      .hsla();
    let code_bg = t.surface_hover().alpha(0.7);
    let mut highlight_bg = t.color(ColorName::Yellow, 3).hsla();
    highlight_bg.a = 0.45;

    // Cloned once instead of per run: `family` is a `SharedString` and
    // `..prose.clone()` would bump its refcount for every styled span on
    // every line, every frame.
    let prose_family = prose.family.clone();
    let mono_family: SharedString = MONO_FAMILY.into();
    let colors = RowColors {
      text: text_color,
      dimmed,
      border,
      code_bg,
    };

    let mut state = DocState::default();
    let mut column = div().flex().flex_col().w_full();
    let lines = self
      .source
      .lines()
      .take(self.max_lines.unwrap_or(usize::MAX));

    for line in lines {
      let block = classify(line, &mut state);
      // `reveal: false` is the whole difference from the editor: markers
      // stay hidden because there is no cursor that might want to edit
      // them. The fence's language is passed as `None` because nothing
      // here highlights code — carrying it would allocate a `String` per
      // line of every fenced block, every frame, to be thrown away.
      let row = plan(line, &block, None, false);
      let row_metrics = metrics(&row.kind);
      let size = base * row_metrics.scale;
      let weight = if matches!(row.kind, RowKind::Heading(_)) {
        FontWeight::BOLD
      } else {
        prose.weight
      };

      let runs: Vec<TextRun> = row
        .runs
        .iter()
        .filter(|run| run.len > 0)
        .map(|run| {
          let s = run.style;
          let mono = s.code || matches!(row.kind, RowKind::Code { .. });
          TextRun {
            len: run.len,
            font: Font {
              family: if mono {
                mono_family.clone()
              } else {
                prose_family.clone()
              },
              weight: if s.bold { FontWeight::BOLD } else { weight },
              style: if s.italic {
                FontStyle::Italic
              } else {
                prose.style
              },
              ..prose.clone()
            },
            color: if run.marker || run.dim {
              dimmed
            } else if s.link {
              accent
            } else {
              text_color
            },
            background_color: if s.highlight {
              Some(highlight_bg)
            } else if s.code && !mono_block(&row.kind) {
              Some(code_bg)
            } else {
              None
            },
            underline: (s.link && !run.marker).then(|| UnderlineStyle {
              thickness: px(1.0),
              color: Some(accent),
              wavy: false,
            }),
            strikethrough: s.strike.then(|| StrikethroughStyle {
              thickness: px(1.0),
              color: Some(dimmed),
            }),
          }
        })
        .collect();

      column = column.child(row_element(row, runs, size, base, &colors));
    }
    column.probe("Markdown")
  }
}

/// The colors a row draws with, resolved once per render. Passed as one value
/// because four bare `Hsla` parameters in a row are silently transposable.
struct RowColors {
  text: gpui::Hsla,
  dimmed: gpui::Hsla,
  border: gpui::Hsla,
  code_bg: gpui::Hsla,
}

/// The chrome around one line: bullets, checkboxes, quote bars, code
/// backgrounds, and the rule.
fn row_element(
  row: RowPlan,
  runs: Vec<TextRun>,
  size: f32,
  base: f32,
  colors: &RowColors,
) -> gpui::Div {
  let m = metrics(&row.kind);
  // `visible` is moved out of the plan rather than cloned: the plan is this
  // frame's and nothing else reads it.
  let text = StyledText::new(SharedString::from(row.visible)).with_runs(runs);

  let mut line = div()
    .flex()
    .flex_row()
    .items_start()
    .w_full()
    .text_size(px(size))
    .pt(px(base * m.pad_top))
    .pb(px(base * m.pad_bottom));

  match &row.kind {
    // A blank line is vertical space, not an empty text box that would
    // collapse to nothing.
    RowKind::Blank => return div().h(px(base * 0.6)),
    RowKind::Rule => {
      return div()
        .py(px(base * 0.5))
        .child(div().w_full().h(px(1.0)).bg(colors.border))
    }
    // The fence markers themselves are syntax, so they leave no row.
    RowKind::Fence { .. } | RowKind::FrontMatter => return div(),
    RowKind::Code { .. } => {
      return div()
        .w_full()
        .px(px(base * 0.6))
        .bg(colors.code_bg)
        .text_size(px(size))
        .child(text);
    }
    RowKind::Quote { depth } => {
      for _ in 0..(*depth).max(1) {
        line = line.child(
          div()
            .flex_none()
            .w(px(2.0))
            .h(px(base * 1.4))
            .mr(px(base * 0.6))
            .bg(colors.border),
        );
      }
    }
    RowKind::Bullet { cols } => {
      line = line
        .pl(px(*cols as f32 * base * 0.5))
        .child(marker(base, colors.dimmed, "\u{2022}"));
    }
    RowKind::Ordered { cols, number } => {
      line = line.pl(px(*cols as f32 * base * 0.5)).child(marker(
        base,
        colors.dimmed,
        format!("{number}."),
      ));
    }
    RowKind::Task { cols, checked } => {
      let glyph = if *checked { "\u{2611}" } else { "\u{2610}" };
      line = line.pl(px(*cols as f32 * base * 0.5)).child(marker(
        base,
        if *checked { colors.dimmed } else { colors.text },
        glyph,
      ));
    }
    RowKind::Heading(_) | RowKind::Paragraph | RowKind::Table => {}
  }

  line.child(div().flex_1().min_w(px(0.0)).child(text))
}

/// The bullet, number, or checkbox in a list row's gutter.
fn marker(base: f32, color: gpui::Hsla, glyph: impl Into<SharedString>) -> gpui::Div {
  div()
    .flex_none()
    .min_w(px(base * 1.1))
    .mr(px(base * 0.4))
    .text_color(color)
    .child(glyph.into())
}

/// Whether the whole row is already monospaced, so an inline-code run should
/// not paint its own background on top.
fn mono_block(kind: &RowKind) -> bool {
  matches!(kind, RowKind::Code { .. } | RowKind::Fence { .. })
}