1use std::rc::Rc;
4
5use cosmic_text::Cursor;
6use crabshell_reactive::Signal;
7
8use crate::cx::Cx;
9use crate::edit::{Edit, grapheme_start, graphemes_before};
10use crate::geometry::{Point, Rect};
11use crate::input::Handler;
12use crate::prop::{IntoProp, Prop};
13use crate::style::Style;
14use crate::text::TextNode;
15use crate::tree::{NodeKind, borrow};
16use crate::widgets::{Element, ElementKind, Widget};
17use crate::window::Window;
18
19const DOT: char = '•';
21
22pub(crate) const DEFAULT_WIDTH: f32 = 200.0;
24
25pub(crate) type AcceptHandler = Rc<Handler<dyn Fn(&Cx, &str)>>;
26
27pub(crate) struct InputProps {
28 pub(crate) text: Signal<String>,
29 pub(crate) placeholder: Option<Prop<String>>,
30 pub(crate) password: Option<Prop<bool>>,
31 pub(crate) focus: Option<Prop<bool>>,
32 pub(crate) on_accept: Option<AcceptHandler>,
33}
34
35pub struct TextInput {
54 input: InputProps,
55 style: Option<Prop<Style>>,
56}
57
58impl TextInput {
59 pub fn new(text: Signal<String>) -> TextInput {
61 TextInput {
62 input: InputProps {
63 text,
64 placeholder: None,
65 password: None,
66 focus: None,
67 on_accept: None,
68 },
69 style: None,
70 }
71 }
72
73 pub fn placeholder(mut self, text: impl IntoProp<String>) -> TextInput {
75 self.input.placeholder = Some(text.into_prop());
76 self
77 }
78
79 pub fn password(mut self, password: impl IntoProp<bool>) -> TextInput {
82 self.input.password = Some(password.into_prop());
83 self
84 }
85
86 pub fn focus(mut self, focus: impl IntoProp<bool>) -> TextInput {
89 self.input.focus = Some(focus.into_prop());
90 self
91 }
92
93 pub fn on_accept(mut self, handler: impl Fn(&Cx, &str) + 'static) -> TextInput {
95 self.input.on_accept = Some(Rc::new(Handler::new(handler)));
96 self
97 }
98
99 pub fn style(mut self, style: impl IntoProp<Style>) -> TextInput {
101 self.style = Some(style.into_prop());
102 self
103 }
104}
105
106impl Widget for TextInput {
107 fn into_element(self: Box<Self>) -> Element {
108 Element {
109 name: "TextInput",
110 style: self.style,
111 kind: ElementKind::Input(Box::new(self.input)),
112 children: Vec::new(),
113 }
114 }
115}
116
117#[derive(Clone, Debug, PartialEq, Eq)]
120pub struct Preedit {
121 pub text: String,
123 pub cursor: Option<(usize, usize)>,
125}
126
127#[derive(Clone, Debug, PartialEq)]
129pub struct FieldState {
130 pub field: u64,
132 pub text: String,
134 pub cursor: usize,
136 pub anchor: usize,
138 pub caret: Rect,
140}
141
142impl Window {
143 pub fn field(&self) -> Option<FieldState> {
146 let tree = self.tree.borrow();
147 if !tree.keyboard.entered {
148 return None;
149 }
150 let id = tree.keyboard.focused?;
151 let NodeKind::Input(input) = &tree.node(id)?.kind else {
152 return None;
153 };
154 (!input.password).then(|| FieldState {
155 field: u64::from(id),
156 text: input.edit.text.clone(),
157 cursor: input.edit.cursor,
158 anchor: input.edit.anchor.unwrap_or(input.edit.cursor),
159 caret: input.caret,
160 })
161 }
162
163 pub fn commit_text(&self, text: &str) {
166 let text = text.replace(['\n', '\r'], " ");
167 self.edit_field(|edit| edit.insert(&text));
168 }
169
170 pub fn paste(&self, text: &str) {
174 if text.is_empty() {
176 return;
177 }
178 let text = text.replace(['\n', '\r'], " ");
179 self.change_field(true, |edit| edit.insert(&text));
180 }
181
182 pub fn delete_around(&self, before: usize, after: usize) {
187 self.edit_field(|edit| {
188 let selected = edit.selection().unwrap_or(edit.cursor..edit.cursor);
189 let start = selected.start.saturating_sub(before);
190 let end = selected.end.saturating_add(after).min(edit.text.len());
191 if !(edit.text.is_char_boundary(start) && edit.text.is_char_boundary(end)) {
192 return;
193 }
194 edit.text.replace_range(selected.end..end, "");
195 edit.text.replace_range(start..selected.start, "");
196 let removed = selected.start - start;
197 edit.cursor -= removed;
198 edit.anchor = edit.anchor.map(|anchor| anchor - removed);
199 });
200 }
201
202 pub fn set_preedit(&self, preedit: Option<Preedit>) {
205 let preedit = preedit.filter(|preedit| !preedit.text.is_empty());
206 if preedit.is_some() {
207 self.edit_field(|edit| {
208 if edit.selection().is_some() {
209 edit.insert("");
210 }
211 });
212 }
213 let mut tree = borrow(&self.tree);
214 let Some(id) = tree.keyboard.focused else {
215 return;
216 };
217 if matches!(tree.node(id).map(|node| &node.kind), Some(NodeKind::Input(input)) if input.password)
218 {
219 return;
220 }
221 tree.update_input(id, |input| {
222 input.preedit = preedit;
223 input.refresh();
224 });
225 }
226
227 fn edit_field(&self, change: impl FnOnce(&mut Edit)) {
231 self.change_field(false, change);
232 }
233
234 fn change_field(&self, passwords: bool, change: impl FnOnce(&mut Edit)) {
237 let write = {
238 let mut tree = borrow(&self.tree);
239 let Some(id) = tree.keyboard.focused else {
240 return;
241 };
242 let before = match tree.node(id).map(|node| &node.kind) {
243 Some(NodeKind::Input(input)) if passwords || !input.password => input.edit.text.clone(),
244 _ => return,
245 };
246 let mut write = None;
247 tree.update_input(id, |input| {
248 change(&mut input.edit);
249 input.refresh();
250 if input.edit.text != before {
251 write = Some((input.signal, input.edit.text.clone()));
252 }
253 });
254 write
255 };
256 if let Some((signal, text)) = write {
257 signal.try_set(text);
259 }
260 }
261}
262
263pub(crate) struct InputNode {
265 pub(crate) text: TextNode,
267 pub(crate) edit: Edit,
268 pub(crate) signal: Signal<String>,
270 pub(crate) placeholder: String,
271 pub(crate) password: bool,
272 pub(crate) focused: bool,
274 pub(crate) scroll: f32,
276 pub(crate) caret: Rect,
279 pub(crate) selection: Vec<Rect>,
281 pub(crate) area: Rect,
283 pub(crate) preedit: Option<Preedit>,
285 pub(crate) underline: Vec<Rect>,
287 pub(crate) on_accept: Signal<Option<AcceptHandler>>,
290}
291
292impl InputNode {
293 pub(crate) fn new(signal: Signal<String>, on_accept: Signal<Option<AcceptHandler>>) -> InputNode {
294 InputNode {
295 text: TextNode::new(),
296 edit: Edit::default(),
297 signal,
298 placeholder: String::new(),
299 password: false,
300 focused: false,
301 scroll: 0.0,
302 caret: Rect::default(),
303 selection: Vec::new(),
304 area: Rect::default(),
305 preedit: None,
306 underline: Vec::new(),
307 on_accept,
308 }
309 }
310
311 pub(crate) fn showing_placeholder(&self) -> bool {
313 self.edit.text.is_empty() && self.preedit.is_none()
314 }
315
316 fn preedit_len(&self) -> usize {
318 self
319 .preedit
320 .as_ref()
321 .map_or(0, |preedit| preedit.text.len())
322 }
323
324 pub(crate) fn refresh(&mut self) -> bool {
327 let shown = if self.showing_placeholder() {
328 self.placeholder.clone()
329 } else if self.password {
330 DOT
331 .to_string()
332 .repeat(graphemes_before(&self.edit.text, self.edit.text.len()))
333 } else if let Some(preedit) = &self.preedit {
334 let (before, after) = self.edit.text.split_at(self.edit.cursor);
335 format!("{before}{}{after}", preedit.text)
336 } else {
337 self.edit.text.clone()
338 };
339 self.text.set_content(shown)
340 }
341
342 fn shown_index(&self, at: usize) -> usize {
344 if self.showing_placeholder() {
345 0
346 } else if self.password {
347 graphemes_before(&self.edit.text, at) * DOT.len_utf8()
348 } else if at > self.edit.cursor {
349 at + self.preedit_len()
350 } else {
351 at
352 }
353 }
354
355 fn text_index(&self, at: usize) -> usize {
357 if self.showing_placeholder() {
358 0
359 } else if self.password {
360 grapheme_start(&self.edit.text, at / DOT.len_utf8())
361 } else if at <= self.edit.cursor {
362 at
363 } else {
364 let after = at.saturating_sub(self.preedit_len()).max(self.edit.cursor);
366 after.min(self.edit.text.len())
367 }
368 }
369
370 fn x_at(&self, at: usize) -> f32 {
373 self
374 .text
375 .runs()
376 .next()
377 .and_then(|run| run.cursor_position(&Cursor::new(0, at)))
378 .unwrap_or(0.0)
379 }
380
381 pub(crate) fn place(&mut self, content: Rect) {
384 let at = self.shown_index(self.edit.cursor);
385 let caret = match &self.preedit {
386 Some(preedit) => {
387 at + preedit
388 .cursor
389 .map_or(preedit.text.len(), |(start, _)| start)
390 }
391 None => at,
392 };
393 let caret = self.x_at(caret);
394 let width = self.text.runs().next().map_or(0.0, |run| run.line_w);
395 let room = (content.width - 1.0).max(0.0);
397 let mut scroll = self.scroll.min((width - room).max(0.0));
398 if caret - scroll > room {
399 scroll = caret - room;
400 }
401 if caret < scroll {
402 scroll = caret;
403 }
404 self.scroll = scroll.max(0.0);
405 self.text.origin.x = content.x - self.scroll;
406 let origin = self.text.origin;
407 let height = self
408 .text
409 .runs()
410 .next()
411 .map_or(content.height, |run| run.line_height);
412 self.caret = Rect::new(origin.x + caret, origin.y, 1.0, height);
413 self.area = content;
414 self.selection = match (self.edit.selection(), self.text.runs().next()) {
415 (Some(range), Some(run)) => {
416 let start = Cursor::new(0, self.shown_index(range.start));
417 let end = Cursor::new(0, self.shown_index(range.end));
418 run
419 .highlight(start, end)
420 .map(|(x, width)| Rect::new(origin.x + x, origin.y, width, height))
421 .collect()
422 }
423 _ => Vec::new(),
424 };
425 self.underline = match (&self.preedit, self.text.runs().next()) {
426 (Some(preedit), Some(run)) => run
427 .highlight(Cursor::new(0, at), Cursor::new(0, at + preedit.text.len()))
428 .map(|(x, width)| Rect::new(origin.x + x, origin.y + height - 1.0, width, 1.0))
429 .collect(),
430 _ => Vec::new(),
431 };
432 }
433
434 pub(crate) fn hit(&self, point: Point) -> usize {
436 let origin = self.text.origin;
437 let at = self
438 .text
439 .buffer
440 .hit(point.x - origin.x, point.y - origin.y)
441 .map_or(0, |cursor| cursor.index);
442 self.text_index(at)
443 }
444}