woocraft 0.4.5

GPUI components lib for Woocraft design system.
Documentation
use std::ops::Range;

use gpui::{
  App, HighlightStyle, IntoElement, ParentElement, RenderOnce, SharedString, StyleRefinement,
  Styled, StyledText, Window, div, prelude::FluentBuilder as _,
};

use crate::{ActiveTheme, StyledExt};

const MASKED: &str = "";

#[derive(Clone)]
pub enum HighlightsMatch {
  Prefix(SharedString),
  Full(SharedString),
}

impl HighlightsMatch {
  pub fn as_str(&self) -> &str {
    match self {
      Self::Prefix(s) => s.as_str(),
      Self::Full(s) => s.as_str(),
    }
  }

  pub fn is_prefix(&self) -> bool {
    matches!(self, Self::Prefix(_))
  }
}

impl From<&str> for HighlightsMatch {
  fn from(value: &str) -> Self {
    Self::Full(value.to_string().into())
  }
}

impl From<String> for HighlightsMatch {
  fn from(value: String) -> Self {
    Self::Full(value.into())
  }
}

impl From<SharedString> for HighlightsMatch {
  fn from(value: SharedString) -> Self {
    Self::Full(value)
  }
}

#[derive(IntoElement)]
pub struct Label {
  style: StyleRefinement,
  label: SharedString,
  secondary: Option<SharedString>,
  masked: bool,
  highlights_text: Option<HighlightsMatch>,
}

impl Label {
  pub fn new(label: impl Into<SharedString>) -> Self {
    Self {
      style: StyleRefinement::default(),
      label: label.into(),
      secondary: None,
      masked: false,
      highlights_text: None,
    }
  }

  pub fn secondary(mut self, secondary: impl Into<SharedString>) -> Self {
    self.secondary = Some(secondary.into());
    self
  }

  pub fn masked(mut self, masked: bool) -> Self {
    self.masked = masked;
    self
  }

  pub fn highlights(mut self, text: impl Into<HighlightsMatch>) -> Self {
    self.highlights_text = Some(text.into());
    self
  }

  fn full_text(&self) -> SharedString {
    match &self.secondary {
      Some(secondary) => format!("{} {}", self.label, secondary).into(),
      None => self.label.clone(),
    }
  }

  fn highlight_ranges(&self) -> Vec<Range<usize>> {
    let full_text = self.full_text();
    let mut ranges = Vec::new();

    if self.secondary.is_some() {
      ranges.push(0..self.label.len());
      ranges.push(self.label.len()..full_text.len());
    }

    if let Some(matched) = &self.highlights_text {
      let matched_str = matched.as_str();
      if !matched_str.is_empty() {
        let search_lower = matched_str.to_lowercase();
        let full_text_lower = full_text.to_lowercase();

        if matched.is_prefix() {
          if full_text_lower.starts_with(&search_lower) {
            ranges.push(0..matched_str.len());
          }
        } else {
          let mut search_start = 0;
          while let Some(pos) = full_text_lower[search_start..].find(&search_lower) {
            let match_start = search_start + pos;
            let match_end = match_start + matched_str.len();
            if match_end <= full_text.len() {
              ranges.push(match_start..match_end);
            }

            search_start = match_start + 1;
            while search_start < full_text.len() && !full_text.is_char_boundary(search_start) {
              search_start += 1;
            }
            if search_start >= full_text.len() {
              break;
            }
          }
        }
      }
    }

    ranges
  }

  fn measure_highlights(&self, cx: &mut App) -> Option<Vec<(Range<usize>, HighlightStyle)>> {
    let ranges = self.highlight_ranges();
    if ranges.is_empty() {
      return None;
    }

    let mut highlights = Vec::new();
    let mut added = 0;

    if self.secondary.is_some() {
      highlights.push((ranges[0].clone(), HighlightStyle::default()));
      highlights.push((
        ranges[1].clone(),
        HighlightStyle {
          color: Some(cx.theme().muted_foreground),
          ..Default::default()
        },
      ));
      added = 2;
    }

    for range in ranges.iter().skip(added) {
      highlights.push((
        range.clone(),
        HighlightStyle {
          color: Some(cx.theme().primary),
          ..Default::default()
        },
      ));
    }

    Some(gpui::combine_highlights(vec![], highlights).collect())
  }
}

impl_styled!(Label);

impl RenderOnce for Label {
  fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
    let mut text = self.full_text();

    if self.masked {
      text = MASKED.repeat(text.chars().count()).into();
    }

    div()
      .line_height(gpui::relative(1.25))
      .text_color(cx.theme().foreground)
      .refine_style(&self.style)
      .child(
        StyledText::new(&text).when_some(self.measure_highlights(cx), |this, hl| {
          this.with_highlights(hl)
        }),
      )
  }
}