woocraft 0.4.5

GPUI components lib for Woocraft design system.
Documentation
use std::rc::Rc;

use gpui::{
  AlignItems, AnyElement, AnyView, App, Axis, Div, Element, ElementId, InteractiveElement as _,
  IntoElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window, div,
  prelude::FluentBuilder as _, px,
};

use crate::{ActiveTheme as _, AxisExt, Size, StyledExt, h_flex, v_flex};

type FieldElementBuilder = Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>;

#[derive(Clone, Copy)]
pub(super) struct FieldProps {
  pub(super) size: Size,
  pub(super) layout: Axis,
  pub(super) columns: usize,
  pub(super) label_width: Option<Pixels>,
  pub(super) label_text_size: Option<Rems>,
}

impl Default for FieldProps {
  fn default() -> Self {
    Self {
      size: Size::default(),
      layout: Axis::Vertical,
      columns: 1,
      label_width: Some(px(140.0)),
      label_text_size: None,
    }
  }
}

pub enum FieldBuilder {
  String(SharedString),
  Element(FieldElementBuilder),
  View(AnyView),
}

impl Default for FieldBuilder {
  fn default() -> Self {
    Self::String(SharedString::default())
  }
}

impl From<AnyView> for FieldBuilder {
  fn from(view: AnyView) -> Self {
    Self::View(view)
  }
}

impl From<&'static str> for FieldBuilder {
  fn from(value: &'static str) -> Self {
    Self::String(value.into())
  }
}

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

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

impl RenderOnce for FieldBuilder {
  fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
    match self {
      FieldBuilder::String(value) => value.into_any_element(),
      FieldBuilder::Element(builder) => builder(window, cx),
      FieldBuilder::View(view) => view.into_any(),
    }
  }
}

#[derive(IntoElement)]
pub struct Field {
  id: ElementId,
  props: FieldProps,
  label: Option<FieldBuilder>,
  label_indent: bool,
  description: Option<FieldBuilder>,
  children: Vec<AnyElement>,
  visible: bool,
  required: bool,
  align_items: Option<AlignItems>,
  col_span: u16,
  col_start: Option<i16>,
  col_end: Option<i16>,
}

impl Default for Field {
  fn default() -> Self {
    Self::new()
  }
}

impl Field {
  pub fn new() -> Self {
    Self {
      id: 0.into(),
      props: FieldProps::default(),
      label: None,
      label_indent: true,
      description: None,
      children: Vec::new(),
      visible: true,
      required: false,
      align_items: None,
      col_span: 1,
      col_start: None,
      col_end: None,
    }
  }

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

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

  pub fn label_fn<F, E>(mut self, label: F) -> Self
  where
    E: IntoElement,
    F: Fn(&mut Window, &mut App) -> E + 'static, {
    self.label = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
      label(window, cx).into_any_element()
    })));
    self
  }

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

  pub fn description_fn<F, E>(mut self, description: F) -> Self
  where
    E: IntoElement,
    F: Fn(&mut Window, &mut App) -> E + 'static, {
    self.description = Some(FieldBuilder::Element(Rc::new(move |window, cx| {
      description(window, cx).into_any_element()
    })));
    self
  }

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

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

  pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self {
    self.id = ix.into();
    self.props = props;
    self
  }

  pub fn items_start(mut self) -> Self {
    self.align_items = Some(AlignItems::Start);
    self
  }

  pub fn items_end(mut self) -> Self {
    self.align_items = Some(AlignItems::End);
    self
  }

  pub fn items_center(mut self) -> Self {
    self.align_items = Some(AlignItems::Center);
    self
  }

  pub fn col_span(mut self, col_span: u16) -> Self {
    self.col_span = col_span.max(1);
    self
  }

  pub fn col_start(mut self, col_start: i16) -> Self {
    self.col_start = Some(col_start);
    self
  }

  pub fn col_end(mut self, col_end: i16) -> Self {
    self.col_end = Some(col_end);
    self
  }
}

impl ParentElement for Field {
  fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
    self.children.extend(elements);
  }
}

impl RenderOnce for Field {
  fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
    if !self.visible {
      return div().into_any_element();
    }

    let layout = self.props.layout;
    let label_width = if layout.is_vertical() {
      None
    } else {
      self.props.label_width
    };
    let has_label = self.label.is_some();
    let reserve_label_space = layout.is_horizontal() && (has_label || self.label_indent);

    #[inline]
    fn wrap_div(layout: Axis) -> Div {
      if layout.is_vertical() {
        v_flex()
      } else {
        h_flex()
      }
    }

    #[inline]
    fn wrap_label(label_width: Option<Pixels>) -> Div {
      div().when_some(label_width, |this, width| this.w(width).flex_shrink_0())
    }

    let outer_gap = match self.props.size {
      Size::Small => px(6.0),
      Size::Medium => px(8.0),
      Size::Large => px(12.0),
    };
    let inner_gap = if layout.is_horizontal() {
      outer_gap / 2.0
    } else {
      outer_gap / 4.0
    };
    let default_label_text_size = self.props.size.smaller().text_size();

    v_flex()
      .flex_1()
      .gap(inner_gap)
      .col_span(self.col_span)
      .when_some(self.col_start, |this, start| this.col_start(start))
      .when_some(self.col_end, |this, end| this.col_end(end))
      .child(
        wrap_div(layout)
          .id(self.id)
          .gap(inner_gap)
          .map(|this| match self.align_items {
            Some(AlignItems::Start) => this.items_start(),
            Some(AlignItems::End) => this.items_end(),
            Some(AlignItems::Center) => this.items_center(),
            Some(AlignItems::Baseline) => this.items_baseline(),
            _ => this,
          })
          .when(reserve_label_space || has_label, |this| {
            this.child(
              wrap_label(label_width)
                .text_size(default_label_text_size)
                .when_some(self.props.label_text_size, |this, size| {
                  this.text_size(size)
                })
                .text_color(cx.theme().foreground)
                .font_medium()
                .gap_1()
                .items_center()
                .when_some(self.label, |this, builder| {
                  this.child(
                    h_flex()
                      .gap_1()
                      .child(div().overflow_x_hidden().child(builder.render(window, cx)))
                      .when(self.required, |this| {
                        this.child(div().text_color(cx.theme().danger).child("*"))
                      }),
                  )
                }),
            )
          })
          .child(
            div()
              .w_full()
              .flex_1()
              .overflow_x_hidden()
              .children(self.children),
          ),
      )
      .child(
        wrap_div(layout)
          .gap(inner_gap)
          .when(reserve_label_space, |this| {
            this.child(wrap_label(label_width))
          })
          .when_some(self.description, |this, builder| {
            this.child(
              div()
                .text_size(default_label_text_size)
                .text_color(cx.theme().muted_foreground)
                .child(builder.render(window, cx)),
            )
          }),
      )
      .into_any_element()
  }
}