use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, OnceLock, Weak};
use blinc_core::Color;
use blinc_theme::{ColorToken, ThemeState};
use crate::canvas::canvas;
use crate::css_parser::{active_stylesheet, ElementState, Stylesheet};
use crate::div::{div, Div, ElementBuilder};
use crate::element::RenderProps;
use crate::stateful::{
refresh_stateful, SharedState, StateTransitions, Stateful, StatefulInner, TextFieldState,
};
use crate::text::text;
use crate::text_selection::{clear_selection, set_selection, SelectionSource};
use crate::tree::{LayoutNodeId, LayoutTree};
use crate::widgets::cursor::{cursor_state, CursorAnimation, SharedCursorState};
pub fn elapsed_ms() -> u64 {
static START_TIME: OnceLock<std::time::Instant> = OnceLock::new();
let start = START_TIME.get_or_init(std::time::Instant::now);
start.elapsed().as_millis() as u64
}
pub const CURSOR_BLINK_INTERVAL_MS: u64 = 400;
static GLOBAL_FOCUS_COUNT: AtomicU64 = AtomicU64::new(0);
static NEEDS_REBUILD: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static NEEDS_RELAYOUT: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static NEEDS_CSS_REPARSE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
static NEEDS_CONTINUOUS_REDRAW: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
static FOCUSED_TEXT_INPUT: Mutex<Option<Weak<Mutex<TextInputData>>>> = Mutex::new(None);
static FOCUSED_TEXT_AREA: Mutex<Option<Weak<Mutex<crate::widgets::text_area::TextAreaState>>>> =
Mutex::new(None);
#[allow(clippy::type_complexity)]
static CONTINUOUS_REDRAW_CALLBACK: Mutex<Option<Box<dyn Fn(bool) + Send + Sync>>> =
Mutex::new(None);
static KEYBOARD_SHOULD_SHOW: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
static KEYBOARD_STATE_CHANGED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
pub fn set_continuous_redraw_callback<F>(callback: F)
where
F: Fn(bool) + Send + Sync + 'static,
{
let mut guard = CONTINUOUS_REDRAW_CALLBACK.lock().unwrap();
*guard = Some(Box::new(callback));
}
fn notify_continuous_redraw(enabled: bool) {
if let Ok(guard) = CONTINUOUS_REDRAW_CALLBACK.lock() {
if let Some(ref callback) = *guard {
callback(enabled);
}
}
}
fn notify_keyboard_visibility(show: bool) {
KEYBOARD_SHOULD_SHOW.store(show, Ordering::SeqCst);
KEYBOARD_STATE_CHANGED.store(true, Ordering::SeqCst);
}
pub fn take_keyboard_state_change() -> Option<bool> {
if KEYBOARD_STATE_CHANGED.swap(false, Ordering::SeqCst) {
Some(KEYBOARD_SHOULD_SHOW.load(Ordering::SeqCst))
} else {
None
}
}
pub fn has_focused_text_input() -> bool {
GLOBAL_FOCUS_COUNT.load(Ordering::Relaxed) > 0
}
pub fn take_needs_continuous_redraw() -> bool {
NEEDS_CONTINUOUS_REDRAW.swap(false, Ordering::SeqCst)
}
fn request_continuous_redraw() {
if has_focused_text_input() {
NEEDS_CONTINUOUS_REDRAW.store(true, Ordering::SeqCst);
}
}
pub fn request_continuous_redraw_pub() {
request_continuous_redraw();
}
pub fn take_needs_rebuild() -> bool {
NEEDS_REBUILD.swap(false, Ordering::SeqCst)
}
pub fn request_rebuild() {
NEEDS_REBUILD.store(true, Ordering::SeqCst);
}
pub fn take_needs_relayout() -> bool {
NEEDS_RELAYOUT.swap(false, Ordering::SeqCst)
}
pub fn request_full_rebuild() {
NEEDS_REBUILD.store(true, Ordering::SeqCst);
NEEDS_RELAYOUT.store(true, Ordering::SeqCst);
crate::stateful::request_redraw();
}
pub fn take_needs_css_reparse() -> bool {
NEEDS_CSS_REPARSE.swap(false, Ordering::SeqCst)
}
pub fn request_css_reparse() {
NEEDS_CSS_REPARSE.store(true, Ordering::SeqCst);
}
pub(crate) fn increment_focus_count() {
let prev = GLOBAL_FOCUS_COUNT.fetch_add(1, Ordering::Relaxed);
if prev == 0 {
notify_continuous_redraw(true);
notify_keyboard_visibility(true);
}
}
pub(crate) fn decrement_focus_count() {
let prev = GLOBAL_FOCUS_COUNT.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |v| {
Some(v.saturating_sub(1))
});
if let Ok(prev_val) = prev {
if prev_val == 1 {
notify_continuous_redraw(false);
notify_keyboard_visibility(false);
}
}
}
pub(crate) fn set_focused_text_input(state: &SharedTextInputData) {
use blinc_core::events::event_types;
let mut focused = FOCUSED_TEXT_INPUT.lock().unwrap();
if let Some(weak) = focused.take() {
if let Some(prev_state) = weak.upgrade() {
if !Arc::ptr_eq(&prev_state, state) {
if let Ok(mut s) = prev_state.lock() {
if let Some(new_state) = s.visual.on_event(event_types::BLUR) {
s.visual = new_state;
decrement_focus_count();
}
}
}
}
}
blur_focused_text_area();
*focused = Some(Arc::downgrade(state));
}
pub(crate) fn clear_focused_text_input(state: &SharedTextInputData) {
let mut focused = FOCUSED_TEXT_INPUT.lock().unwrap();
if let Some(weak) = focused.as_ref() {
if let Some(prev_state) = weak.upgrade() {
if Arc::ptr_eq(&prev_state, state) {
*focused = None;
}
}
}
}
pub(crate) fn set_focused_text_area(state: &crate::widgets::text_area::SharedTextAreaState) {
use blinc_core::events::event_types;
{
let mut focused = FOCUSED_TEXT_INPUT.lock().unwrap();
if let Some(weak) = focused.take() {
if let Some(prev_state) = weak.upgrade() {
if let Ok(mut s) = prev_state.lock() {
if let Some(new_state) = s.visual.on_event(event_types::BLUR) {
s.visual = new_state;
decrement_focus_count();
}
}
}
}
}
{
let mut focused = FOCUSED_TEXT_AREA.lock().unwrap();
if let Some(weak) = focused.take() {
if let Some(prev_state) = weak.upgrade() {
if !Arc::ptr_eq(&prev_state, state) {
if let Ok(mut s) = prev_state.lock() {
if let Some(new_state) = s.visual.on_event(event_types::BLUR) {
s.visual = new_state;
decrement_focus_count();
}
}
}
}
}
*focused = Some(Arc::downgrade(state));
}
}
pub(crate) fn clear_focused_text_area(state: &crate::widgets::text_area::SharedTextAreaState) {
let mut focused = FOCUSED_TEXT_AREA.lock().unwrap();
if let Some(weak) = focused.as_ref() {
if let Some(prev_state) = weak.upgrade() {
if Arc::ptr_eq(&prev_state, state) {
*focused = None;
}
}
}
}
fn blur_focused_text_area() {
use blinc_core::events::event_types;
let mut focused = FOCUSED_TEXT_AREA.lock().unwrap();
if let Some(weak) = focused.take() {
if let Some(prev_state) = weak.upgrade() {
if let Ok(mut s) = prev_state.lock() {
if let Some(new_state) = s.visual.on_event(event_types::BLUR) {
s.visual = new_state;
decrement_focus_count();
}
}
}
}
}
pub fn blur_all_text_inputs() {
use crate::stateful::refresh_stateful;
use blinc_core::events::event_types;
{
let mut focused = FOCUSED_TEXT_INPUT.lock().unwrap();
if let Some(weak) = focused.take() {
if let Some(state) = weak.upgrade() {
if let Ok(mut s) = state.lock() {
if s.visual.is_focused() {
if let Some(new_state) = s.visual.on_event(event_types::BLUR) {
s.visual = new_state;
decrement_focus_count();
}
let stateful_ref = s.stateful_state.clone();
if let Some(ref stateful) = stateful_ref {
if let Ok(mut shared) = stateful.lock() {
if let Some(new_fsm) = shared.state.on_event(event_types::BLUR) {
shared.state = new_fsm;
shared.needs_visual_update = true;
}
}
}
drop(s);
if let Some(ref stateful) = stateful_ref {
refresh_stateful(stateful);
}
}
}
}
}
}
{
let mut focused = FOCUSED_TEXT_AREA.lock().unwrap();
if let Some(weak) = focused.take() {
if let Some(state) = weak.upgrade() {
if let Ok(mut s) = state.lock() {
if s.visual.is_focused() {
if let Some(new_state) = s.visual.on_event(event_types::BLUR) {
s.visual = new_state;
decrement_focus_count();
}
let stateful_ref = s.stateful_state.clone();
if let Some(ref stateful) = stateful_ref {
if let Ok(mut shared) = stateful.lock() {
if let Some(new_fsm) = shared.state.on_event(event_types::BLUR) {
shared.state = new_fsm;
shared.needs_visual_update = true;
}
}
}
drop(s);
if let Some(ref stateful) = stateful_ref {
refresh_stateful(stateful);
}
}
}
}
}
}
}
pub fn focused_text_input_node_id() -> Option<LayoutNodeId> {
let focused = FOCUSED_TEXT_INPUT.lock().ok()?;
let weak = focused.as_ref()?;
let data = weak.upgrade()?;
let guard = data.lock().ok()?;
let stateful = guard.stateful_state.as_ref()?;
let shared = stateful.lock().ok()?;
shared.node_id
}
pub fn focused_text_area_node_id() -> Option<LayoutNodeId> {
let focused = FOCUSED_TEXT_AREA.lock().ok()?;
let weak = focused.as_ref()?;
let data = weak.upgrade()?;
let guard = data.lock().ok()?;
let stateful = guard.stateful_state.as_ref()?;
let shared = stateful.lock().ok()?;
shared.node_id
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum InputType {
#[default]
Text,
Number,
Integer,
Email,
Password,
Url,
Tel,
Search,
}
#[derive(Clone, Debug, Default)]
pub struct InputConstraints {
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub min_value: Option<f64>,
pub max_value: Option<f64>,
pub pattern: Option<String>,
pub required: bool,
}
impl InputConstraints {
pub fn max_length(max: usize) -> Self {
Self {
max_length: Some(max),
..Default::default()
}
}
pub fn required() -> Self {
Self {
required: true,
..Default::default()
}
}
pub fn number_range(min: f64, max: f64) -> Self {
Self {
min_value: Some(min),
max_value: Some(max),
..Default::default()
}
}
}
pub type SharedTextInputData = Arc<Mutex<TextInputData>>;
#[derive(Clone)]
pub struct TextInputData {
pub value: String,
pub cursor: usize,
pub selection_start: Option<usize>,
pub placeholder: String,
pub input_type: InputType,
pub constraints: InputConstraints,
pub disabled: bool,
pub masked: bool,
pub is_valid: bool,
pub visual: TextFieldState,
pub focus_time_ms: u64,
pub cursor_state: SharedCursorState,
pub scroll_offset_x: f32,
pub computed_width: Option<f32>,
pub layout_bounds_storage: crate::renderer::LayoutBoundsStorage,
pub(crate) stateful_state: Option<SharedState<TextFieldState>>,
pub(crate) on_change_callback: Option<OnChangeCallback>,
pub(crate) css_element_id: Option<String>,
pub(crate) css_classes: Vec<String>,
pub(crate) last_click_time: Option<std::time::Instant>,
pub(crate) drag_select_anchor: Option<usize>,
}
impl std::fmt::Debug for TextInputData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextInputData")
.field("value", &self.value)
.field("cursor", &self.cursor)
.field("selection_start", &self.selection_start)
.field("placeholder", &self.placeholder)
.field("input_type", &self.input_type)
.field("constraints", &self.constraints)
.field("disabled", &self.disabled)
.field("masked", &self.masked)
.field("is_valid", &self.is_valid)
.field("visual", &self.visual)
.field("focus_time_ms", &self.focus_time_ms)
.finish()
}
}
impl Default for TextInputData {
fn default() -> Self {
Self::new()
}
}
impl TextInputData {
pub fn new() -> Self {
Self {
value: String::new(),
cursor: 0,
selection_start: None,
placeholder: String::new(),
input_type: InputType::Text,
constraints: InputConstraints::default(),
disabled: false,
masked: false,
is_valid: true,
visual: TextFieldState::Idle,
focus_time_ms: 0,
cursor_state: cursor_state(),
scroll_offset_x: 0.0,
computed_width: None,
layout_bounds_storage: Arc::new(Mutex::new(None)),
stateful_state: None,
on_change_callback: None,
css_element_id: None,
css_classes: Vec::new(),
last_click_time: None,
drag_select_anchor: None,
}
}
pub fn with_placeholder(placeholder: impl Into<String>) -> Self {
Self {
placeholder: placeholder.into(),
..Self::new()
}
}
pub fn with_value(value: impl Into<String>) -> Self {
let v: String = value.into();
let cursor = v.chars().count();
Self {
value: v,
cursor,
..Self::new()
}
}
pub fn display_text(&self) -> String {
if self.masked {
"•".repeat(self.value.chars().count())
} else {
self.value.clone()
}
}
pub fn insert(&mut self, text: &str) {
if let Some(start) = self.selection_start {
let (from, to) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
let before: String = self.value.chars().take(from).collect();
let after: String = self.value.chars().skip(to).collect();
self.value = before + &after;
self.cursor = from;
self.selection_start = None;
}
let filtered: String = match self.input_type {
InputType::Number => text
.chars()
.filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-')
.collect(),
InputType::Integer => text
.chars()
.filter(|c| c.is_ascii_digit() || *c == '-')
.collect(),
InputType::Tel => text
.chars()
.filter(|c| c.is_ascii_digit() || *c == '+' || *c == '-' || *c == ' ')
.collect(),
_ => text.to_string(),
};
if filtered.is_empty() {
return;
}
if let Some(max) = self.constraints.max_length {
if self.value.chars().count() + filtered.chars().count() > max {
return;
}
}
let before: String = self.value.chars().take(self.cursor).collect();
let after: String = self.value.chars().skip(self.cursor).collect();
self.value = before + &filtered + &after;
self.cursor += filtered.chars().count();
self.validate();
}
pub fn delete_backward(&mut self) {
if let Some(start) = self.selection_start {
let (from, to) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
let before: String = self.value.chars().take(from).collect();
let after: String = self.value.chars().skip(to).collect();
self.value = before + &after;
self.cursor = from;
self.selection_start = None;
} else if self.cursor > 0 {
let before: String = self.value.chars().take(self.cursor - 1).collect();
let after: String = self.value.chars().skip(self.cursor).collect();
self.value = before + &after;
self.cursor -= 1;
}
self.validate();
}
pub fn delete_forward(&mut self) {
if let Some(start) = self.selection_start {
let (from, to) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
let before: String = self.value.chars().take(from).collect();
let after: String = self.value.chars().skip(to).collect();
self.value = before + &after;
self.cursor = from;
self.selection_start = None;
} else if self.cursor < self.value.chars().count() {
let before: String = self.value.chars().take(self.cursor).collect();
let after: String = self.value.chars().skip(self.cursor + 1).collect();
self.value = before + &after;
}
self.validate();
}
pub fn move_left(&mut self, shift: bool) {
if shift {
if self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
}
} else {
self.selection_start = None;
}
if self.cursor > 0 {
self.cursor -= 1;
}
}
pub fn move_right(&mut self, shift: bool) {
if shift {
if self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
}
} else {
self.selection_start = None;
}
if self.cursor < self.value.chars().count() {
self.cursor += 1;
}
}
pub fn move_to_start(&mut self, shift: bool) {
if shift {
if self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
}
} else {
self.selection_start = None;
}
self.cursor = 0;
}
pub fn move_to_end(&mut self, shift: bool) {
if shift {
if self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
}
} else {
self.selection_start = None;
}
self.cursor = self.value.chars().count();
}
pub fn select_all(&mut self) {
self.selection_start = Some(0);
self.cursor = self.value.chars().count();
}
pub fn selected_text(&self) -> Option<String> {
self.selection_start.map(|start| {
let (from, to) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
self.value.chars().skip(from).take(to - from).collect()
})
}
pub fn move_word_left(&mut self, shift: bool) {
if shift && self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
} else if !shift {
self.selection_start = None;
}
self.cursor = crate::widgets::text_edit::word_boundary_left(&self.value, self.cursor);
}
pub fn move_word_right(&mut self, shift: bool) {
if shift && self.selection_start.is_none() {
self.selection_start = Some(self.cursor);
} else if !shift {
self.selection_start = None;
}
self.cursor = crate::widgets::text_edit::word_boundary_right(&self.value, self.cursor);
}
pub fn delete_word_backward(&mut self) {
if self.selection_start.is_some() {
self.delete_selection();
return;
}
let target = crate::widgets::text_edit::word_boundary_left(&self.value, self.cursor);
if target < self.cursor {
let byte_start = self
.value
.char_indices()
.nth(target)
.map(|(i, _)| i)
.unwrap_or(0);
let byte_end = self
.value
.char_indices()
.nth(self.cursor)
.map(|(i, _)| i)
.unwrap_or(self.value.len());
self.value = format!("{}{}", &self.value[..byte_start], &self.value[byte_end..]);
self.cursor = target;
}
}
pub fn delete_word_forward(&mut self) {
if self.selection_start.is_some() {
self.delete_selection();
return;
}
let target = crate::widgets::text_edit::word_boundary_right(&self.value, self.cursor);
if target > self.cursor {
let byte_start = self
.value
.char_indices()
.nth(self.cursor)
.map(|(i, _)| i)
.unwrap_or(self.value.len());
let byte_end = self
.value
.char_indices()
.nth(target)
.map(|(i, _)| i)
.unwrap_or(self.value.len());
self.value = format!("{}{}", &self.value[..byte_start], &self.value[byte_end..]);
}
}
pub fn delete_selection(&mut self) -> bool {
if let Some(start) = self.selection_start.take() {
let (from, to) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
let byte_from = self
.value
.char_indices()
.nth(from)
.map(|(i, _)| i)
.unwrap_or(0);
let byte_to = self
.value
.char_indices()
.nth(to)
.map(|(i, _)| i)
.unwrap_or(self.value.len());
self.value = format!("{}{}", &self.value[..byte_from], &self.value[byte_to..]);
self.cursor = from;
true
} else {
false
}
}
pub fn validate(&mut self) {
self.is_valid = match self.input_type {
InputType::Email => {
self.value.is_empty() || (self.value.contains('@') && self.value.contains('.'))
}
InputType::Number => self.value.is_empty() || self.value.parse::<f64>().is_ok(),
InputType::Integer => self.value.is_empty() || self.value.parse::<i64>().is_ok(),
InputType::Url => {
self.value.is_empty()
|| self.value.starts_with("http://")
|| self.value.starts_with("https://")
}
_ => true,
};
if self.constraints.required && self.value.is_empty() {
self.is_valid = false;
}
if let Some(min) = self.constraints.min_length {
if self.value.len() < min {
self.is_valid = false;
}
}
}
pub fn reset_cursor_blink(&mut self) {
if let Ok(mut cs) = self.cursor_state.lock() {
cs.reset_blink();
}
}
pub fn sync_global_selection(&self) {
if let Some(start) = self.selection_start {
if start != self.cursor {
let (from, to) = if start < self.cursor {
(start, self.cursor)
} else {
(self.cursor, start)
};
let selected: String = self.value.chars().skip(from).take(to - from).collect();
set_selection(selected, SelectionSource::TextInput, true);
} else {
clear_selection();
}
} else {
clear_selection();
}
}
pub fn cursor_position_from_x(&self, x: f32, font_size: f32) -> usize {
let display = self.display_text();
if display.is_empty() {
return 0;
}
let text_x = x + self.scroll_offset_x;
let char_count = display.chars().count();
let mut best_pos = 0;
let mut min_dist = f32::MAX;
for i in 0..=char_count {
let prefix: String = display.chars().take(i).collect();
let prefix_width = crate::text_measure::measure_text(&prefix, font_size).width;
let dist = (prefix_width - text_x).abs();
if dist < min_dist {
min_dist = dist;
best_pos = i;
}
}
best_pos
}
pub fn ensure_cursor_visible(&mut self, config: &TextInputConfig) {
let layout_width = self
.layout_bounds_storage
.lock()
.ok()
.and_then(|guard| guard.as_ref().map(|b| b.width));
let effective_computed_width = layout_width.or(self.computed_width);
if config.use_full_width && effective_computed_width.is_none() {
self.scroll_offset_x = 0.0;
return;
}
let display = self.display_text();
let total_text_width = if !display.is_empty() {
crate::text_measure::measure_text(&display, config.font_size).width
} else {
0.0
};
let cursor_x = if self.cursor > 0 && !display.is_empty() {
let text_before: String = display.chars().take(self.cursor).collect();
crate::text_measure::measure_text(&text_before, config.font_size).width
} else {
0.0
};
let base_width = effective_computed_width.unwrap_or(config.width);
let available_width = base_width - config.padding_x * 2.0 - config.border_width * 2.0;
let visible_right = self.scroll_offset_x + available_width;
let cursor_margin = 4.0;
if cursor_x > visible_right - cursor_margin {
self.scroll_offset_x = cursor_x - available_width + cursor_margin;
} else if cursor_x < self.scroll_offset_x {
self.scroll_offset_x = cursor_x;
}
self.scroll_offset_x = self.scroll_offset_x.max(0.0);
let max_scroll = (total_text_width - available_width + cursor_margin).max(0.0);
self.scroll_offset_x = self.scroll_offset_x.min(max_scroll);
}
}
pub fn text_input_data() -> SharedTextInputData {
Arc::new(Mutex::new(TextInputData::new()))
}
pub fn text_input_data_with_placeholder(placeholder: impl Into<String>) -> SharedTextInputData {
Arc::new(Mutex::new(TextInputData::with_placeholder(placeholder)))
}
pub type TextInputState = TextInputData;
pub type SharedTextInputState = SharedTextInputData;
pub fn text_input_state() -> SharedTextInputData {
text_input_data()
}
pub fn text_input_state_with_placeholder(placeholder: impl Into<String>) -> SharedTextInputData {
text_input_data_with_placeholder(placeholder)
}
fn apply_css_overrides(
cfg: &mut TextInputConfig,
stylesheet: &Stylesheet,
element_id: Option<&str>,
css_classes: &[String],
visual: &TextFieldState,
) {
let state = match visual {
TextFieldState::Hovered | TextFieldState::FocusedHovered => Some(ElementState::Hover),
TextFieldState::Focused => Some(ElementState::Focus),
TextFieldState::Disabled => Some(ElementState::Disabled),
TextFieldState::Idle => None,
};
for class in css_classes {
if let Some(base) = stylesheet.get_class(class) {
apply_style_to_config(cfg, base, visual);
}
if matches!(visual, TextFieldState::FocusedHovered) {
if let Some(s) = stylesheet.get_class_with_state(class, ElementState::Focus) {
apply_style_to_config(cfg, s, visual);
}
}
if let Some(s) = state {
if let Some(state_style) = stylesheet.get_class_with_state(class, s) {
apply_style_to_config(cfg, state_style, visual);
}
}
}
if let Some(element_id) = element_id {
if let Some(base) = stylesheet.get(element_id) {
apply_style_to_config(cfg, base, visual);
}
if matches!(visual, TextFieldState::FocusedHovered) {
if let Some(focus_style) = stylesheet.get_with_state(element_id, ElementState::Focus) {
apply_style_to_config(cfg, focus_style, visual);
}
}
if let Some(s) = state {
if let Some(state_style) = stylesheet.get_with_state(element_id, s) {
apply_style_to_config(cfg, state_style, visual);
}
}
if let Some(placeholder_style) = stylesheet.get_placeholder_style(element_id) {
if let Some(color) = placeholder_style.text_color {
cfg.placeholder_color = color;
}
if let Some(color) = placeholder_style.placeholder_color {
cfg.placeholder_color = color;
}
}
}
}
fn apply_style_to_config(
cfg: &mut TextInputConfig,
style: &crate::element_style::ElementStyle,
visual: &TextFieldState,
) {
if let Some(ref bg) = style.background {
let color = match bg {
blinc_core::Brush::Solid(c) => *c,
_ => return, };
match visual {
TextFieldState::Idle => cfg.bg_color = color,
TextFieldState::Hovered => cfg.hover_bg_color = color,
TextFieldState::Focused | TextFieldState::FocusedHovered => {
cfg.focused_bg_color = color;
}
TextFieldState::Disabled => {} }
}
if let Some(color) = style.border_color {
match visual {
TextFieldState::Idle => cfg.border_color = color,
TextFieldState::Hovered => cfg.hover_border_color = color,
TextFieldState::Focused | TextFieldState::FocusedHovered => {
cfg.focused_border_color = color;
}
TextFieldState::Disabled => {}
}
}
if let Some(w) = style.border_width {
cfg.border_width = w;
}
if let Some(cr) = style.corner_radius {
cfg.corner_radius = cr.top_left; }
if let Some(color) = style.text_color {
cfg.text_color = color;
}
if let Some(size) = style.font_size {
cfg.font_size = size;
}
if let Some(color) = style.caret_color {
cfg.cursor_color = color;
}
if let Some(color) = style.selection_color {
cfg.selection_color = color;
}
if let Some(color) = style.placeholder_color {
cfg.placeholder_color = color;
}
}
fn extract_outline_from_stylesheet(
stylesheet: &Stylesheet,
element_id: &str,
visual: &TextFieldState,
) -> Option<(f32, Color, f32)> {
let mut width = None;
let mut color = None;
let mut offset = None;
if let Some(base) = stylesheet.get(element_id) {
if let Some(w) = base.outline_width {
width = Some(w);
}
if let Some(c) = base.outline_color {
color = Some(c);
}
if let Some(o) = base.outline_offset {
offset = Some(o);
}
}
let state = match visual {
TextFieldState::Hovered | TextFieldState::FocusedHovered => Some(ElementState::Hover),
TextFieldState::Focused => Some(ElementState::Focus),
TextFieldState::Disabled => Some(ElementState::Disabled),
TextFieldState::Idle => None,
};
if matches!(visual, TextFieldState::FocusedHovered) {
if let Some(focus_style) = stylesheet.get_with_state(element_id, ElementState::Focus) {
if let Some(w) = focus_style.outline_width {
width = Some(w);
}
if let Some(c) = focus_style.outline_color {
color = Some(c);
}
if let Some(o) = focus_style.outline_offset {
offset = Some(o);
}
}
}
if let Some(s) = state {
if let Some(state_style) = stylesheet.get_with_state(element_id, s) {
if let Some(w) = state_style.outline_width {
width = Some(w);
}
if let Some(c) = state_style.outline_color {
color = Some(c);
}
if let Some(o) = state_style.outline_offset {
offset = Some(o);
}
}
}
width.map(|w| {
(
w,
color.unwrap_or(Color::rgba(0.23, 0.51, 0.97, 0.5)),
offset.unwrap_or(0.0),
)
})
}
#[derive(Clone, Debug)]
pub struct TextInputConfig {
pub width: f32,
pub height: f32,
pub use_full_width: bool,
pub font_size: f32,
pub text_color: Color,
pub placeholder_color: Color,
pub bg_color: Color,
pub hover_bg_color: Color,
pub focused_bg_color: Color,
pub border_color: Color,
pub hover_border_color: Color,
pub focused_border_color: Color,
pub error_border_color: Color,
pub cursor_color: Color,
pub selection_color: Color,
pub corner_radius: f32,
pub border_width: f32,
pub padding_x: f32,
pub placeholder: String,
}
impl Default for TextInputConfig {
fn default() -> Self {
let theme = ThemeState::get();
Self {
width: 200.0,
height: 44.0,
use_full_width: false,
font_size: 16.0,
text_color: theme.color(ColorToken::TextPrimary),
placeholder_color: theme.color(ColorToken::TextTertiary),
bg_color: theme.color(ColorToken::InputBg),
hover_bg_color: theme.color(ColorToken::InputBgHover),
focused_bg_color: theme.color(ColorToken::InputBgFocus),
border_color: theme.color(ColorToken::BorderSecondary),
hover_border_color: theme.color(ColorToken::BorderHover),
focused_border_color: theme.color(ColorToken::BorderFocus),
error_border_color: theme.color(ColorToken::BorderError),
cursor_color: theme.color(ColorToken::Accent),
selection_color: theme.color(ColorToken::Selection),
corner_radius: 8.0,
border_width: 1.5,
padding_x: 12.0,
placeholder: String::new(),
}
}
}
pub type OnChangeCallback = Arc<dyn Fn(&str) + Send + Sync>;
pub struct TextInput {
inner: Stateful<TextFieldState>,
data: SharedTextInputData,
config: Arc<Mutex<TextInputConfig>>,
stateful_state: SharedState<TextFieldState>,
on_change_callback: Option<OnChangeCallback>,
}
impl TextInput {
pub fn new(data: SharedTextInputData) -> Self {
let config = Arc::new(Mutex::new(TextInputConfig::default()));
let (initial_visual, existing_stateful_state) = {
let d = data.lock().unwrap();
(d.visual, d.stateful_state.clone())
};
let stateful_state: SharedState<TextFieldState> =
existing_stateful_state.unwrap_or_else(|| {
let new_state = Arc::new(Mutex::new(StatefulInner::new(initial_visual)));
if let Ok(mut d) = data.lock() {
d.stateful_state = Some(Arc::clone(&new_state));
}
new_state
});
{
let mut shared = stateful_state.lock().unwrap();
shared.node_id = None;
}
let mut inner = Self::create_inner_with_handlers(
Arc::clone(&stateful_state),
Arc::clone(&data),
Arc::clone(&config),
);
{
let cfg = config.lock().unwrap();
if cfg.use_full_width {
inner = inner.w_full();
}
inner = inner.h(cfg.height).min_w(0.0);
}
{
let config_for_callback = Arc::clone(&config);
let data_for_callback = Arc::clone(&data);
let mut shared = stateful_state.lock().unwrap();
shared.state_callback = Some(Arc::new(
move |visual: &TextFieldState, container: &mut Div| {
let mut cfg = config_for_callback.lock().unwrap().clone();
let mut data_guard = data_for_callback.lock().unwrap();
let has_css_target =
data_guard.css_element_id.is_some() || !data_guard.css_classes.is_empty();
let css_outline = if has_css_target {
if let Some(stylesheet) = active_stylesheet() {
apply_css_overrides(
&mut cfg,
&stylesheet,
data_guard.css_element_id.as_deref(),
&data_guard.css_classes,
visual,
);
if let Some(ref element_id) = data_guard.css_element_id {
extract_outline_from_stylesheet(&stylesheet, element_id, visual)
} else {
None
}
} else {
None
}
} else {
None
};
let old_scroll = data_guard.scroll_offset_x;
data_guard.ensure_cursor_visible(&cfg);
if data_guard.scroll_offset_x != old_scroll {
tracing::debug!(
"TextInput scroll changed: {} -> {} (cursor={}, text_len={})",
old_scroll,
data_guard.scroll_offset_x,
data_guard.cursor,
data_guard.value.len()
);
}
let (bg, border_color) = match visual {
TextFieldState::Idle => (cfg.bg_color, cfg.border_color),
TextFieldState::Hovered => (cfg.hover_bg_color, cfg.hover_border_color),
TextFieldState::Focused | TextFieldState::FocusedHovered => {
(cfg.focused_bg_color, cfg.focused_border_color)
}
TextFieldState::Disabled => (
Color::rgba(0.12, 0.12, 0.15, 0.5),
Color::rgba(0.25, 0.25, 0.3, 0.5),
),
};
let border_color = if !data_guard.is_valid && !data_guard.value.is_empty() {
cfg.error_border_color
} else {
border_color
};
let mut inner = div()
.w_full()
.bg(bg)
.border(cfg.border_width, border_color)
.rounded(cfg.corner_radius);
if let Some((width, color, offset)) = css_outline {
inner = inner
.outline_width(width)
.outline_color(color)
.outline_offset(offset);
}
let content = TextInput::build_content(*visual, &data_guard, &cfg);
container.merge(inner.child(content));
},
));
shared.needs_visual_update = true;
}
inner.ensure_state_handlers_registered();
Self {
inner,
data,
config,
stateful_state,
on_change_callback: None,
}
}
fn create_inner_with_handlers(
stateful_state: SharedState<TextFieldState>,
data: SharedTextInputData,
config: Arc<Mutex<TextInputConfig>>,
) -> Stateful<TextFieldState> {
use blinc_core::events::event_types;
let data_for_click = Arc::clone(&data);
let data_for_drag = Arc::clone(&data);
let config_for_drag = Arc::clone(&config);
let stateful_for_drag = Arc::clone(&stateful_state);
let data_for_text = Arc::clone(&data);
let data_for_key = Arc::clone(&data);
let config_for_click = Arc::clone(&config);
let stateful_for_click = Arc::clone(&stateful_state);
let stateful_for_text = Arc::clone(&stateful_state);
let stateful_for_key = Arc::clone(&stateful_state);
Stateful::with_shared_state(stateful_state)
.w_full()
.on_mouse_down(move |ctx| {
let needs_refresh = {
let mut d = match data_for_click.lock() {
Ok(d) => d,
Err(_) => return,
};
if d.disabled {
return;
}
let font_size = config_for_click.lock().unwrap().font_size;
{
let mut shared = stateful_for_click.lock().unwrap();
if !shared.state.is_focused() {
if let Some(new_state) = shared
.state
.on_event(event_types::POINTER_DOWN)
.or_else(|| shared.state.on_event(event_types::FOCUS))
{
shared.state = new_state;
shared.needs_visual_update = true;
}
}
}
if !d.visual.is_focused() {
d.visual = TextFieldState::Focused;
d.focus_time_ms = elapsed_ms();
d.reset_cursor_blink();
increment_focus_count();
set_focused_text_input(&data_for_click);
request_continuous_redraw();
}
if ctx.bounds_width > 0.0 {
d.computed_width = Some(ctx.bounds_width);
}
let text_x = ctx.local_x.max(0.0);
let cursor_pos = d.cursor_position_from_x(text_x, font_size);
let now = std::time::Instant::now();
let is_double_click = d
.last_click_time
.map(|t| now.duration_since(t).as_millis() < 400)
.unwrap_or(false);
d.last_click_time = Some(now);
if is_double_click {
let (start, end) =
crate::widgets::text_edit::word_at_position(&d.value, cursor_pos);
d.selection_start = Some(start);
d.cursor = end;
} else {
d.cursor = cursor_pos;
d.selection_start = None;
d.drag_select_anchor = Some(cursor_pos);
}
d.reset_cursor_blink();
true
};
if needs_refresh {
refresh_stateful(&stateful_for_click);
}
})
.on_drag({
move |ctx| {
let needs_refresh = {
let mut d = match data_for_drag.lock() {
Ok(d) => d,
Err(_) => return,
};
if !d.visual.is_focused() {
return;
}
let font_size = config_for_drag.lock().unwrap().font_size;
let text_x = ctx.local_x.max(0.0);
let new_pos = d.cursor_position_from_x(text_x, font_size);
if let Some(anchor) = d.drag_select_anchor {
if new_pos != anchor {
d.selection_start = Some(anchor);
d.cursor = new_pos;
}
}
true
};
if needs_refresh {
refresh_stateful(&stateful_for_drag);
}
}
})
.on_event(event_types::TEXT_INPUT, move |ctx| {
let (needs_refresh, callback_info) = {
let mut d = match data_for_text.lock() {
Ok(d) => d,
Err(_) => return,
};
if d.disabled || !d.visual.is_focused() {
return;
}
if let Some(c) = ctx.key_char {
d.insert(&c.to_string());
d.reset_cursor_blink();
tracing::debug!("TextInput received char: {:?}, value: {}", c, d.value);
let cb_info = d
.on_change_callback
.as_ref()
.map(|cb| (Arc::clone(cb), d.value.clone()));
(true, cb_info)
} else {
(false, None)
}
};
if needs_refresh {
refresh_stateful(&stateful_for_text);
}
if let Some((callback, new_value)) = callback_info {
callback(&new_value);
}
})
.on_key_down(move |ctx| {
let (needs_refresh, callback_info) = {
let mut d = match data_for_key.lock() {
Ok(d) => d,
Err(_) => return,
};
if d.disabled || !d.visual.is_focused() {
return;
}
let mut changed = true;
let mut should_blur = false;
let mut value_changed = false;
let mod_key = ctx.meta || ctx.ctrl;
match ctx.key_code {
8 if mod_key => {
d.delete_word_backward();
value_changed = true;
}
8 => {
if d.selection_start.is_some() {
d.delete_selection();
} else {
d.delete_backward();
}
value_changed = true;
}
127 if mod_key => {
d.delete_word_forward();
value_changed = true;
}
127 => {
if d.selection_start.is_some() {
d.delete_selection();
} else {
d.delete_forward();
}
value_changed = true;
}
37 if mod_key => d.move_word_left(ctx.shift), 39 if mod_key => d.move_word_right(ctx.shift), 37 => d.move_left(ctx.shift), 39 => d.move_right(ctx.shift), 36 => d.move_to_start(ctx.shift), 35 => d.move_to_end(ctx.shift), 27 => {
should_blur = true;
}
_ if mod_key => {
match ctx.key_code {
65 => d.select_all(),
67 => {
if let Some(text) = d.selected_text() {
crate::widgets::text_edit::clipboard_write(&text);
}
changed = true;
}
88 => {
if let Some(text) = d.selected_text() {
crate::widgets::text_edit::clipboard_write(&text);
d.delete_selection();
value_changed = true;
}
}
86 => {
if let Some(clip) = crate::widgets::text_edit::clipboard_read()
{
let clean: String = clip
.chars()
.filter(|c| *c != '\n' && *c != '\r')
.collect();
if !clean.is_empty() {
if d.selection_start.is_some() {
d.delete_selection();
}
d.insert(&clean);
value_changed = true;
}
}
}
_ => changed = false,
}
}
_ => changed = false,
}
if changed && !should_blur {
d.reset_cursor_blink();
d.sync_global_selection();
}
let cb_info = if value_changed {
d.on_change_callback
.as_ref()
.map(|cb| (Arc::clone(cb), d.value.clone()))
} else {
None
};
((changed, should_blur), cb_info)
};
if needs_refresh.1 {
blur_all_text_inputs();
} else if needs_refresh.0 {
refresh_stateful(&stateful_for_key);
}
if let Some((callback, new_value)) = callback_info {
callback(&new_value);
}
})
.cursor_text()
}
fn build_content(
visual: TextFieldState,
data: &TextInputData,
config: &TextInputConfig,
) -> Div {
let display = if data.value.is_empty() {
if !data.placeholder.is_empty() {
data.placeholder.clone()
} else {
config.placeholder.clone()
}
} else {
data.display_text()
};
let text_color = if data.value.is_empty() {
config.placeholder_color
} else if data.disabled {
Color::rgba(0.4, 0.4, 0.4, 1.0)
} else {
config.text_color
};
let is_focused = visual.is_focused();
let cursor_color = config.cursor_color;
let selection_color = config.selection_color;
let cursor_pos = data.cursor;
let cursor_height = config.font_size * 1.2;
let scroll_offset = data.scroll_offset_x;
let selection_range: Option<(usize, usize)> = data.selection_start.map(|start| {
if start < cursor_pos {
(start, cursor_pos)
} else {
(cursor_pos, start)
}
});
let cursor_state_for_canvas = Arc::clone(&data.cursor_state);
let cursor_x = if cursor_pos > 0 && !display.is_empty() {
let text_before: String = display.chars().take(cursor_pos).collect();
crate::text_measure::measure_text(&text_before, config.font_size).width
} else {
0.0
};
let inner_height = config.height - config.border_width * 2.0;
let mut main_content = div().h_full().w_full().relative().flex_row().items_center();
main_content =
main_content.child(div().w(config.padding_x).h(inner_height).flex_shrink_0());
let mut clip_container = div()
.h(inner_height)
.relative()
.overflow_clip()
.flex_1()
.min_w(0.0);
let mut text_wrapper = div()
.absolute()
.left(-scroll_offset)
.top(0.0)
.h(inner_height)
.flex_row()
.items_center();
if !display.is_empty() {
if let Some((sel_start, sel_end)) = selection_range {
let mut text_container = div().flex_row().items_center();
let before_sel: String = display.chars().take(sel_start).collect();
if !before_sel.is_empty() {
text_container = text_container.child(
text(&before_sel)
.size(config.font_size)
.color(text_color)
.text_left()
.no_wrap()
.v_center(),
);
}
let selected: String = display
.chars()
.skip(sel_start)
.take(sel_end - sel_start)
.collect();
if !selected.is_empty() {
text_container = text_container.child(
div()
.bg(selection_color)
.rounded(config.corner_radius)
.child(
text(&selected)
.size(config.font_size)
.color(text_color)
.text_left()
.no_wrap()
.v_center(),
),
);
}
let after_sel: String = display.chars().skip(sel_end).collect();
if !after_sel.is_empty() {
text_container = text_container.child(
text(&after_sel)
.size(config.font_size)
.color(text_color)
.text_left()
.no_wrap()
.v_center(),
);
}
text_wrapper = text_wrapper.child(text_container);
} else {
text_wrapper = text_wrapper.child(
text(&display)
.size(config.font_size)
.color(text_color)
.text_left()
.no_wrap()
.v_center(),
);
}
}
clip_container = clip_container.child(text_wrapper);
if is_focused && selection_range.is_none() {
let cursor_left = cursor_x - scroll_offset;
let cursor_margin = (inner_height - cursor_height) / 2.0;
{
if let Ok(mut cs) = cursor_state_for_canvas.lock() {
cs.visible = true;
cs.color = cursor_color;
cs.x = cursor_left;
cs.animation = CursorAnimation::SmoothFade;
}
}
let cursor_state_clone = Arc::clone(&cursor_state_for_canvas);
let cursor_canvas = canvas(
move |ctx: &mut dyn blinc_core::DrawContext,
bounds: crate::canvas::CanvasBounds| {
let cs = cursor_state_clone.lock().unwrap();
if !cs.visible {
return;
}
let opacity = cs.current_opacity();
if opacity < 0.01 {
return;
}
let color = blinc_core::Color::rgba(
cs.color.r,
cs.color.g,
cs.color.b,
cs.color.a * opacity,
);
ctx.fill_rect(
blinc_core::Rect::new(0.0, 0.0, cs.width, bounds.height),
blinc_core::CornerRadius::default(),
blinc_core::Brush::Solid(color),
);
},
)
.absolute()
.left(cursor_left)
.top(cursor_margin)
.w(2.0)
.h(cursor_height);
clip_container = clip_container.child(cursor_canvas);
} else if let Ok(mut cs) = cursor_state_for_canvas.lock() {
cs.visible = false;
}
main_content = main_content.child(clip_container);
main_content =
main_content.child(div().w(config.padding_x).h(inner_height).flex_shrink_0());
main_content
}
pub fn w(mut self, px: f32) -> Self {
{
let mut cfg = self.config.lock().unwrap();
cfg.width = px;
}
self.inner = std::mem::take(&mut self.inner).w(px);
self
}
pub fn w_full(mut self) -> Self {
self.config.lock().unwrap().use_full_width = true;
self.inner = std::mem::take(&mut self.inner).w_full();
self
}
pub fn min_w(mut self, px: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).min_w(px);
self
}
pub fn h(mut self, px: f32) -> Self {
{
let mut cfg = self.config.lock().unwrap();
cfg.height = px;
}
self.inner = std::mem::take(&mut self.inner).h(px);
self
}
pub fn placeholder(self, text: impl Into<String>) -> Self {
let placeholder = text.into();
self.config.lock().unwrap().placeholder = placeholder.clone();
if let Ok(mut d) = self.data.lock() {
d.placeholder = placeholder;
}
self
}
pub fn input_type(self, input_type: InputType) -> Self {
if let Ok(mut d) = self.data.lock() {
d.input_type = input_type;
}
self
}
pub fn disabled(self, disabled: bool) -> Self {
if let Ok(mut d) = self.data.lock() {
d.disabled = disabled;
if disabled {
d.visual = TextFieldState::Disabled;
}
}
self
}
pub fn masked(self, masked: bool) -> Self {
if let Ok(mut d) = self.data.lock() {
d.masked = masked;
}
self
}
pub fn max_length(self, max: usize) -> Self {
if let Ok(mut d) = self.data.lock() {
d.constraints.max_length = Some(max);
}
self
}
pub fn text_size(self, size: f32) -> Self {
self.config.lock().unwrap().font_size = size;
self
}
pub fn rounded(mut self, radius: f32) -> Self {
self.config.lock().unwrap().corner_radius = radius;
self.inner = std::mem::take(&mut self.inner).rounded(radius);
self
}
pub fn border(mut self, width: f32, color: blinc_core::Color) -> Self {
self.inner = std::mem::take(&mut self.inner).border(width, color);
self
}
pub fn border_color(mut self, color: blinc_core::Color) -> Self {
self.inner = std::mem::take(&mut self.inner).border_color(color);
self
}
pub fn border_width(mut self, width: f32) -> Self {
self.inner = std::mem::take(&mut self.inner).border_width(width);
self
}
pub fn shadow_sm(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).shadow_sm();
self
}
pub fn shadow_md(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).shadow_md();
self
}
pub fn flex_grow(mut self) -> Self {
self.inner = std::mem::take(&mut self.inner).flex_grow();
self
}
pub fn id(mut self, id: &str) -> Self {
if let Ok(mut d) = self.data.lock() {
d.css_element_id = Some(id.to_string());
}
self.inner = std::mem::take(&mut self.inner).id(id);
self
}
pub fn class(mut self, name: &str) -> Self {
if let Ok(mut d) = self.data.lock() {
d.css_classes.push(name.to_string());
}
self.inner = std::mem::take(&mut self.inner).class(name);
self
}
pub fn idle_border_color(self, color: Color) -> Self {
self.config.lock().unwrap().border_color = color;
self
}
pub fn hover_border_color(self, color: Color) -> Self {
self.config.lock().unwrap().hover_border_color = color;
self
}
pub fn focused_border_color(self, color: Color) -> Self {
self.config.lock().unwrap().focused_border_color = color;
self
}
pub fn error_border_color(self, color: Color) -> Self {
self.config.lock().unwrap().error_border_color = color;
self
}
pub fn border_colors(self, idle: Color, hover: Color, focused: Color, error: Color) -> Self {
let mut cfg = self.config.lock().unwrap();
cfg.border_color = idle;
cfg.hover_border_color = hover;
cfg.focused_border_color = focused;
cfg.error_border_color = error;
drop(cfg);
self
}
pub fn idle_bg_color(self, color: Color) -> Self {
self.config.lock().unwrap().bg_color = color;
self
}
pub fn hover_bg_color(self, color: Color) -> Self {
self.config.lock().unwrap().hover_bg_color = color;
self
}
pub fn focused_bg_color(self, color: Color) -> Self {
self.config.lock().unwrap().focused_bg_color = color;
self
}
pub fn bg_colors(self, idle: Color, hover: Color, focused: Color) -> Self {
let mut cfg = self.config.lock().unwrap();
cfg.bg_color = idle;
cfg.hover_bg_color = hover;
cfg.focused_bg_color = focused;
drop(cfg);
self
}
pub fn text_color(self, color: Color) -> Self {
self.config.lock().unwrap().text_color = color;
self
}
pub fn placeholder_color(self, color: Color) -> Self {
self.config.lock().unwrap().placeholder_color = color;
self
}
pub fn cursor_color(self, color: Color) -> Self {
self.config.lock().unwrap().cursor_color = color;
self
}
pub fn selection_color(self, color: Color) -> Self {
self.config.lock().unwrap().selection_color = color;
self
}
pub fn on_change<F>(mut self, callback: F) -> Self
where
F: Fn(&str) + Send + Sync + 'static,
{
let cb: OnChangeCallback = Arc::new(callback);
self.on_change_callback = Some(Arc::clone(&cb));
if let Ok(mut d) = self.data.lock() {
d.on_change_callback = Some(cb);
}
self
}
}
pub fn text_input(data: &SharedTextInputData) -> TextInput {
TextInput::new(Arc::clone(data))
}
impl ElementBuilder for TextInput {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
{
let mut shared = self.stateful_state.lock().unwrap();
shared.base_render_props = Some(self.inner.inner_render_props());
shared.base_style = self.inner.inner_layout_style();
}
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn element_type_id(&self) -> crate::div::ElementTypeId {
crate::div::ElementTypeId::Div
}
fn semantic_type_name(&self) -> Option<&'static str> {
Some("input")
}
fn event_handlers(&self) -> Option<&crate::event_handler::EventHandlers> {
self.inner.event_handlers()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn layout_bounds_storage(&self) -> Option<crate::renderer::LayoutBoundsStorage> {
if let Ok(data) = self.data.lock() {
Some(Arc::clone(&data.layout_bounds_storage))
} else {
None
}
}
fn layout_bounds_callback(&self) -> Option<crate::renderer::LayoutBoundsCallback> {
let stateful_state = Arc::clone(&self.stateful_state);
Some(Arc::new(move |_bounds| {
refresh_stateful(&stateful_state);
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_input_data_insert() {
let mut data = TextInputData::new();
data.stateful_state = None;
data.insert("hello");
assert_eq!(data.value, "hello");
assert_eq!(data.cursor, 5);
data.cursor = 0;
data.insert("world ");
assert_eq!(data.value, "world hello");
}
#[test]
fn test_text_input_data_delete() {
let mut data = TextInputData::with_value("hello");
data.stateful_state = None;
data.cursor = 5;
data.delete_backward();
assert_eq!(data.value, "hell");
data.cursor = 0;
data.delete_forward();
assert_eq!(data.value, "ell");
}
#[test]
fn test_input_type_filtering() {
let mut data = TextInputData::new();
data.stateful_state = None;
data.input_type = InputType::Number;
data.insert("123.45");
assert_eq!(data.value, "123.45");
data.value.clear();
data.cursor = 0;
data.insert("abc123");
assert_eq!(data.value, "123");
}
}