use teksilo_core::build_context::BuildContext;
use teksilo_core::color_prop::ColorProp;
use teksilo_core::signal::Signal;
use teksilo_core::styles::{
TextInputStyle, TextInputStyleConfig, TextInputValidationLevel, TextInputVariant,
};
use teksilo_core::widget_id::WidgetId;
use teksilo_tokens::{BorderRole, CornerRadius, SurfaceRole};
use crate::primitives::{MinSize, Padding, RectWidget, ZStack};
pub const TEXT_FIELD_HEIGHT: f32 = 28.0;
pub const TEXT_FIELD_PADDING_HORIZONTAL: f32 = 4.0;
pub const TEXT_FIELD_PADDING_VERTICAL: f32 = 4.0;
pub const TEXT_FIELD_BORDER_WIDTH: f32 = 1.0;
pub const TEXT_FIELD_CORNER_RADIUS: f32 = 4.0;
pub const TEXT_FIELD_CARET_WIDTH: f32 = 1.0;
pub const TEXT_FIELD_VALIDATION_STRIP_GAP: f32 = 4.0;
pub const TEXT_FIELD_ERROR_PULSE_DURATION_MS: u32 = 240;
pub const TEXT_FIELD_CORRECTED_PULSE_DURATION_MS: u32 = 1500;
pub const TEXT_FIELD_MASK_PLACEHOLDER_CHAR: char = '_';
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextInputRecipe {
pub height: f32,
pub padding_horizontal: f32,
pub padding_vertical: f32,
pub border_width: f32,
pub corner_radius: f32,
pub caret_width: f32,
pub validation_strip_gap: f32,
pub error_pulse_duration_ms: u32,
pub corrected_pulse_duration_ms: u32,
pub mask_placeholder_char: char,
}
impl Default for TextInputRecipe {
fn default() -> Self {
Self {
height: TEXT_FIELD_HEIGHT,
padding_horizontal: TEXT_FIELD_PADDING_HORIZONTAL,
padding_vertical: TEXT_FIELD_PADDING_VERTICAL,
border_width: TEXT_FIELD_BORDER_WIDTH,
corner_radius: TEXT_FIELD_CORNER_RADIUS,
caret_width: TEXT_FIELD_CARET_WIDTH,
validation_strip_gap: TEXT_FIELD_VALIDATION_STRIP_GAP,
error_pulse_duration_ms: TEXT_FIELD_ERROR_PULSE_DURATION_MS,
corrected_pulse_duration_ms: TEXT_FIELD_CORRECTED_PULSE_DURATION_MS,
mask_placeholder_char: TEXT_FIELD_MASK_PLACEHOLDER_CHAR,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RecipeTextInputStyle {
pub recipe: TextInputRecipe,
}
impl RecipeTextInputStyle {
pub fn new(recipe: TextInputRecipe) -> Self {
Self { recipe }
}
}
impl TextInputStyle for RecipeTextInputStyle {
fn make_body(&self, cfg: &TextInputStyleConfig, ctx: &mut BuildContext) -> WidgetId {
let theme = ctx.theme();
let border_width = self.recipe.border_width;
let focus_ring_width = theme.shape.focus_ring_width;
let padding_h = self.recipe.padding_horizontal;
let corner_radius = self.recipe.corner_radius;
let height = self.recipe.height;
if matches!(cfg.variant, TextInputVariant::Bare) {
return ctx.add(MinSize::new(0.0, height).child_id(cfg.editor));
}
let border_role = derive_border_role(
cfg.is_focused.clone(),
cfg.validation.clone(),
cfg.is_disabled.clone(),
);
let variant = cfg.variant;
let border_width_signal = cfg.is_focused.map(move |focused| match variant {
TextInputVariant::Filled => 0.0,
_ => {
if *focused {
focus_ring_width
} else {
border_width
}
}
});
let bg_role: ColorProp = match variant {
TextInputVariant::Filled => ColorProp::DynamicSurfaceRole(cfg.is_disabled.map(|d| {
if *d {
SurfaceRole::Disabled
} else {
SurfaceRole::Hover
}
})),
_ => SurfaceRole::Field.into(),
};
let bg = RectWidget::new()
.background(bg_role)
.border_color(border_role)
.border_width(border_width_signal)
.corner_radius(CornerRadius::uniform(corner_radius));
let bg_id = ctx.add(bg);
let padded_id = ctx.add(Padding::new(0.0, padding_h, 0.0, padding_h).child_id(cfg.editor));
let zstack_id = ctx.add(ZStack::new().add_child(bg_id).add_child(padded_id));
ctx.add(MinSize::new(0.0, height).child_id(zstack_id))
}
}
fn derive_border_role(
is_focused: Signal<bool>,
validation: Signal<TextInputValidationLevel>,
is_disabled: Signal<bool>,
) -> Signal<BorderRole> {
is_focused
.zip3(&validation, &is_disabled)
.map(|(focused, level, disabled)| {
if *disabled {
return BorderRole::Disabled;
}
match *level {
TextInputValidationLevel::Error => BorderRole::Error,
TextInputValidationLevel::Warning => BorderRole::Warning,
TextInputValidationLevel::Corrected | TextInputValidationLevel::Info => {
BorderRole::Focused
}
TextInputValidationLevel::None => {
if *focused {
BorderRole::Focused
} else {
BorderRole::Field
}
}
}
})
}