1use std::cell::{Cell, RefCell};
27use std::rc::Rc;
28use std::sync::Arc;
29
30use web_time::Instant;
31
32use crate::cursor::{set_cursor_icon, CursorIcon};
33use crate::draw_ctx::DrawCtx;
34use crate::event::{Event, EventResult, Key, MouseButton};
35use crate::geometry::{Point, Rect, Size};
36use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
37use crate::text::{measure_advance, measure_text_metrics, Font};
38use crate::widget::Widget;
39use crate::widgets::text_field_core::{next_char_boundary, prev_char_boundary, TextEditState};
40
41fn clipboard_get() -> Option<String> {
42 crate::clipboard::get_text()
43}
44
45fn clipboard_set(text: &str) {
46 crate::clipboard::set_text(text);
47}
48
49#[derive(Clone, Debug)]
53struct WrappedLine {
54 start: usize,
57 end: usize,
60 text: String,
62 hard_break: bool,
67}
68
69fn wrap_text_indexed(
75 font: &Arc<Font>,
76 text: &str,
77 font_size: f64,
78 max_width: f64,
79) -> Vec<WrappedLine> {
80 let mut out: Vec<WrappedLine> = Vec::new();
81 let mut para_start = 0usize;
82 for (rel_end, chunk) in split_keep_newlines(text).enumerate() {
83 let _ = rel_end;
84 let para = chunk;
85 let para_abs_start = para_start;
86 let para_abs_end = para_abs_start + para.len();
87 let mut cursor = 0usize; let last_boundary = 0usize;
91 while cursor < para.len() {
92 let line_start = cursor;
97 let mut fit_end = line_start;
98 let mut last_word_end: Option<usize> = None;
99 let mut idx = line_start;
100 while idx < para.len() {
101 let next = next_char_boundary(para, idx);
102 let candidate = ¶[line_start..next];
103 let w = measure_text_metrics(font, candidate, font_size).width;
104 if w > max_width && fit_end > line_start {
105 break;
106 }
107 fit_end = next;
108 if next < para.len() {
110 let next_ch = para[next..].chars().next().unwrap_or(' ');
111 if next_ch.is_whitespace() {
112 last_word_end = Some(next);
113 }
114 }
115 idx = next;
116 }
117 let break_at = if fit_end < para.len() && last_word_end.is_some() {
121 last_word_end.unwrap()
122 } else {
123 fit_end.max(next_char_boundary(para, line_start))
124 };
125 let _ = last_boundary; let line_text = para[line_start..break_at].trim_end().to_string();
127 let abs_start = para_abs_start + line_start;
128 let abs_end = para_abs_start + break_at;
129 out.push(WrappedLine {
130 start: abs_start,
131 end: abs_end,
132 text: line_text,
133 hard_break: false,
134 });
135 let mut next_line_start = break_at;
137 while next_line_start < para.len() {
138 let ch = para[next_line_start..].chars().next().unwrap_or('x');
139 if !ch.is_whitespace() || ch == '\n' {
140 break;
141 }
142 next_line_start = next_char_boundary(para, next_line_start);
143 }
144 cursor = next_line_start;
145 if cursor >= para.len() {
146 break;
147 }
148 }
149 if out.is_empty() || out.last().map(|l| l.end).unwrap_or(0) != para_abs_end {
152 if para.is_empty() {
153 out.push(WrappedLine {
154 start: para_abs_start,
155 end: para_abs_end,
156 text: String::new(),
157 hard_break: false,
158 });
159 }
160 }
161 let source_end = para_abs_end + 1; let had_newline =
166 source_end <= text.len() && text.as_bytes().get(para_abs_end) == Some(&b'\n');
167 if had_newline {
168 if let Some(last) = out.last_mut() {
169 last.hard_break = true;
170 }
171 }
172 para_start = if had_newline {
173 source_end
174 } else {
175 para_abs_end
176 };
177 }
178 if out.is_empty() {
179 out.push(WrappedLine {
180 start: 0,
181 end: 0,
182 text: String::new(),
183 hard_break: false,
184 });
185 }
186 out
187}
188
189fn split_keep_newlines(text: &str) -> impl Iterator<Item = &str> + '_ {
193 text.split('\n')
197}
198
199pub struct TextArea {
203 bounds: Rect,
204 children: Vec<Box<dyn Widget>>, base: WidgetBase,
206
207 font: Arc<Font>,
208 font_size: f64,
209 padding: f64,
210
211 edit: Rc<RefCell<TextEditState>>,
213
214 cached_wrap_width: f64,
216 cached_lines: Vec<WrappedLine>,
217 cached_line_h: f64,
218
219 focused: bool,
221 hovered: bool,
222 selecting_drag: bool,
223 focus_time: Option<Instant>,
224 blink_last_phase: Cell<u64>,
225}
226
227impl TextArea {
228 pub fn new(font: Arc<Font>) -> Self {
229 Self {
230 bounds: Rect::default(),
231 children: Vec::new(),
232 base: WidgetBase::new(),
233 font,
234 font_size: 13.0,
235 padding: 8.0,
236 edit: Rc::new(RefCell::new(TextEditState::default())),
237 cached_wrap_width: -1.0,
238 cached_lines: Vec::new(),
239 cached_line_h: 0.0,
240 focused: false,
241 hovered: false,
242 selecting_drag: false,
243 focus_time: None,
244 blink_last_phase: Cell::new(0),
245 }
246 }
247
248 pub fn with_text(self, text: impl Into<String>) -> Self {
249 let t: String = text.into();
250 let cursor = t.len();
251 *self.edit.borrow_mut() = TextEditState {
252 text: t,
253 cursor,
254 anchor: cursor,
255 };
256 self
257 }
258 pub fn with_font_size(mut self, size: f64) -> Self {
259 self.font_size = size;
260 self
261 }
262 pub fn with_padding(mut self, p: f64) -> Self {
263 self.padding = p;
264 self
265 }
266
267 pub fn with_margin(mut self, m: Insets) -> Self {
268 self.base.margin = m;
269 self
270 }
271 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
272 self.base.h_anchor = h;
273 self
274 }
275 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
276 self.base.v_anchor = v;
277 self
278 }
279 pub fn with_min_size(mut self, s: Size) -> Self {
280 self.base.min_size = s;
281 self
282 }
283 pub fn with_max_size(mut self, s: Size) -> Self {
284 self.base.max_size = s;
285 self
286 }
287
288 pub fn text(&self) -> String {
290 self.edit.borrow().text.clone()
291 }
292
293 pub fn cursor(&self) -> usize {
295 self.edit.borrow().cursor
296 }
297
298 pub fn visual_line_count(&self) -> usize {
300 self.cached_lines.len()
301 }
302
303 fn refresh_wrap(&mut self, inner_w: f64) {
305 let st = self.edit.borrow();
306 let same_width = (self.cached_wrap_width - inner_w).abs() < 0.5;
307 if same_width && !self.cached_lines.is_empty() {
308 return;
312 }
313 let lines = wrap_text_indexed(&self.font, &st.text, self.font_size, inner_w.max(1.0));
314 self.cached_lines = lines;
315 self.cached_wrap_width = inner_w;
316 self.cached_line_h = self.font_size * 1.35;
319 }
320
321 fn mark_dirty(&mut self) {
323 self.cached_wrap_width = -1.0;
324 }
325
326 fn line_for_cursor(&self, byte_pos: usize) -> usize {
329 for (i, l) in self.cached_lines.iter().enumerate() {
330 if byte_pos >= l.start && byte_pos <= l.end {
331 return i;
332 }
333 }
334 self.cached_lines.len().saturating_sub(1)
335 }
336
337 fn byte_offset_at(&self, local: Point) -> usize {
340 if self.cached_lines.is_empty() || self.cached_line_h <= 0.0 {
341 return 0;
342 }
343 let inner_top_y = self.bounds.height - self.padding;
346 let rel_from_top = inner_top_y - local.y;
347 let mut line_idx = (rel_from_top / self.cached_line_h).floor() as isize;
348 if line_idx < 0 {
349 line_idx = 0;
350 }
351 if line_idx as usize >= self.cached_lines.len() {
352 line_idx = self.cached_lines.len() as isize - 1;
353 }
354 let line = &self.cached_lines[line_idx as usize];
355 let pad_x = self.padding;
358 let rel_x = (local.x - pad_x).max(0.0);
359 let txt = &line.text;
360 let mut best_byte = 0usize;
361 let mut best_delta = f64::INFINITY;
362 let mut acc = 0.0_f64;
363 let mut prev_byte = 0usize;
364 for (i, _c) in txt.char_indices().chain(std::iter::once((txt.len(), ' '))) {
365 let w_here = if i > prev_byte {
366 measure_advance(&self.font, &txt[prev_byte..i], self.font_size)
367 } else {
368 0.0
369 };
370 acc += w_here;
371 let d = (acc - rel_x).abs();
372 if d < best_delta {
373 best_delta = d;
374 best_byte = i;
375 }
376 prev_byte = i;
377 }
378 line.start + best_byte
379 }
380
381 fn pos_for_cursor(&self, byte_pos: usize) -> Point {
385 if self.cached_lines.is_empty() {
386 return Point::ORIGIN;
387 }
388 let line_idx = self.line_for_cursor(byte_pos);
389 let line = &self.cached_lines[line_idx];
390 let offset = byte_pos.saturating_sub(line.start).min(line.text.len());
391 let x = self.padding + measure_advance(&self.font, &line.text[..offset], self.font_size);
392 let inner_top_y = self.bounds.height - self.padding;
394 let line_top = inner_top_y - line_idx as f64 * self.cached_line_h;
395 let line_bottom = line_top - self.cached_line_h;
396 Point::new(x, line_bottom)
397 }
398
399 fn insert_str(&mut self, s: &str) {
401 let mut st = self.edit.borrow_mut();
402 let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
403 let lo = lo.min(st.text.len());
405 let hi = hi.min(st.text.len());
406 st.text.replace_range(lo..hi, s);
407 st.cursor = lo + s.len();
408 st.anchor = st.cursor;
409 drop(st);
410 self.mark_dirty();
411 }
412
413 fn delete(&mut self, dir: i32) {
417 let mut st = self.edit.borrow_mut();
418 let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
419 if lo != hi {
420 st.text.replace_range(lo..hi, "");
421 st.cursor = lo;
422 st.anchor = lo;
423 } else if dir < 0 && st.cursor > 0 {
424 let cur = st.cursor;
425 let prev = prev_char_boundary(&st.text, cur);
426 st.text.replace_range(prev..cur, "");
427 st.cursor = prev;
428 st.anchor = prev;
429 } else if dir > 0 && st.cursor < st.text.len() {
430 let cur = st.cursor;
431 let next = next_char_boundary(&st.text, cur);
432 st.text.replace_range(cur..next, "");
433 }
434 drop(st);
435 self.mark_dirty();
436 }
437
438 fn move_cursor_to(&mut self, pos: usize, with_selection: bool) {
442 let mut st = self.edit.borrow_mut();
443 let p = pos.min(st.text.len());
444 st.cursor = p;
445 if !with_selection {
446 st.anchor = p;
447 }
448 }
449
450 fn move_char(&mut self, dir: i32, with_selection: bool) {
452 let st = self.edit.borrow();
453 let p = if dir < 0 {
454 prev_char_boundary(&st.text, st.cursor)
455 } else {
456 next_char_boundary(&st.text, st.cursor)
457 };
458 drop(st);
459 self.move_cursor_to(p, with_selection);
460 }
461
462 fn move_line(&mut self, dir: i32, with_selection: bool) {
464 if self.cached_lines.is_empty() {
465 return;
466 }
467 let cursor = self.edit.borrow().cursor;
468 let cur_line = self.line_for_cursor(cursor);
469 let target_line = if dir < 0 {
470 cur_line.saturating_sub(1)
471 } else {
472 (cur_line + 1).min(self.cached_lines.len() - 1)
473 };
474 if target_line == cur_line {
475 return;
476 }
477 let cur_x = self.pos_for_cursor(cursor).x - self.padding;
479 let line = &self.cached_lines[target_line];
481 let txt = &line.text;
482 let mut best_byte = 0usize;
483 let mut best_delta = f64::INFINITY;
484 let mut acc = 0.0_f64;
485 let mut prev_byte = 0usize;
486 for (i, _) in txt.char_indices().chain(std::iter::once((txt.len(), ' '))) {
487 let w = if i > prev_byte {
488 measure_advance(&self.font, &txt[prev_byte..i], self.font_size)
489 } else {
490 0.0
491 };
492 acc += w;
493 let d = (acc - cur_x).abs();
494 if d < best_delta {
495 best_delta = d;
496 best_byte = i;
497 }
498 prev_byte = i;
499 }
500 let target = line.start + best_byte;
501 self.move_cursor_to(target, with_selection);
502 }
503}
504
505mod widget_impl;