use super::label::{LabelSize, label};
use blinc_layout::prelude::*;
use blinc_layout::widgets::text_area::{
SharedTextAreaState, TextArea as LayoutTextArea, text_area,
};
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,
textarea_ref: Option<blinc_layout::selector::TextareaRef>,
on_change: Option<blinc_layout::widgets::TextCallback>,
size: TextareaSize,
rows: Option<usize>,
cols: Option<usize>,
label: Option<blinc_layout::binding::Reactive<String>>,
description: Option<blinc_layout::binding::Reactive<String>>,
error: Option<blinc_layout::binding::Reactive<String>>,
disabled: blinc_layout::binding::Reactive<bool>,
required: bool,
placeholder: Option<blinc_layout::binding::Reactive<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,
textarea_ref: None,
size: TextareaSize::default(),
rows: None,
cols: None,
label: None,
description: None,
error: None,
disabled: blinc_layout::binding::Reactive::Const(false),
required: false,
placeholder: None,
max_length: None,
wrap: true,
width: None,
height: None,
full_width: true,
border_width: None,
corner_radius: None,
on_change: 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);
if let Some(textarea_ref) = &config.textarea_ref {
ta = ta.bind(textarea_ref);
}
let mut ta = ta
.font_size(config.size.font_size(&typography))
.rounded(radius)
.disabled(config.disabled.clone())
.wrap(config.wrap);
if let Some(cb) = config.on_change.clone() {
ta = ta.on_change(move |text| cb(text));
}
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 mut ta = ta.class("cn-textarea").class("tabindex=text");
if let Some(ref error) = config.error {
ta = ta.invalid(crate::reactive_props::non_empty(error));
}
let inner =
if config.label.is_none() && config.description.is_none() && config.error.is_none() {
let mut wrapper = div().child(ta);
if let Some(w) = config.width {
wrapper = wrapper.w(w);
} else if config.full_width {
wrapper = wrapper.w_full();
}
wrapper
} 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(label_text) = config.label.clone() {
let mut lbl = label(label_text)
.size(LabelSize::Medium)
.disabled(config.disabled.clone());
if config.required {
lbl = lbl.required();
}
container = container.child(lbl);
}
container = container.child(ta);
if let Some(message) = crate::reactive_props::field_message(
&config.error,
&config.description,
typography.text_xs,
) {
container = container.child(message);
}
container
};
Self { inner }
}
pub fn class(mut self, name: impl AsRef<str>) -> 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) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn element_id(&self) -> Option<&str> {
ElementBuilder::element_id(&self.inner)
}
}
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 {
::blinc_layout::build_once::build_once(&self.built, || {
Textarea::from_config(self.config.clone())
})
}
pub fn size(mut self, size: TextareaSize) -> Self {
self.config.size = size;
self
}
pub fn bind(mut self, textarea_ref: &blinc_layout::selector::TextareaRef) -> Self {
self.config.textarea_ref = Some(textarea_ref.clone());
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 blinc_layout::binding::IntoReactive<String>) -> Self {
self.config.label = Some(label.into_reactive());
self
}
pub fn description(
mut self,
description: impl blinc_layout::binding::IntoReactive<String>,
) -> Self {
self.config.description = Some(description.into_reactive());
self
}
pub fn error(mut self, error: impl blinc_layout::binding::IntoReactive<String>) -> Self {
self.config.error = Some(error.into_reactive());
self
}
pub fn placeholder(
mut self,
placeholder: impl blinc_layout::binding::IntoReactive<String>,
) -> Self {
self.config.placeholder = Some(placeholder.into_reactive());
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.config.max_length = Some(max);
self
}
pub fn on_change<F>(mut self, callback: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.config.on_change = Some(std::sync::Arc::new(callback));
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: impl blinc_layout::binding::IntoReactive<bool>) -> Self {
self.config.disabled = disabled.into_reactive();
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) -> &[std::sync::Arc<str>] {
self.get_or_build().element_classes()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(self.get_or_build())
}
fn element_id(&self) -> Option<&str> {
ElementBuilder::element_id(self.get_or_build())
}
}
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.as_ref().and_then(|l| l.now()).as_deref(),
Some("Description")
);
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);
}
}