use super::label::{label, LabelSize};
use blinc_layout::prelude::*;
use blinc_layout::widgets::text_area::{
text_area, SharedTextAreaState, TextArea as LayoutTextArea,
};
use blinc_theme::{ColorToken, RadiusToken, SpacingToken, ThemeState, TypographyTokens};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextareaSize {
Small,
#[default]
Medium,
Large,
}
impl TextareaSize {
fn default_rows(&self) -> usize {
match self {
TextareaSize::Small => 3,
TextareaSize::Medium => 4,
TextareaSize::Large => 6,
}
}
fn font_size(&self, typography: &TypographyTokens) -> f32 {
match self {
TextareaSize::Small => typography.text_xs, TextareaSize::Medium => typography.text_sm, TextareaSize::Large => typography.text_base, }
}
}
#[derive(Clone)]
struct TextareaConfig {
state: SharedTextAreaState,
size: TextareaSize,
rows: Option<usize>,
cols: Option<usize>,
label: Option<String>,
description: Option<String>,
error: Option<String>,
disabled: bool,
required: bool,
placeholder: Option<String>,
max_length: Option<usize>,
wrap: bool,
width: Option<f32>,
height: Option<f32>,
full_width: bool,
border_width: Option<f32>,
corner_radius: Option<f32>,
}
impl TextareaConfig {
fn new(state: SharedTextAreaState) -> Self {
Self {
state,
size: TextareaSize::default(),
rows: None,
cols: None,
label: None,
description: None,
error: None,
disabled: false,
required: false,
placeholder: None,
max_length: None,
wrap: true,
width: None,
height: None,
full_width: true,
border_width: None,
corner_radius: None,
}
}
}
pub struct Textarea {
inner: Div,
}
impl Textarea {
fn from_config(config: TextareaConfig) -> Self {
let theme = ThemeState::get();
let typography = theme.typography();
let radius = config
.corner_radius
.unwrap_or_else(|| theme.radius(RadiusToken::Md));
let mut ta = text_area(&config.state)
.font_size(config.size.font_size(&typography))
.rounded(radius)
.disabled(config.disabled)
.wrap(config.wrap);
if let Some(border) = config.border_width {
ta = ta.border_width(border);
}
if let Some(rows) = config.rows {
ta = ta.rows(rows);
} else {
ta = ta.rows(config.size.default_rows());
}
if let Some(cols) = config.cols {
ta = ta.cols(cols);
}
if let Some(w) = config.width {
ta = ta.w(w);
} else if config.full_width {
ta = ta.w_full();
}
if let Some(h) = config.height {
ta = ta.h(h);
}
if let Some(ref placeholder) = config.placeholder {
ta = ta.placeholder(placeholder.clone());
}
if let Some(max) = config.max_length {
ta = ta.max_length(max);
}
let ta = ta.class("cn-textarea");
let inner =
if config.label.is_none() && config.description.is_none() && config.error.is_none() {
div().child(ta)
} else {
let spacing = theme.spacing_value(SpacingToken::Space2);
let mut container = div().flex_col().gap_px(spacing).h_fit();
if config.full_width {
container = container.w_full();
} else if let Some(w) = config.width {
container = container.w(w);
}
if let Some(ref label_text) = config.label {
let mut lbl = label(label_text).size(LabelSize::Medium);
if config.required {
lbl = lbl.required();
}
if config.disabled {
lbl = lbl.disabled(true);
}
container = container.child(lbl);
}
container = container.child(ta);
if let Some(ref error_text) = config.error {
let error_color = theme.color(ColorToken::Error);
container = container
.child(text(error_text).size(typography.text_xs).color(error_color));
} else if let Some(ref desc_text) = config.description {
let desc_color = theme.color(ColorToken::TextTertiary);
container =
container.child(text(desc_text).size(typography.text_xs).color(desc_color));
}
container
};
Self { inner }
}
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for Textarea {
fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
self.inner.element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct TextareaBuilder {
config: TextareaConfig,
built: std::cell::OnceCell<Textarea>,
}
impl TextareaBuilder {
pub fn new(state: &SharedTextAreaState) -> Self {
Self {
config: TextareaConfig::new(state.clone()),
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Textarea {
self.built
.get_or_init(|| Textarea::from_config(self.config.clone()))
}
pub fn size(mut self, size: TextareaSize) -> Self {
self.config.size = size;
self
}
pub fn rows(mut self, rows: usize) -> Self {
self.config.rows = Some(rows);
self
}
pub fn cols(mut self, cols: usize) -> Self {
self.config.cols = Some(cols);
self
}
pub fn label(mut self, label: impl Into<String>) -> Self {
self.config.label = Some(label.into());
self
}
pub fn description(mut self, description: impl Into<String>) -> Self {
self.config.description = Some(description.into());
self
}
pub fn error(mut self, error: impl Into<String>) -> Self {
self.config.error = Some(error.into());
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.config.placeholder = Some(placeholder.into());
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.config.max_length = Some(max);
self
}
pub fn wrap(mut self, wrap: bool) -> Self {
self.config.wrap = wrap;
self
}
pub fn no_wrap(mut self) -> Self {
self.config.wrap = false;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.config.disabled = disabled;
self
}
pub fn required(mut self) -> Self {
self.config.required = true;
self
}
pub fn w(mut self, width: f32) -> Self {
self.config.width = Some(width);
self.config.full_width = false;
self
}
pub fn h(mut self, height: f32) -> Self {
self.config.height = Some(height);
self
}
pub fn w_full(mut self) -> Self {
self.config.full_width = true;
self.config.width = None;
self
}
pub fn border_width(mut self, width: f32) -> Self {
self.config.border_width = Some(width);
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.config.corner_radius = Some(radius);
self
}
pub fn build_component(self) -> Textarea {
Textarea::from_config(self.config)
}
}
impl ElementBuilder for TextareaBuilder {
fn build(&self, tree: &mut blinc_layout::tree::LayoutTree) -> blinc_layout::tree::LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> blinc_layout::element::RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn element_type_id(&self) -> blinc_layout::div::ElementTypeId {
self.get_or_build().element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn textarea(state: &SharedTextAreaState) -> TextareaBuilder {
TextareaBuilder::new(state)
}
#[cfg(test)]
mod tests {
use super::*;
use blinc_theme::TypographyTokens;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_textarea_size_values() {
let typography = TypographyTokens::default();
assert_eq!(TextareaSize::Small.default_rows(), 3);
assert_eq!(TextareaSize::Medium.default_rows(), 4);
assert_eq!(TextareaSize::Large.default_rows(), 6);
assert_eq!(
TextareaSize::Small.font_size(&typography),
typography.text_xs
);
assert_eq!(
TextareaSize::Medium.font_size(&typography),
typography.text_sm
);
assert_eq!(
TextareaSize::Large.font_size(&typography),
typography.text_base
);
}
#[test]
fn test_textarea_builder() {
init_theme();
let state = blinc_layout::widgets::text_area::text_area_state();
let ta = TextareaBuilder::new(&state)
.label("Description")
.placeholder("Enter description...")
.rows(5)
.size(TextareaSize::Large);
assert_eq!(ta.config.size, TextareaSize::Large);
assert_eq!(ta.config.label, Some("Description".to_string()));
assert_eq!(ta.config.rows, Some(5));
}
#[test]
fn test_textarea_wrap_settings() {
init_theme();
let state = blinc_layout::widgets::text_area::text_area_state();
let ta = TextareaBuilder::new(&state);
assert!(ta.config.wrap);
let ta_no_wrap = TextareaBuilder::new(&state).no_wrap();
assert!(!ta_no_wrap.config.wrap);
}
}