1use densa_core::{Color, Point, Rect, Size};
2use densa_draw::Canvas;
3use densa_paragraph::{Motion, ParagraphError, ParagraphStyle, TextEditor, TextSelection};
4use densa_ui::{
5 AccessAction, AccessNode, AccessRequest, ActionKind, Constraints, Cursor, Event, EventCx,
6 Handled, Ime, Key, KeyEvent, LayoutCx, NamedKey, PaintCx, PointerButton, PointerKind, Role,
7 Widget,
8};
9
10use crate::paint::outline;
11use crate::text::Text;
12
13const PADDING_X: f32 = 6.0;
15const PADDING_Y: f32 = 5.0;
16const WIDTH: f32 = 180.0;
18
19#[derive(Debug)]
26pub struct TextInput {
27 initial: String,
29 placeholder: Text,
30 editor: Option<TextEditor>,
31 shaped: Option<(f32, Color)>,
33 scroll: f32,
35}
36
37impl TextInput {
38 pub fn new(text: impl Into<String>) -> Self {
40 Self {
41 initial: text.into(),
42 placeholder: Text::new(""),
43 editor: None,
44 shaped: None,
45 scroll: 0.0,
46 }
47 }
48
49 #[must_use]
51 pub fn with_placeholder(mut self, placeholder: impl Into<String>) -> Self {
52 self.placeholder = Text::new(placeholder);
53 self
54 }
55
56 pub fn text(&self) -> &str {
58 self.editor
59 .as_ref()
60 .map_or(self.initial.as_str(), TextEditor::text)
61 }
62
63 pub fn set_text(
69 &mut self,
70 cx: &mut EventCx<'_>,
71 text: impl Into<String>,
72 ) -> Result<(), ParagraphError> {
73 let text = text.into();
74 match &mut self.editor {
75 Some(editor) => editor.set_text(cx.text_system(), &text)?,
76 None => self.initial = text,
77 }
78 self.follow_caret(cx);
79 Ok(())
80 }
81
82 fn prepare(&mut self, cx: &mut LayoutCx<'_>) {
84 let px = cx.px(cx.theme().font_size);
85 let color = cx.theme().text;
86 if self.shaped == Some((px, color)) && self.editor.is_some() {
87 return;
88 }
89 let style = ParagraphStyle::new(px).with_color(color);
90 match &mut self.editor {
91 Some(editor) => {
92 if editor.set_style(cx.text_system(), style).is_err() {
93 return;
94 }
95 }
96 None => {
97 let text = std::mem::take(&mut self.initial);
98 match TextEditor::new(cx.text_system(), text.clone(), style) {
99 Ok(editor) => self.editor = Some(editor),
100 Err(_) => {
101 self.initial = text;
102 return;
103 }
104 }
105 }
106 }
107 self.shaped = Some((px, color));
108 }
109
110 fn inner(content: Rect, scale: f32) -> Rect {
112 let pad = PADDING_X * scale;
113 Rect::from_edges(
114 content.left() + pad,
115 content.top(),
116 content.right() - pad,
117 content.bottom(),
118 )
119 }
120
121 fn origin(&self, inner: Rect) -> Point {
123 let height = self.editor.as_ref().map_or(0.0, |e| e.paragraph().height());
124 Point::new(
125 inner.left() - self.scroll,
126 (inner.top() + (inner.height() - height) / 2.0).round(),
127 )
128 }
129
130 fn follow_caret(&mut self, cx: &mut EventCx<'_>) {
132 let Some(editor) = &self.editor else {
133 return;
134 };
135 let inner = Self::inner(cx.content_rect(), cx.scale());
136 let paragraph = editor.paragraph();
137 let caret = editor.selection().caret_rect(paragraph, cx.px(1.0));
138 let room = inner.width().max(0.0);
139 if caret.left() < self.scroll {
140 self.scroll = caret.left();
141 } else if caret.right() > self.scroll + room {
142 self.scroll = caret.right() - room;
143 }
144 let longest = (paragraph.width() + caret.width() - room).max(0.0);
145 self.scroll = self.scroll.clamp(0.0, longest);
146 let origin = self.origin(inner);
147 cx.set_ime_area(Some(editor.ime_area().translate(origin)));
148 cx.request_paint();
149 }
150
151 fn pointer(
152 &mut self,
153 cx: &mut EventCx<'_>,
154 kind: PointerKind,
155 at: Point,
156 clicks: u8,
157 shift: bool,
158 ) -> Handled {
159 let inner = Self::inner(cx.content_rect(), cx.scale());
160 let origin = self.origin(inner);
161 let Some(editor) = &mut self.editor else {
162 return Handled::No;
163 };
164 let local = Point::new(at.x - origin.x, at.y - origin.y);
165 let paragraph = editor.paragraph();
166 let selection = match kind {
167 PointerKind::Down(PointerButton::Primary) => match clicks {
168 1 if shift => editor.selection().shift_click(paragraph, local),
169 1 => TextSelection::at_point(paragraph, local),
170 2 => TextSelection::word_at_point(paragraph, local),
171 _ => TextSelection::select_all(paragraph),
172 },
173 PointerKind::Move if cx.is_pressed() => {
174 editor.selection().extend_to_point(paragraph, local)
175 }
176 _ => return Handled::No,
177 };
178 editor.set_selection(selection);
179 self.follow_caret(cx);
180 Handled::Yes
181 }
182
183 fn key(&mut self, cx: &mut EventCx<'_>, key: &KeyEvent<'_>) -> Handled {
184 let Some(editor) = &mut self.editor else {
185 return Handled::No;
186 };
187 let (command, shift) = (key.modifiers.command(), key.modifiers.shift());
188 let by_word = |word: Motion, single: Motion| if command { word } else { single };
189 let types = !command || key.modifiers.alt();
191 let edited = match key.key {
192 Key::Named(NamedKey::Backspace) => editor.delete(
193 cx.text_system(),
194 by_word(Motion::PreviousWord, Motion::Previous),
195 ),
196 Key::Named(NamedKey::Delete) => {
197 editor.delete(cx.text_system(), by_word(Motion::NextWord, Motion::Next))
198 }
199 Key::Named(named) => {
200 let motion = match named {
201 NamedKey::ArrowLeft => by_word(Motion::WordLeft, Motion::Left),
202 NamedKey::ArrowRight => by_word(Motion::WordRight, Motion::Right),
203 NamedKey::Home => Motion::LineStart,
204 NamedKey::End => Motion::LineEnd,
205 NamedKey::Enter => {
206 cx.emit(ActionKind::Submitted);
207 return Handled::Yes;
208 }
209 _ => match key.text.filter(|_| types) {
211 Some(text) => {
212 let edited = editor.insert(cx.text_system(), text);
213 if edited.is_ok() {
214 cx.emit(ActionKind::Changed);
215 }
216 self.follow_caret(cx);
217 return Handled::Yes;
218 }
219 None => return Handled::No,
220 },
221 };
222 editor.move_caret(motion, shift);
223 self.follow_caret(cx);
224 return Handled::Yes;
225 }
226 Key::Character(c) if command && c.eq_ignore_ascii_case("a") => {
227 let all = TextSelection::select_all(editor.paragraph());
228 editor.set_selection(all);
229 self.follow_caret(cx);
230 return Handled::Yes;
231 }
232 _ => match key.text.filter(|_| types) {
233 Some(text) => editor.insert(cx.text_system(), text),
234 None => return Handled::No,
235 },
236 };
237 if edited.is_ok() {
238 cx.emit(ActionKind::Changed);
239 }
240 self.follow_caret(cx);
241 Handled::Yes
242 }
243
244 fn ime(&mut self, cx: &mut EventCx<'_>, ime: &Ime<'_>) -> Handled {
245 let Some(editor) = &mut self.editor else {
246 return Handled::No;
247 };
248 let changed = match *ime {
251 Ime::Preedit { text, cursor } => {
252 let cursor = cursor.map(|(start, end)| start..end);
253 let _ = editor.set_composition(cx.text_system(), text, cursor);
254 false
255 }
256 Ime::Commit(text) => editor.insert(cx.text_system(), text).is_ok(),
257 Ime::Disabled => {
258 let _ = editor.set_composition(cx.text_system(), "", None);
259 false
260 }
261 _ => false,
262 };
263 if changed {
264 cx.emit(ActionKind::Changed);
265 }
266 self.follow_caret(cx);
267 Handled::Yes
268 }
269}
270
271impl Widget for TextInput {
272 fn accessibility(&self, _: &PaintCx<'_>, node: &mut AccessNode) {
273 node.set_role(Role::TextInput);
274 node.set_value(self.text());
275 if !self.placeholder.as_str().is_empty() {
276 node.set_placeholder(self.placeholder.as_str());
277 }
278 node.add_action(AccessAction::SetValue);
279 }
280
281 fn access_action(&mut self, cx: &mut EventCx<'_>, request: &AccessRequest<'_>) -> Handled {
282 let (AccessAction::SetValue, Some(value)) = (request.action, request.value) else {
283 return Handled::No;
284 };
285 if self.set_text(cx, value).is_err() {
286 return Handled::No;
287 }
288 cx.emit(ActionKind::Changed);
289 Handled::Yes
290 }
291
292 fn measure(&mut self, cx: &mut LayoutCx<'_>, _: Constraints) -> Size {
293 self.prepare(cx);
294 let line = self
295 .editor
296 .as_ref()
297 .map_or(0.0, |e| e.paragraph().height())
298 .max(cx.px(cx.theme().font_size) * 1.2);
299 Size::new(cx.px(WIDTH), (line + 2.0 * cx.px(PADDING_Y)).ceil())
300 }
301
302 fn layout(&mut self, cx: &mut LayoutCx<'_>, size: Size) {
303 self.prepare(cx);
304 if let Some(editor) = &self.editor {
306 let room = (size.width - 2.0 * cx.px(PADDING_X)).max(0.0);
307 let longest = (editor.paragraph().width() + cx.px(1.0) - room).max(0.0);
308 self.scroll = self.scroll.clamp(0.0, longest);
309 }
310 let dim = cx.theme().text_dim;
311 self.placeholder.fit(cx, f32::INFINITY, dim);
312 }
313
314 fn paint(&mut self, cx: &PaintCx<'_>, canvas: &mut Canvas<'_>) {
315 let theme = cx.theme();
316 let rect = cx.rect();
317 canvas.fill_rect(rect, theme.raised);
318 let border = if cx.is_focused() {
319 theme.focus
320 } else {
321 theme.border
322 };
323 outline(canvas, rect, cx.px(1.0).round(), border);
324 let inner = Self::inner(cx.content_rect(), cx.scale());
325 let origin = self.origin(inner);
326 let Some(editor) = &self.editor else {
327 return;
328 };
329 let saved = canvas.state();
330 canvas.clip(inner);
331 if editor.text().is_empty() && editor.composition().is_none() {
332 let size = self.placeholder.size();
333 let y = (inner.top() + (inner.height() - size.height) / 2.0).round();
334 self.placeholder.paint(canvas, Point::new(inner.left(), y));
335 }
336 let paragraph = editor.paragraph();
337 let selection = editor.selection();
338 if cx.is_focused() && !selection.is_caret() {
339 selection.for_each_rect(paragraph, |r| {
340 canvas.fill_rect(r.translate(origin), theme.selection);
341 });
342 }
343 let underline = cx.px(1.0).round();
344 editor.for_each_composition_rect(|r| {
345 let r = r.translate(origin);
346 canvas.fill_rect(
347 Rect::from_edges(r.left(), r.bottom() - underline, r.right(), r.bottom()),
348 theme.text,
349 );
350 });
351 for run in paragraph.runs() {
352 canvas.draw_run(run, origin);
353 }
354 if cx.is_focused() && editor.shows_caret() {
355 let caret = selection.caret_rect(paragraph, cx.px(1.0).round());
356 canvas.fill_rect(caret.translate(origin), theme.text);
357 }
358 canvas.restore(saved);
359 }
360
361 fn event(&mut self, cx: &mut EventCx<'_>, event: &Event<'_>) -> Handled {
362 match event {
363 Event::Pointer(pointer) => match pointer.kind {
364 PointerKind::Enter => {
365 cx.set_cursor(Some(Cursor::Text));
366 Handled::Yes
367 }
368 PointerKind::Up(PointerButton::Primary) => Handled::Yes,
369 kind => {
370 let shift = pointer.modifiers.shift();
371 self.pointer(cx, kind, pointer.position, pointer.clicks, shift)
372 }
373 },
374 Event::Key(key) if key.pressed => self.key(cx, key),
375 Event::Ime(ime) => self.ime(cx, ime),
376 Event::FocusGained | Event::FocusLost => {
377 self.follow_caret(cx);
378 Handled::Yes
379 }
380 _ => Handled::No,
381 }
382 }
383
384 fn accepts_focus(&self) -> bool {
385 true
386 }
387}