1use std::cell::RefCell;
25use std::rc::Rc;
26
27use smudge_core::element::{Element, Value};
28use smudge_core::geometry::{Color, Corners, Point, Rect, Size};
29use smudge_core::input::{Handled, Key, KeyEvent};
30use smudge_core::paint::PaintOp;
31use smudge_core::reactive::{RwSignal, prelude::*};
32use smudge_core::semantics::{Role, Semantics};
33use smudge_core::style::Length;
34use smudge_core::theme::ColorToken;
35use smudge_core::tree::Dirty;
36use smudge_core::widget::{AvailableSpace, MeasureInput, PaintCx, Widget};
37use smudge_core::window::Cursor;
38
39use crate::chrome::{
40 Direction, InputChrome, ParagraphCache, Placeholder, SELECTION_ALPHA, apply_edit_count,
41 apply_move, apply_outside_write, caret_node, copy_selection, cut_selection, handle_commit,
42 handle_focus, handle_move, handle_preedit, handle_press, handle_release, is_shortcut,
43 outside_write, paint_paragraphs, paint_selection, types_text,
44};
45use crate::style::{TextStyle, current_text_style};
46
47pub struct TextInput {
53 chrome: Rc<InputChrome>,
55 placeholder: RefCell<Placeholder>,
57 style: TextStyle,
60 runs: RefCell<Option<ParagraphCache>>,
63}
64
65impl TextInput {
66 fn new(chrome: Rc<InputChrome>) -> TextInput {
69 TextInput {
70 chrome,
71 placeholder: RefCell::new(Placeholder::new()),
72 style: TextStyle::default(),
73 runs: RefCell::new(None),
74 }
75 }
76
77 fn text_color(&self, cx: &PaintCx<'_>) -> Color {
80 match &cx.style.paint.text_color {
81 Some(color) => color.resolve(cx.theme),
82 None => self.style.color.resolve(cx.theme),
83 }
84 }
85}
86
87impl Widget for TextInput {
88 fn semantics(&self) -> Option<Semantics> {
94 let label = (!self.placeholder.borrow().text().is_empty())
95 .then(|| self.placeholder.borrow().text().into());
96 Some(Semantics {
97 role: Role::TextInput,
98 label,
99 value: Some(self.chrome.editor.text()),
100 })
101 }
102
103 fn measure(&mut self, input: MeasureInput) -> Option<Size> {
115 let width = match input.available_width {
116 AvailableSpace::Definite(width) => width.max(0.0),
117 AvailableSpace::MinContent | AvailableSpace::MaxContent => 0.0,
118 };
119 Some(Size {
120 width,
121 height: self.style.size * self.style.line_height,
122 })
123 }
124
125 fn paint(&self, cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
142 let color = self.text_color(cx);
143 let inset = Point {
144 x: cx.content.x0 - cx.rect.x0,
145 y: cx.content.y0 - cx.rect.y0,
146 };
147 self.chrome.painted.set(Some((cx.content, color, inset)));
148 let (scroll_x, scroll_y) = self.chrome.place(cx.content, color);
149 self.chrome
150 .line_height
151 .set(self.style.size * self.style.line_height);
152 let text_origin = Point {
153 x: cx.content.x0 - scroll_x,
154 y: cx.content.y0 - scroll_y,
155 };
156
157 let selection_color = Color {
158 a: SELECTION_ALPHA,
159 ..cx.theme.color(ColorToken::ACCENT)
160 };
161 paint_selection(
162 &self.chrome,
163 text_origin,
164 (f32::NEG_INFINITY, f32::INFINITY),
165 selection_color,
166 out,
167 );
168
169 for area in self.chrome.editor.compose_geometry() {
172 out.push(PaintOp::FillRect {
173 rect: Rect::new(
174 text_origin.x + area.x0,
175 text_origin.y + area.y0,
176 text_origin.x + area.x1,
177 text_origin.y + area.y1,
178 ),
179 radius: Corners::ZERO,
180 color,
181 });
182 }
183
184 if self.chrome.editor.is_empty()
185 && !self.chrome.editor.is_composing()
186 && !self.placeholder.borrow().text().is_empty()
187 {
188 let muted = cx.theme.color(ColorToken::TEXT_MUTED);
189 self.placeholder
190 .borrow_mut()
191 .paint(&self.style, text_origin, muted, out);
192 } else {
193 paint_paragraphs(
194 &mut self.runs.borrow_mut(),
195 &self.chrome.editor,
196 text_origin,
197 color,
198 (f32::NEG_INFINITY, f32::INFINITY),
199 out,
200 );
201 }
202 }
203
204 fn ime_area(&self) -> Option<Rect> {
211 let (content, _, _) = self.chrome.painted.get()?;
212 let area = self.chrome.editor.ime_area();
213 let scroll_x = self.chrome.scroll_x.get();
214 let scroll_y = self.chrome.scroll_y.get();
215 Some(Rect::new(
216 content.x0 + area.x0 - scroll_x,
217 content.y0 + area.y0 - scroll_y,
218 content.x0 + area.x1 - scroll_x,
219 content.y0 + area.y1 - scroll_y,
220 ))
221 }
222}
223
224pub fn text_input(value: RwSignal<String>) -> Element<TextInput> {
245 let chrome = Rc::new(InputChrome::new(value.clone(), false));
246
247 let key_chrome = Rc::clone(&chrome);
248 let press_chrome = Rc::clone(&chrome);
249 let move_chrome = Rc::clone(&chrome);
250 let up_chrome = Rc::clone(&chrome);
251 let focus_chrome = Rc::clone(&chrome);
252 let commit_chrome = Rc::clone(&chrome);
253 let preedit_chrome = Rc::clone(&chrome);
254 let edits_chrome = Rc::clone(&chrome);
255 let caret_chrome = Rc::clone(&chrome);
256 let value_chrome = Rc::clone(&chrome);
257
258 Element::new(TextInput::new(Rc::clone(&chrome)))
259 .clip(true)
260 .cursor(Cursor::Text)
261 .min_height(one_line)
262 .bind(AmbientStyleKey, current_text_style, apply_ambient_style)
263 .bind(
264 ValueKey,
265 move || outside_write(&value_chrome, &value),
266 apply_value,
267 )
268 .bind(EditsKey, move || edits_chrome.edits.get(), apply_edits)
269 .on_key(move |event| handle_key(&key_chrome, event))
270 .on_pointer_down(move |position| handle_press(&press_chrome, position))
271 .on_pointer_move(move |position| handle_move(&move_chrome, position))
272 .on_pointer_up(move |position| handle_release(&up_chrome, position))
273 .on_focus(move |focused| handle_focus(&focus_chrome, focused))
274 .on_ime_commit(move |text| handle_commit(&commit_chrome, text))
275 .on_ime_preedit(move |text, cursor| handle_preedit(&preedit_chrome, text, cursor))
276 .child(caret_node(&caret_chrome))
277}
278
279pub trait TextInputExt: Sized {
291 fn placeholder(self, text: impl Into<Value<String>>) -> Self;
296}
297
298struct AmbientStyleKey;
300
301struct ValueKey;
303
304struct EditsKey;
306
307struct PlaceholderKey;
309
310impl TextInputExt for Element<TextInput> {
311 fn placeholder(self, text: impl Into<Value<String>>) -> Self {
312 self.bind(PlaceholderKey, text, set_placeholder)
313 }
314}
315
316fn one_line() -> Length {
321 let style = current_text_style();
322 Length::px(style.size * style.line_height)
323}
324
325fn apply_ambient_style(input: &mut TextInput, ambient: TextStyle) -> Dirty {
327 if input.style == ambient {
328 return Dirty::Clean;
329 }
330 input.style = ambient;
331 input.chrome.editor.set_style(&input.style);
332 input.placeholder.borrow_mut().invalidate_style();
333 *input.runs.borrow_mut() = None;
334 Dirty::Layout
335}
336
337fn apply_value(input: &mut TextInput, value: Option<String>) -> Dirty {
340 match value {
341 Some(value) => apply_outside_write(&input.chrome, &value),
342 None => Dirty::Clean,
343 }
344}
345
346fn apply_edits(input: &mut TextInput, _edits: u64) -> Dirty {
348 apply_edit_count(&input.chrome)
349}
350
351fn set_placeholder(input: &mut TextInput, text: String) -> Dirty {
353 let mut placeholder = input.placeholder.borrow_mut();
354 if placeholder.text() == text {
355 return Dirty::Clean;
356 }
357 placeholder.set_text(text);
358 Dirty::Paint
359}
360
361fn handle_key(chrome: &InputChrome, event: &KeyEvent) -> Handled {
371 if !event.pressed {
372 return Handled::No;
373 }
374 let modifiers = event.modifiers;
375 let by_word = modifiers.control || modifiers.alt;
376 match &event.key {
377 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("a") => {
378 chrome.editor.select_all();
379 chrome.touched();
380 Handled::Yes
381 }
382 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("c") => {
383 copy_selection(chrome);
384 Handled::Yes
385 }
386 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("x") => {
387 if cut_selection(chrome) {
388 chrome.touched();
389 }
390 Handled::Yes
391 }
392 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("v") => {
393 paste_from_clipboard(chrome);
394 Handled::Yes
395 }
396 Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("z") => {
397 if modifiers.shift {
398 chrome.editor.redo();
399 } else {
400 chrome.editor.undo();
401 }
402 chrome.touched();
403 Handled::Yes
404 }
405 Key::Character(text)
406 if cfg!(not(target_os = "macos"))
407 && is_shortcut(modifiers)
408 && text.eq_ignore_ascii_case("y") =>
409 {
410 chrome.editor.redo();
411 chrome.touched();
412 Handled::Yes
413 }
414 Key::Backspace => {
415 if by_word {
416 chrome.editor.backspace_word();
417 } else {
418 chrome.editor.backspace();
419 }
420 chrome.touched();
421 Handled::Yes
422 }
423 Key::Delete => {
424 if by_word {
425 chrome.editor.delete_word();
426 } else {
427 chrome.editor.delete();
428 }
429 chrome.touched();
430 Handled::Yes
431 }
432 Key::ArrowLeft => {
433 apply_move(chrome, modifiers.shift, by_word, Direction::Left);
434 Handled::Yes
435 }
436 Key::ArrowRight => {
437 apply_move(chrome, modifiers.shift, by_word, Direction::Right);
438 Handled::Yes
439 }
440 Key::Home => {
441 if modifiers.shift {
442 chrome.editor.extend_to_start();
443 } else {
444 chrome.editor.move_to_start();
445 }
446 chrome.touched();
447 Handled::Yes
448 }
449 Key::End => {
450 if modifiers.shift {
451 chrome.editor.extend_to_end();
452 } else {
453 chrome.editor.move_to_end();
454 }
455 chrome.touched();
456 Handled::Yes
457 }
458 Key::Character(text) => {
459 if !types_text(modifiers) {
460 return Handled::No;
461 }
462 chrome.editor.insert(text);
463 chrome.touched();
464 Handled::Yes
465 }
466 Key::Space => {
467 if !types_text(modifiers) {
468 return Handled::No;
469 }
470 chrome.editor.insert(" ");
471 chrome.touched();
472 Handled::Yes
473 }
474 _ => Handled::No,
475 }
476}
477
478fn paste_from_clipboard(chrome: &InputChrome) {
486 if let Some(text) = smudge_core::clipboard::with(|slot| slot.get()).flatten() {
487 chrome.editor.paste(&text);
488 chrome.touched();
489 }
490}