use crate::components::input::InputVariant;
use crate::theme::use_theme;
use kael::{prelude::FluentBuilder as _, *};
use std::rc::Rc;
#[derive(IntoElement)]
pub struct Textarea {
id: SharedString,
value: SharedString,
placeholder: SharedString,
variant: InputVariant,
disabled: bool,
error: bool,
rows: usize,
min_rows: Option<usize>,
max_rows: Option<usize>,
auto_grow: bool,
resizable: bool,
on_change: Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>,
on_blur: Option<Rc<dyn Fn(SharedString, &mut Window, &mut App)>>,
style: StyleRefinement,
}
impl Textarea {
pub fn new(id: impl Into<SharedString>) -> Self {
Self {
id: id.into(),
value: "".into(),
placeholder: "".into(),
variant: InputVariant::Default,
disabled: false,
error: false,
rows: 3,
min_rows: None,
max_rows: None,
auto_grow: false,
resizable: true,
on_change: None,
on_blur: None,
style: StyleRefinement::default(),
}
}
pub fn value(mut self, value: impl Into<SharedString>) -> Self {
self.value = value.into();
self
}
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn variant(mut self, variant: InputVariant) -> Self {
self.variant = variant;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn error(mut self, error: bool) -> Self {
self.error = error;
self
}
pub fn rows(mut self, rows: usize) -> Self {
self.rows = rows.max(1);
self
}
pub fn min_rows(mut self, min_rows: usize) -> Self {
self.min_rows = Some(min_rows.max(1));
self
}
pub fn max_rows(mut self, max_rows: usize) -> Self {
self.max_rows = Some(max_rows.max(1));
self
}
pub fn auto_grow(mut self, auto_grow: bool) -> Self {
self.auto_grow = auto_grow;
self
}
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
pub fn on_change<F>(mut self, callback: F) -> Self
where
F: Fn(SharedString, &mut Window, &mut App) + 'static,
{
self.on_change = Some(Rc::new(callback));
self
}
pub fn on_blur<F>(mut self, callback: F) -> Self
where
F: Fn(SharedString, &mut Window, &mut App) + 'static,
{
self.on_blur = Some(Rc::new(callback));
self
}
fn calculate_height(&self) -> Pixels {
let line_height = 20.0;
let padding_y = 8.0;
px(self.rows as f32 * line_height + padding_y * 2.0)
}
}
impl Styled for Textarea {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Textarea {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let theme = use_theme();
let user_style = self.style.clone();
let height = self.calculate_height();
let (bg_color, border_color, text_color) = if self.disabled {
(
theme.tokens.muted.opacity(0.5),
theme.tokens.border,
theme.tokens.muted_foreground,
)
} else if self.error {
match self.variant {
InputVariant::Default => (
theme.tokens.background,
theme.tokens.destructive,
theme.tokens.foreground,
),
InputVariant::Outline => (
theme.tokens.background,
theme.tokens.destructive,
theme.tokens.foreground,
),
InputVariant::Ghost => (
kael::transparent_black(),
theme.tokens.destructive.opacity(0.3),
theme.tokens.foreground,
),
}
} else {
match self.variant {
InputVariant::Default => (
theme.tokens.background,
theme.tokens.input,
theme.tokens.foreground,
),
InputVariant::Outline => (
theme.tokens.background,
theme.tokens.border,
theme.tokens.foreground,
),
InputVariant::Ghost => (
kael::transparent_black(),
theme.tokens.border.opacity(0.3),
theme.tokens.foreground,
),
}
};
let textarea_id = self.id.clone();
let has_value = !self.value.is_empty();
div()
.id(textarea_id)
.w_full()
.h(height)
.when(self.auto_grow, |this| this.min_h(height))
.px(px(12.0))
.py(px(8.0))
.bg(bg_color)
.border_1()
.border_color(border_color)
.rounded(theme.tokens.radius_md)
.when(!self.disabled, |this| {
this.hover(|style| {
style.border_color(if self.error {
theme.tokens.destructive
} else {
theme.tokens.ring
})
})
})
.when(!self.resizable, |this| this)
.map(|this| {
let mut div = this;
div.style().refine(&user_style);
div
})
.child(
div()
.size_full()
.text_size(px(14.0))
.font_family(theme.tokens.font_mono.clone())
.text_color(text_color)
.line_height(relative(1.4))
.child(if has_value {
self.value.to_string()
} else {
self.placeholder.to_string()
})
.when(!has_value, |this| {
this.text_color(theme.tokens.muted_foreground)
}),
)
}
}