use std::rc::Rc;
use std::time::{Duration, Instant};
use crate::animation::Animation;
use parley::BoundingBox;
use crate::clipboard;
use crate::event::{Event, EventContext, Key, MouseButton};
use crate::geometry::Color;
use crate::layout::style::FlexStyle;
use crate::layout::types::FlexAlign;
use crate::text::{
PlainTextEditor, TextEngine, align_to_utf8_boundary, apply_plain_editor_style,
create_plain_editor, editor_cursor_geometry, with_text_contexts,
};
use crate::theme;
use crate::view::node::{Listener, ViewNode};
use crate::view::paint::{PaintStyle, TextStyle};
use crate::widget::layout::LayoutAttr;
use crate::widget::{BuildContext, Stateful, Widget};
pub struct InputState {
pub editor: PlainTextEditor,
pub focused: bool,
pub selecting: bool,
pub last_activity: Option<Instant>,
anim: Option<Animation>,
}
const BLINK_SOLID: Duration = Duration::from_millis(500);
const BLINK_HALF: Duration = Duration::from_millis(530);
const BLINK_INTERVAL: Duration = BLINK_HALF;
impl InputState {
pub fn new(style: &TextStyle) -> Self {
Self {
editor: create_plain_editor(style),
focused: false,
selecting: false,
last_activity: None,
anim: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InputStatus {
#[default]
Default,
Success,
Warning,
Danger,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum InputSize {
Sm,
#[default]
Md,
Lg,
}
pub struct Input {
placeholder: String,
layout: LayoutAttr,
text_style: TextStyle,
paint: PaintStyle,
padding_h: Option<f32>,
padding_v: Option<f32>,
status: InputStatus,
size: InputSize,
multiline: bool,
on_change: Option<Rc<dyn Fn(String)>>,
on_submit: Option<Rc<dyn Fn(String)>>,
on_focus: Option<Rc<dyn Fn()>>,
on_blur: Option<Rc<dyn Fn()>>,
listeners: Vec<Listener>,
}
impl Input {
pub fn new(placeholder: impl Into<String>) -> Self {
let t = theme::current();
Self {
placeholder: placeholder.into(),
layout: LayoutAttr::new().width(200.0),
text_style: TextStyle {
font_size: 0.0,
color: t.text.regular_default,
wrap: false,
text_align: crate::view::paint::TextAlign::Start,
..TextStyle::default()
},
paint: PaintStyle::new()
.background(t.background.primary_default)
.border(1.0, t.border.default)
.radius(t.radius.small),
padding_h: None,
padding_v: None,
status: InputStatus::Default,
size: InputSize::Md,
multiline: false,
on_change: None,
on_submit: None,
on_focus: None,
on_blur: None,
listeners: Vec::new(),
}
}
pub fn multiline(mut self, v: bool) -> Self {
self.multiline = v;
self
}
pub fn width(mut self, w: f32) -> Self {
self.layout = self.layout.width(w.max(0.0));
self
}
pub fn height(mut self, h: f32) -> Self {
self.layout = self.layout.height(h.max(0.0));
self
}
pub fn layout(mut self, l: LayoutAttr) -> Self {
self.layout = l;
self
}
pub fn padding_h(mut self, v: f32) -> Self {
self.padding_h = Some(v.max(0.0));
self
}
pub fn padding_v(mut self, v: f32) -> Self {
self.padding_v = Some(v.max(0.0));
self
}
pub fn status(mut self, s: InputStatus) -> Self {
self.status = s;
self
}
pub fn size(mut self, s: InputSize) -> Self {
self.size = s;
self
}
pub fn font_size(mut self, s: f64) -> Self {
self.text_style.font_size = s;
self
}
pub fn color(mut self, c: Color) -> Self {
self.text_style.color = c;
self
}
pub fn text_style(mut self, s: TextStyle) -> Self {
self.text_style = s;
self
}
pub fn placeholder_color(mut self, c: Color) -> Self {
self.text_style.color = c;
self
}
pub fn background(mut self, c: Color) -> Self {
self.paint.background_color = Some(c);
self
}
pub fn border_color(mut self, c: Color) -> Self {
self.paint.border_color = Some(c);
self
}
pub fn radius(mut self, r: f32) -> Self {
self.paint.border_radius = r;
self
}
pub fn on_change<F: Fn(String) + 'static>(mut self, f: F) -> Self {
self.on_change = Some(Rc::new(f));
self
}
pub fn on_submit<F: Fn(String) + 'static>(mut self, f: F) -> Self {
self.on_submit = Some(Rc::new(f));
self
}
pub fn on_focus<F: Fn() + 'static>(mut self, f: F) -> Self {
self.on_focus = Some(Rc::new(f));
self
}
pub fn on_blur<F: Fn() + 'static>(mut self, f: F) -> Self {
self.on_blur = Some(Rc::new(f));
self
}
pub fn on_click<F: Fn() + 'static>(mut self, f: F) -> Self {
self.listeners.push(Listener::on_click(Rc::new(f)));
self
}
pub fn on_click_with_ctx<F: Fn(&mut EventContext) + 'static>(mut self, f: F) -> Self {
self.listeners.push(Listener::on_click_with_ctx(Rc::new(f)));
self
}
fn focus_listener(
state: &Stateful<InputState>,
focused: bool,
on_focus: Option<Rc<dyn Fn()>>,
on_blur: Option<Rc<dyn Fn()>>,
) -> Listener {
let state = state.clone();
let event = if focused {
crate::event::EventType::FocusIn
} else {
crate::event::EventType::FocusOut
};
Listener {
event,
kind: crate::view::node::ListenerKind::BuiltIn,
callback: crate::view::node::Callback::WithCtx(Rc::new(
move |ctx: &mut EventContext| {
state.update(|s| {
s.focused = focused;
let now = Instant::now();
if focused {
s.last_activity = Some(now);
if s.anim.is_none() {
s.anim = Some(Animation::new(BLINK_INTERVAL, || {
crate::state::request_rebuild();
}));
}
if let Some(cb) = &on_focus {
cb();
}
} else {
s.anim = None;
s.last_activity = None;
if let Some(cb) = &on_blur {
cb();
}
}
});
ctx.request_render();
},
)),
}
}
fn key_listener(&self, state: &Stateful<InputState>) -> Listener {
let state = state.clone();
let on_change = self.on_change.clone();
let on_submit = self.on_submit.clone();
let multiline = self.multiline;
Listener::on_key_down(Rc::new(move |ctx: &mut EventContext| {
let Some(Event::KeyDown { key, modifiers }) = ctx.event() else {
return;
};
let key = *key;
let modifiers = *modifiers;
let mut text_changed = false;
{
let mut st = state.get_mut();
st.last_activity = Some(Instant::now());
let editor = &mut st.editor;
match key {
Key::Backspace => {
with_text_contexts(|fc, lc| editor.driver(fc, lc).backdelete());
text_changed = true;
}
Key::Delete => {
with_text_contexts(|fc, lc| editor.driver(fc, lc).delete());
text_changed = true;
}
Key::ArrowLeft => {
if modifiers.ctrl && modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_word_left());
} else if modifiers.ctrl {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_word_left());
} else if modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_left());
} else {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_left());
}
}
Key::ArrowRight => {
if modifiers.ctrl && modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_word_right());
} else if modifiers.ctrl {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_word_right());
} else if modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_right());
} else {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_right());
}
}
Key::ArrowUp => {
if modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_up());
} else {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_up());
}
}
Key::ArrowDown => {
if modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_down());
} else {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_down());
}
}
Key::Home => {
if modifiers.shift {
with_text_contexts(|fc, lc| {
editor.driver(fc, lc).select_to_text_start()
});
} else {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_to_text_start());
}
}
Key::End => {
if modifiers.shift {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_to_text_end());
} else {
with_text_contexts(|fc, lc| editor.driver(fc, lc).move_to_text_end());
}
}
Key::Character(c) => {
if modifiers.ctrl && (c == 'a' || c == 'A') {
with_text_contexts(|fc, lc| editor.driver(fc, lc).select_all());
} else if modifiers.ctrl && (c == 'c' || c == 'C') {
if let Some(selected) = editor.selected_text() {
clipboard::copy_text(selected);
}
} else if modifiers.ctrl && (c == 'x' || c == 'X') {
if let Some(selected) = editor.selected_text() {
clipboard::copy_text(selected);
with_text_contexts(|fc, lc| {
editor.driver(fc, lc).delete_selection()
});
text_changed = true;
}
} else if modifiers.ctrl && (c == 'v' || c == 'V') {
if let Some(text) = clipboard::paste_text() {
with_text_contexts(|fc, lc| {
editor.driver(fc, lc).insert_or_replace_selection(&text)
});
text_changed = true;
}
} else if !modifiers.ctrl && !modifiers.alt && !modifiers.meta {
let s = c.to_string();
with_text_contexts(|fc, lc| {
editor.driver(fc, lc).insert_or_replace_selection(&s)
});
text_changed = true;
}
}
Key::Enter => {
if multiline {
with_text_contexts(|fc, lc| {
editor.driver(fc, lc).insert_or_replace_selection("\n")
});
text_changed = true;
} else if let Some(cb) = &on_submit {
let text = editor.raw_text().to_string();
cb(text);
}
}
Key::Escape => {
with_text_contexts(|fc, lc| editor.driver(fc, lc).collapse_selection());
}
_ => {}
}
}
if text_changed && let Some(cb) = &on_change {
let text = state.get().editor.raw_text().to_string();
cb(text);
}
ctx.request_render();
ctx.request_rebuild();
}))
.builtin()
}
fn ime_preedit_listener(state: &Stateful<InputState>) -> Listener {
let state = state.clone();
Listener::on_ime_preedit(Rc::new(move |ctx: &mut EventContext| {
let Some(Event::ImePreedit {
text,
cursor_start,
cursor_end,
}) = ctx.event()
else {
return;
};
let text = text.clone();
state.update(|s| {
with_text_contexts(|fc, lc| {
let mut driver = s.editor.driver(fc, lc);
if text.is_empty() {
driver.clear_compose();
} else {
let start = cursor_start
.map(|i| align_to_utf8_boundary(&text, i))
.unwrap_or(text.len());
let end = cursor_end
.map(|i| align_to_utf8_boundary(&text, i))
.unwrap_or(start);
let start = start.min(end);
let end = end.max(start);
driver.set_compose(&text, Some((start, end)));
}
});
});
ctx.request_render();
ctx.request_rebuild();
}))
.builtin()
}
fn ime_commit_listener(state: &Stateful<InputState>) -> Listener {
let state = state.clone();
Listener::on_ime_commit(Rc::new(move |ctx: &mut EventContext| {
let Some(Event::ImeCommit { text }) = ctx.event() else {
return;
};
let text = text.clone();
state.update(|s| {
with_text_contexts(|fc, lc| {
let mut driver = s.editor.driver(fc, lc);
driver.clear_compose();
driver.insert_or_replace_selection(&text);
});
});
ctx.request_render();
ctx.request_rebuild();
}))
.builtin()
}
fn ime_disabled_listener(state: &Stateful<InputState>) -> Listener {
let state = state.clone();
Listener::on_ime_disabled(Rc::new(move |ctx: &mut EventContext| {
state.update(|s| {
with_text_contexts(|fc, lc| {
s.editor.driver(fc, lc).clear_compose();
});
});
ctx.request_render();
ctx.request_rebuild();
}))
.builtin()
}
fn mouse_down_listener(&self, state: &Stateful<InputState>) -> Listener {
let state = state.clone();
let (sz_pv, sz_ph, _sz_font) = input_size_metrics(theme::current(), self.size);
let (pad_h, pad_v) = (
self.padding_h.unwrap_or(sz_ph),
self.padding_v.unwrap_or(sz_pv),
);
Listener::on_mouse_down(Rc::new(move |ctx: &mut EventContext| {
let (x, y, shift, clicks) = match ctx.event() {
Some(Event::MouseDown {
x,
y,
button: MouseButton::Left,
modifiers,
click_count,
}) => (*x, *y, modifiers.shift, *click_count),
_ => return,
};
let Some(rect) = ctx.current_rect() else {
return;
};
let lx = x - rect.x - pad_h;
let ly = y - rect.y - pad_v;
state.update(|s| {
s.selecting = true;
s.last_activity = Some(Instant::now());
with_text_contexts(|fc, lc| {
let mut driver = s.editor.driver(fc, lc);
match clicks {
2 => driver.select_word_at_point(lx, ly),
3 => driver.select_line_at_point(lx, ly),
_ if shift => driver.shift_click_extension(lx, ly),
_ => driver.move_to_point(lx, ly),
}
});
});
ctx.capture_mouse();
ctx.request_render();
ctx.request_rebuild();
}))
.builtin()
}
fn mouse_move_listener(&self, state: &Stateful<InputState>) -> Listener {
let state = state.clone();
let (sz_pv, sz_ph, _sz_font) = input_size_metrics(theme::current(), self.size);
let (pad_h, pad_v) = (
self.padding_h.unwrap_or(sz_ph),
self.padding_v.unwrap_or(sz_pv),
);
Listener::on_mouse_move(Rc::new(move |ctx: &mut EventContext| {
if !state.get().selecting {
return;
}
let (x, y) = match ctx.event() {
Some(Event::MouseMove { x, y }) => (*x, *y),
_ => return,
};
let Some(rect) = ctx.current_rect() else {
return;
};
let lx = x - rect.x - pad_h;
let ly = y - rect.y - pad_v;
state.update(|s| {
s.last_activity = Some(Instant::now());
with_text_contexts(|fc, lc| {
s.editor.driver(fc, lc).extend_selection_to_point(lx, ly);
});
});
ctx.request_render();
ctx.request_rebuild();
}))
.builtin()
}
fn mouse_up_listener(state: &Stateful<InputState>) -> Listener {
let state = state.clone();
Listener::on_mouse_up(Rc::new(move |_ctx: &mut EventContext| {
if state.get().selecting {
state.update(|s| s.selecting = false);
}
}))
.builtin()
}
}
fn input_size_metrics(t: theme::Theme, s: InputSize) -> (f32, f32, f64) {
match s {
InputSize::Sm => (4.0, 8.0, t.font.sm),
InputSize::Md => (6.0, 12.0, t.font.sm),
InputSize::Lg => (8.0, 16.0, t.font.md),
}
}
fn input_status_color(t: theme::Theme, s: InputStatus) -> Color {
match s {
InputStatus::Default => t.border.default,
InputStatus::Success => t.status.success,
InputStatus::Warning => t.status.warning,
InputStatus::Danger => t.status.danger,
}
}
impl Widget for Input {
fn build(&self, ctx: &mut BuildContext) -> ViewNode {
let t = theme::current();
let (sz_pv, sz_ph, sz_font) = input_size_metrics(t, self.size);
let pv = self.padding_v.unwrap_or(sz_pv);
let ph = self.padding_h.unwrap_or(sz_ph);
let mut text_style = self.text_style.clone();
if text_style.font_size <= 0.0 {
text_style.font_size = sz_font;
}
let state = ctx.use_state(|| InputState::new(&text_style));
let input_width = self.layout.width.unwrap_or(200.0);
let input_height = self.layout.height;
let content_width = (input_width - ph * 2.0).max(0.0);
let line_h = TextEngine::measure_text("Mg", &text_style).1 as f32;
let inner_height = input_height.unwrap_or(line_h + pv * 2.0);
let (text_content, caret, sel_rects, focused) = {
let mut st = state.get_mut();
apply_plain_editor_style(&mut st.editor, &text_style);
let caret =
editor_cursor_geometry(&mut st.editor, &text_style, Some(content_width), 1.0);
let sel_rects = st.editor.selection_geometry();
let text_content = st.editor.raw_text().to_string();
(text_content, caret, sel_rects, st.focused)
};
let show_caret = if focused {
match state.get().last_activity {
Some(t) => {
let elapsed = Instant::now().duration_since(t);
if elapsed < BLINK_SOLID {
true
} else {
let half = BLINK_HALF.as_millis();
((elapsed - BLINK_SOLID).as_millis() / half).is_multiple_of(2)
}
}
None => true,
}
} else {
false
};
let is_empty = text_content.is_empty();
let show_placeholder = is_empty && !focused;
let display_text = if show_placeholder {
self.placeholder.clone()
} else {
text_content
};
let display_style = {
let mut s = if show_placeholder {
let mut s = text_style.clone();
s.color = t.text.subtle_default;
s
} else {
text_style.clone()
};
s.wrap = self.multiline;
s
};
let content_inner_h = inner_height - pv * 2.0;
let center_offset = if self.multiline {
0.0
} else {
((content_inner_h - line_h) / 2.0).max(0.0)
};
let text_node = ViewNode::Text {
content: display_text,
style: display_style,
layout: FlexStyle::default().flex_grow(1.0),
key: None,
listeners: vec![],
};
let mut children: Vec<ViewNode> = Vec::new();
if focused && !sel_rects.is_empty() {
let b = t.text.brand_default;
let sel_color = Color::rgba(b.r, b.g, b.b, 0x4d);
for (i, (bb, _line)) in sel_rects.iter().enumerate() {
let w = (bb.x1 - bb.x0) as f32;
let h = (bb.y1 - bb.y0) as f32;
if w <= 0.0 || h <= 0.0 {
continue;
}
children.push(ViewNode::Div {
layout: FlexStyle::default()
.width(w)
.height(h)
.absolute()
.position_left(ph + bb.x0 as f32)
.position_top(pv + center_offset + bb.y0.max(0.0) as f32),
paint: PaintStyle::new().background(sel_color),
key: Some(format!("__sel_{}__", i)),
children: vec![],
listeners: vec![],
});
}
}
children.push(text_node);
if focused
&& show_caret
&& let Some(BoundingBox { x0, y0, y1, .. }) = caret
{
let caret_y = y0.max(0.0);
let caret_height = (y1 - y0).max(8.0);
let caret = ViewNode::Div {
layout: FlexStyle::default()
.width(1.0)
.height(caret_height as f32)
.absolute()
.position_left(ph + x0 as f32)
.position_top(pv + center_offset + caret_y as f32),
paint: PaintStyle::new().background(text_style.color),
key: Some("__ime_caret__".to_string()),
children: vec![],
listeners: vec![],
};
children.push(caret);
}
let mut paint = self.paint.clone();
if focused {
paint.border_color = Some(t.text.brand_default);
} else if self.status != InputStatus::Default {
paint.border_color = Some(input_status_color(t, self.status));
}
let mut layout = FlexStyle::row()
.align_items(if self.multiline {
FlexAlign::Start
} else {
FlexAlign::Center
})
.width(input_width)
.height(inner_height)
.padding_left(ph)
.padding_right(ph)
.padding_top(pv)
.padding_bottom(pv);
layout = self.layout.apply(layout);
let mut listeners = self.listeners.clone();
let on_focus = self.on_focus.clone();
let on_blur = self.on_blur.clone();
listeners.push(Self::focus_listener(
&state,
true,
on_focus.clone(),
on_blur.clone(),
));
listeners.push(Self::focus_listener(&state, false, on_focus, on_blur));
listeners.push(self.key_listener(&state));
listeners.push(self.mouse_down_listener(&state));
listeners.push(self.mouse_move_listener(&state));
listeners.push(Self::mouse_up_listener(&state));
listeners.push(Self::ime_preedit_listener(&state));
listeners.push(Self::ime_commit_listener(&state));
listeners.push(Self::ime_disabled_listener(&state));
ViewNode::Div {
layout,
paint,
key: None,
children,
listeners,
}
}
}