use std::rc::Rc;
use cosmic_text::Cursor;
use crabshell_reactive::Signal;
use crate::cx::Cx;
use crate::edit::{Edit, grapheme_start, graphemes_before};
use crate::geometry::{Point, Rect};
use crate::input::Handler;
use crate::prop::{IntoProp, Prop};
use crate::style::Style;
use crate::text::TextNode;
use crate::tree::{NodeKind, borrow};
use crate::widgets::{Element, ElementKind, Widget};
use crate::window::Window;
const DOT: char = '•';
pub(crate) const DEFAULT_WIDTH: f32 = 200.0;
pub(crate) type AcceptHandler = Rc<Handler<dyn Fn(&Cx, &str)>>;
pub(crate) struct InputProps {
pub(crate) text: Signal<String>,
pub(crate) placeholder: Option<Prop<String>>,
pub(crate) password: Option<Prop<bool>>,
pub(crate) focus: Option<Prop<bool>>,
pub(crate) on_accept: Option<AcceptHandler>,
}
pub struct TextInput {
input: InputProps,
style: Option<Prop<Style>>,
}
impl TextInput {
pub fn new(text: Signal<String>) -> TextInput {
TextInput {
input: InputProps {
text,
placeholder: None,
password: None,
focus: None,
on_accept: None,
},
style: None,
}
}
pub fn placeholder(mut self, text: impl IntoProp<String>) -> TextInput {
self.input.placeholder = Some(text.into_prop());
self
}
pub fn password(mut self, password: impl IntoProp<bool>) -> TextInput {
self.input.password = Some(password.into_prop());
self
}
pub fn focus(mut self, focus: impl IntoProp<bool>) -> TextInput {
self.input.focus = Some(focus.into_prop());
self
}
pub fn on_accept(mut self, handler: impl Fn(&Cx, &str) + 'static) -> TextInput {
self.input.on_accept = Some(Rc::new(Handler::new(handler)));
self
}
pub fn style(mut self, style: impl IntoProp<Style>) -> TextInput {
self.style = Some(style.into_prop());
self
}
}
impl Widget for TextInput {
fn into_element(self: Box<Self>) -> Element {
Element {
name: "TextInput",
style: self.style,
kind: ElementKind::Input(Box::new(self.input)),
children: Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Preedit {
pub text: String,
pub cursor: Option<(usize, usize)>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct FieldState {
pub field: u64,
pub text: String,
pub cursor: usize,
pub anchor: usize,
pub caret: Rect,
}
impl Window {
pub fn field(&self) -> Option<FieldState> {
let tree = self.tree.borrow();
if !tree.keyboard.entered {
return None;
}
let id = tree.keyboard.focused?;
let NodeKind::Input(input) = &tree.node(id)?.kind else {
return None;
};
(!input.password).then(|| FieldState {
field: u64::from(id),
text: input.edit.text.clone(),
cursor: input.edit.cursor,
anchor: input.edit.anchor.unwrap_or(input.edit.cursor),
caret: input.caret,
})
}
pub fn commit_text(&self, text: &str) {
let text = text.replace(['\n', '\r'], " ");
self.edit_field(|edit| edit.insert(&text));
}
pub fn paste(&self, text: &str) {
if text.is_empty() {
return;
}
let text = text.replace(['\n', '\r'], " ");
self.change_field(true, |edit| edit.insert(&text));
}
pub fn delete_around(&self, before: usize, after: usize) {
self.edit_field(|edit| {
let selected = edit.selection().unwrap_or(edit.cursor..edit.cursor);
let start = selected.start.saturating_sub(before);
let end = selected.end.saturating_add(after).min(edit.text.len());
if !(edit.text.is_char_boundary(start) && edit.text.is_char_boundary(end)) {
return;
}
edit.text.replace_range(selected.end..end, "");
edit.text.replace_range(start..selected.start, "");
let removed = selected.start - start;
edit.cursor -= removed;
edit.anchor = edit.anchor.map(|anchor| anchor - removed);
});
}
pub fn set_preedit(&self, preedit: Option<Preedit>) {
let preedit = preedit.filter(|preedit| !preedit.text.is_empty());
if preedit.is_some() {
self.edit_field(|edit| {
if edit.selection().is_some() {
edit.insert("");
}
});
}
let mut tree = borrow(&self.tree);
let Some(id) = tree.keyboard.focused else {
return;
};
if matches!(tree.node(id).map(|node| &node.kind), Some(NodeKind::Input(input)) if input.password)
{
return;
}
tree.update_input(id, |input| {
input.preedit = preedit;
input.refresh();
});
}
fn edit_field(&self, change: impl FnOnce(&mut Edit)) {
self.change_field(false, change);
}
fn change_field(&self, passwords: bool, change: impl FnOnce(&mut Edit)) {
let write = {
let mut tree = borrow(&self.tree);
let Some(id) = tree.keyboard.focused else {
return;
};
let before = match tree.node(id).map(|node| &node.kind) {
Some(NodeKind::Input(input)) if passwords || !input.password => input.edit.text.clone(),
_ => return,
};
let mut write = None;
tree.update_input(id, |input| {
change(&mut input.edit);
input.refresh();
if input.edit.text != before {
write = Some((input.signal, input.edit.text.clone()));
}
});
write
};
if let Some((signal, text)) = write {
signal.try_set(text);
}
}
}
pub(crate) struct InputNode {
pub(crate) text: TextNode,
pub(crate) edit: Edit,
pub(crate) signal: Signal<String>,
pub(crate) placeholder: String,
pub(crate) password: bool,
pub(crate) focused: bool,
pub(crate) scroll: f32,
pub(crate) caret: Rect,
pub(crate) selection: Vec<Rect>,
pub(crate) area: Rect,
pub(crate) preedit: Option<Preedit>,
pub(crate) underline: Vec<Rect>,
pub(crate) on_accept: Signal<Option<AcceptHandler>>,
}
impl InputNode {
pub(crate) fn new(signal: Signal<String>, on_accept: Signal<Option<AcceptHandler>>) -> InputNode {
InputNode {
text: TextNode::new(),
edit: Edit::default(),
signal,
placeholder: String::new(),
password: false,
focused: false,
scroll: 0.0,
caret: Rect::default(),
selection: Vec::new(),
area: Rect::default(),
preedit: None,
underline: Vec::new(),
on_accept,
}
}
pub(crate) fn showing_placeholder(&self) -> bool {
self.edit.text.is_empty() && self.preedit.is_none()
}
fn preedit_len(&self) -> usize {
self
.preedit
.as_ref()
.map_or(0, |preedit| preedit.text.len())
}
pub(crate) fn refresh(&mut self) -> bool {
let shown = if self.showing_placeholder() {
self.placeholder.clone()
} else if self.password {
DOT
.to_string()
.repeat(graphemes_before(&self.edit.text, self.edit.text.len()))
} else if let Some(preedit) = &self.preedit {
let (before, after) = self.edit.text.split_at(self.edit.cursor);
format!("{before}{}{after}", preedit.text)
} else {
self.edit.text.clone()
};
self.text.set_content(shown)
}
fn shown_index(&self, at: usize) -> usize {
if self.showing_placeholder() {
0
} else if self.password {
graphemes_before(&self.edit.text, at) * DOT.len_utf8()
} else if at > self.edit.cursor {
at + self.preedit_len()
} else {
at
}
}
fn text_index(&self, at: usize) -> usize {
if self.showing_placeholder() {
0
} else if self.password {
grapheme_start(&self.edit.text, at / DOT.len_utf8())
} else if at <= self.edit.cursor {
at
} else {
let after = at.saturating_sub(self.preedit_len()).max(self.edit.cursor);
after.min(self.edit.text.len())
}
}
fn x_at(&self, at: usize) -> f32 {
self
.text
.runs()
.next()
.and_then(|run| run.cursor_position(&Cursor::new(0, at)))
.unwrap_or(0.0)
}
pub(crate) fn place(&mut self, content: Rect) {
let at = self.shown_index(self.edit.cursor);
let caret = match &self.preedit {
Some(preedit) => {
at + preedit
.cursor
.map_or(preedit.text.len(), |(start, _)| start)
}
None => at,
};
let caret = self.x_at(caret);
let width = self.text.runs().next().map_or(0.0, |run| run.line_w);
let room = (content.width - 1.0).max(0.0);
let mut scroll = self.scroll.min((width - room).max(0.0));
if caret - scroll > room {
scroll = caret - room;
}
if caret < scroll {
scroll = caret;
}
self.scroll = scroll.max(0.0);
self.text.origin.x = content.x - self.scroll;
let origin = self.text.origin;
let height = self
.text
.runs()
.next()
.map_or(content.height, |run| run.line_height);
self.caret = Rect::new(origin.x + caret, origin.y, 1.0, height);
self.area = content;
self.selection = match (self.edit.selection(), self.text.runs().next()) {
(Some(range), Some(run)) => {
let start = Cursor::new(0, self.shown_index(range.start));
let end = Cursor::new(0, self.shown_index(range.end));
run
.highlight(start, end)
.map(|(x, width)| Rect::new(origin.x + x, origin.y, width, height))
.collect()
}
_ => Vec::new(),
};
self.underline = match (&self.preedit, self.text.runs().next()) {
(Some(preedit), Some(run)) => run
.highlight(Cursor::new(0, at), Cursor::new(0, at + preedit.text.len()))
.map(|(x, width)| Rect::new(origin.x + x, origin.y + height - 1.0, width, 1.0))
.collect(),
_ => Vec::new(),
};
}
pub(crate) fn hit(&self, point: Point) -> usize {
let origin = self.text.origin;
let at = self
.text
.buffer
.hit(point.x - origin.x, point.y - origin.y)
.map_or(0, |cursor| cursor.index);
self.text_index(at)
}
}