use std::sync::Arc;
use blinc_core::Color;
use blinc_layout::div::ElementTypeId;
use blinc_layout::prelude::*;
use blinc_layout::widgets::text_input::{
InputType, OnChangeCallback, SharedTextInputData, TextInput,
};
use blinc_theme::{ColorToken, RadiusToken, SpacingToken, ThemeState, TypographyTokens};
use std::ops::{Deref, DerefMut};
use super::label::{label, LabelSize};
#[derive(Clone, Debug, Default)]
pub struct InputBorderColors {
pub idle: Option<Color>,
pub hover: Option<Color>,
pub focused: Option<Color>,
pub error: Option<Color>,
}
#[derive(Clone, Debug, Default)]
pub struct InputBgColors {
pub idle: Option<Color>,
pub hover: Option<Color>,
pub focused: Option<Color>,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum InputSize {
Small,
#[default]
Medium,
Large,
}
impl InputSize {
fn height(&self, theme: &ThemeState) -> f32 {
match self {
InputSize::Small => theme.spacing_value(SpacingToken::Space8), InputSize::Medium => theme.spacing_value(SpacingToken::Space10), InputSize::Large => theme.spacing_value(SpacingToken::Space12), }
}
fn font_size(&self, typography: &TypographyTokens) -> f32 {
match self {
InputSize::Small => typography.text_xs, InputSize::Medium => typography.text_sm, InputSize::Large => typography.text_base, }
}
}
pub struct Input {
inner: Div,
}
impl Input {
pub fn new(data: &SharedTextInputData) -> Self {
Self::with_config(InputConfig {
data: data.clone(),
..Default::default()
})
}
fn with_config(config: InputConfig) -> Self {
let theme = ThemeState::get();
let typography = theme.typography();
if config.error.is_some() {
if let Ok(mut data) = config.data.lock() {
data.is_valid = false;
}
}
let text_input = Self::build_text_input(&config, theme, &typography);
let size_class = match config.size {
InputSize::Small => "cn-input--sm",
InputSize::Medium => "cn-input--md",
InputSize::Large => "cn-input--lg",
};
let text_input = text_input.class("cn-input").class(size_class);
if config.label.is_none() && config.description.is_none() && config.error.is_none() {
let mut container = div().child(text_input);
if config.full_width {
container = container.w_full();
} else if let Some(w) = config.width {
container = container.w(w);
}
return Self { inner: container };
}
let spacing = theme.spacing_value(SpacingToken::Space2);
let mut container = div()
.class("cn-input-group")
.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(text_input);
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));
}
Self { inner: container }
}
fn build_text_input(
config: &InputConfig,
theme: &ThemeState,
typography: &TypographyTokens,
) -> TextInput {
let default_border = theme.color(ColorToken::Border);
let default_border_hover = theme.color(ColorToken::BorderHover);
let default_border_focus = theme.color(ColorToken::BorderFocus);
let default_border_error = theme.color(ColorToken::BorderError);
let default_bg = theme.color(ColorToken::InputBg);
let default_bg_hover = theme.color(ColorToken::InputBgHover);
let default_bg_focus = theme.color(ColorToken::InputBgFocus);
let default_text = theme.color(ColorToken::TextPrimary);
let default_placeholder = theme.color(ColorToken::TextTertiary);
let default_cursor = theme.color(ColorToken::Primary);
let default_selection = theme.color(ColorToken::Selection);
let radius = config
.corner_radius
.unwrap_or_else(|| theme.radius(RadiusToken::Md));
let mut input = blinc_layout::widgets::text_input::text_input(&config.data)
.h(config.size.height(theme))
.text_size(config.size.font_size(typography))
.rounded(radius)
.input_type(config.input_type)
.disabled(config.disabled)
.masked(config.password);
input = input.idle_border_color(config.border_colors.idle.unwrap_or(default_border));
input =
input.hover_border_color(config.border_colors.hover.unwrap_or(default_border_hover));
input = input
.focused_border_color(config.border_colors.focused.unwrap_or(default_border_focus));
input =
input.error_border_color(config.border_colors.error.unwrap_or(default_border_error));
input = input.idle_bg_color(config.bg_colors.idle.unwrap_or(default_bg));
input = input.hover_bg_color(config.bg_colors.hover.unwrap_or(default_bg_hover));
input = input.focused_bg_color(config.bg_colors.focused.unwrap_or(default_bg_focus));
input = input.text_color(config.text_color.unwrap_or(default_text));
input = input.placeholder_color(config.placeholder_color.unwrap_or(default_placeholder));
input = input.cursor_color(config.cursor_color.unwrap_or(default_cursor));
input = input.selection_color(config.selection_color.unwrap_or(default_selection));
if let Some(width) = config.border_width {
input = input.border_width(width);
}
if let Some(ref placeholder) = config.placeholder {
input = input.placeholder(placeholder.clone());
}
if config.full_width {
input = input.w_full();
} else if let Some(w) = config.width {
input = input.w(w);
}
if let Some(ref callback) = config.on_change {
input = input.on_change({
let cb = Arc::clone(callback);
move |value: &str| cb(value)
});
}
input
}
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 Deref for Input {
type Target = Div;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl DerefMut for Input {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl ElementBuilder for Input {
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) -> ElementTypeId {
self.inner.element_type_id()
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
#[derive(Clone)]
struct InputConfig {
data: SharedTextInputData,
size: InputSize,
label: Option<String>,
description: Option<String>,
error: Option<String>,
disabled: bool,
required: bool,
input_type: InputType,
placeholder: Option<String>,
password: bool,
width: Option<f32>,
full_width: bool,
border_colors: InputBorderColors,
bg_colors: InputBgColors,
text_color: Option<Color>,
placeholder_color: Option<Color>,
cursor_color: Option<Color>,
selection_color: Option<Color>,
border_width: Option<f32>,
corner_radius: Option<f32>,
on_change: Option<OnChangeCallback>,
}
impl Default for InputConfig {
fn default() -> Self {
Self {
data: blinc_layout::widgets::text_input::text_input_data(),
size: InputSize::default(),
label: None,
description: None,
error: None,
disabled: false,
required: false,
input_type: InputType::Text,
placeholder: None,
password: false,
width: None,
full_width: true,
border_colors: InputBorderColors::default(),
bg_colors: InputBgColors::default(),
text_color: None,
placeholder_color: None,
cursor_color: None,
selection_color: None,
border_width: None,
corner_radius: None,
on_change: None,
}
}
}
pub struct InputBuilder {
config: InputConfig,
built: std::cell::OnceCell<Input>,
}
impl InputBuilder {
pub fn new(data: &SharedTextInputData) -> Self {
Self {
config: InputConfig {
data: data.clone(),
..Default::default()
},
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Input {
self.built
.get_or_init(|| Input::with_config(self.config.clone()))
}
pub fn size(mut self, size: InputSize) -> Self {
self.config.size = size;
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 input_type(mut self, input_type: InputType) -> Self {
self.config.input_type = input_type;
self
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.config.placeholder = Some(placeholder.into());
self
}
pub fn password(mut self) -> Self {
self.config.password = true;
self.config.input_type = InputType::Password;
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 w_full(mut self) -> Self {
self.config.full_width = true;
self.config.width = None;
self
}
pub fn idle_border_color(mut self, color: impl Into<Color>) -> Self {
self.config.border_colors.idle = Some(color.into());
self
}
pub fn hover_border_color(mut self, color: impl Into<Color>) -> Self {
self.config.border_colors.hover = Some(color.into());
self
}
pub fn focused_border_color(mut self, color: impl Into<Color>) -> Self {
self.config.border_colors.focused = Some(color.into());
self
}
pub fn error_border_color(mut self, color: impl Into<Color>) -> Self {
self.config.border_colors.error = Some(color.into());
self
}
pub fn border_colors(
mut self,
idle: impl Into<Color>,
hover: impl Into<Color>,
focused: impl Into<Color>,
error: impl Into<Color>,
) -> Self {
self.config.border_colors = InputBorderColors {
idle: Some(idle.into()),
hover: Some(hover.into()),
focused: Some(focused.into()),
error: Some(error.into()),
};
self
}
pub fn idle_bg_color(mut self, color: impl Into<Color>) -> Self {
self.config.bg_colors.idle = Some(color.into());
self
}
pub fn hover_bg_color(mut self, color: impl Into<Color>) -> Self {
self.config.bg_colors.hover = Some(color.into());
self
}
pub fn focused_bg_color(mut self, color: impl Into<Color>) -> Self {
self.config.bg_colors.focused = Some(color.into());
self
}
pub fn bg_colors(
mut self,
idle: impl Into<Color>,
hover: impl Into<Color>,
focused: impl Into<Color>,
) -> Self {
self.config.bg_colors = InputBgColors {
idle: Some(idle.into()),
hover: Some(hover.into()),
focused: Some(focused.into()),
};
self
}
pub fn text_color(mut self, color: impl Into<Color>) -> Self {
self.config.text_color = Some(color.into());
self
}
pub fn placeholder_color(mut self, color: impl Into<Color>) -> Self {
self.config.placeholder_color = Some(color.into());
self
}
pub fn cursor_color(mut self, color: impl Into<Color>) -> Self {
self.config.cursor_color = Some(color.into());
self
}
pub fn selection_color(mut self, color: impl Into<Color>) -> Self {
self.config.selection_color = Some(color.into());
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 on_change<F>(mut self, callback: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
self.config.on_change = Some(Arc::new(callback));
self
}
pub fn build_component(self) -> Input {
Input::with_config(self.config)
}
}
impl ElementBuilder for InputBuilder {
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) -> ElementTypeId {
self.get_or_build().element_type_id()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
pub fn input(data: &SharedTextInputData) -> InputBuilder {
InputBuilder::new(data)
}
#[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_input_size_values() {
init_theme();
let theme = ThemeState::get();
let typography = TypographyTokens::default();
assert!(InputSize::Small.height(theme) > 0.0);
assert!(InputSize::Medium.height(theme) > InputSize::Small.height(theme));
assert!(InputSize::Large.height(theme) > InputSize::Medium.height(theme));
assert_eq!(InputSize::Small.font_size(&typography), typography.text_xs);
assert_eq!(InputSize::Medium.font_size(&typography), typography.text_sm);
assert_eq!(
InputSize::Large.font_size(&typography),
typography.text_base
);
}
#[test]
fn test_input_builder() {
init_theme();
let data = blinc_layout::widgets::text_input::text_input_data();
let _input = input(&data)
.label("Username")
.placeholder("Enter username")
.size(InputSize::Large);
}
}