use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::sync::Arc;
use web_time::Instant;
use crate::cursor::{set_cursor_icon, CursorIcon};
use crate::draw_ctx::DrawCtx;
use crate::event::{Event, EventResult, Key, MouseButton};
use crate::geometry::{Point, Rect, Size};
use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
use crate::text::{measure_advance, measure_text_metrics, Font};
use crate::widget::Widget;
use crate::widgets::text_field_core::{next_char_boundary, prev_char_boundary, TextEditState};
fn clipboard_get() -> Option<String> {
crate::clipboard::get_text()
}
fn clipboard_set(text: &str) {
crate::clipboard::set_text(text);
}
#[derive(Clone, Debug)]
struct WrappedLine {
start: usize,
end: usize,
text: String,
hard_break: bool,
}
fn wrap_text_indexed(
font: &Arc<Font>,
text: &str,
font_size: f64,
max_width: f64,
) -> Vec<WrappedLine> {
let mut out: Vec<WrappedLine> = Vec::new();
let mut para_start = 0usize;
for (rel_end, chunk) in split_keep_newlines(text).enumerate() {
let _ = rel_end;
let para = chunk;
let para_abs_start = para_start;
let para_abs_end = para_abs_start + para.len();
let mut cursor = 0usize; let last_boundary = 0usize;
while cursor < para.len() {
let line_start = cursor;
let mut fit_end = line_start;
let mut last_word_end: Option<usize> = None;
let mut idx = line_start;
while idx < para.len() {
let next = next_char_boundary(para, idx);
let candidate = ¶[line_start..next];
let w = measure_text_metrics(font, candidate, font_size).width;
if w > max_width && fit_end > line_start {
break;
}
fit_end = next;
if next < para.len() {
let next_ch = para[next..].chars().next().unwrap_or(' ');
if next_ch.is_whitespace() {
last_word_end = Some(next);
}
}
idx = next;
}
let break_at = if fit_end < para.len() && last_word_end.is_some() {
last_word_end.unwrap()
} else {
fit_end.max(next_char_boundary(para, line_start))
};
let _ = last_boundary; let line_text = para[line_start..break_at].trim_end().to_string();
let abs_start = para_abs_start + line_start;
let abs_end = para_abs_start + break_at;
out.push(WrappedLine {
start: abs_start,
end: abs_end,
text: line_text,
hard_break: false,
});
let mut next_line_start = break_at;
while next_line_start < para.len() {
let ch = para[next_line_start..].chars().next().unwrap_or('x');
if !ch.is_whitespace() || ch == '\n' {
break;
}
next_line_start = next_char_boundary(para, next_line_start);
}
cursor = next_line_start;
if cursor >= para.len() {
break;
}
}
if out.is_empty() || out.last().map(|l| l.end).unwrap_or(0) != para_abs_end {
if para.is_empty() {
out.push(WrappedLine {
start: para_abs_start,
end: para_abs_end,
text: String::new(),
hard_break: false,
});
}
}
let source_end = para_abs_end + 1; let had_newline =
source_end <= text.len() && text.as_bytes().get(para_abs_end) == Some(&b'\n');
if had_newline {
if let Some(last) = out.last_mut() {
last.hard_break = true;
}
}
para_start = if had_newline {
source_end
} else {
para_abs_end
};
}
if out.is_empty() {
out.push(WrappedLine {
start: 0,
end: 0,
text: String::new(),
hard_break: false,
});
}
out
}
fn split_keep_newlines(text: &str) -> impl Iterator<Item = &str> + '_ {
text.split('\n')
}
pub struct TextArea {
bounds: Rect,
children: Vec<Box<dyn Widget>>, base: WidgetBase,
font: Arc<Font>,
font_size: f64,
padding: f64,
edit: Rc<RefCell<TextEditState>>,
cached_wrap_width: f64,
cached_lines: Vec<WrappedLine>,
cached_line_h: f64,
focused: bool,
hovered: bool,
selecting_drag: bool,
focus_time: Option<Instant>,
blink_last_phase: Cell<u64>,
}
impl TextArea {
pub fn new(font: Arc<Font>) -> Self {
Self {
bounds: Rect::default(),
children: Vec::new(),
base: WidgetBase::new(),
font,
font_size: 13.0,
padding: 8.0,
edit: Rc::new(RefCell::new(TextEditState::default())),
cached_wrap_width: -1.0,
cached_lines: Vec::new(),
cached_line_h: 0.0,
focused: false,
hovered: false,
selecting_drag: false,
focus_time: None,
blink_last_phase: Cell::new(0),
}
}
pub fn with_text(self, text: impl Into<String>) -> Self {
let t: String = text.into();
let cursor = t.len();
*self.edit.borrow_mut() = TextEditState {
text: t,
cursor,
anchor: cursor,
};
self
}
pub fn with_font_size(mut self, size: f64) -> Self {
self.font_size = size;
self
}
pub fn with_padding(mut self, p: f64) -> Self {
self.padding = p;
self
}
pub fn with_margin(mut self, m: Insets) -> Self {
self.base.margin = m;
self
}
pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
self.base.h_anchor = h;
self
}
pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
self.base.v_anchor = v;
self
}
pub fn with_min_size(mut self, s: Size) -> Self {
self.base.min_size = s;
self
}
pub fn with_max_size(mut self, s: Size) -> Self {
self.base.max_size = s;
self
}
pub fn text(&self) -> String {
self.edit.borrow().text.clone()
}
pub fn cursor(&self) -> usize {
self.edit.borrow().cursor
}
pub fn visual_line_count(&self) -> usize {
self.cached_lines.len()
}
fn refresh_wrap(&mut self, inner_w: f64) {
let st = self.edit.borrow();
let same_width = (self.cached_wrap_width - inner_w).abs() < 0.5;
if same_width && !self.cached_lines.is_empty() {
return;
}
let lines = wrap_text_indexed(&self.font, &st.text, self.font_size, inner_w.max(1.0));
self.cached_lines = lines;
self.cached_wrap_width = inner_w;
self.cached_line_h = self.font_size * 1.35;
}
fn mark_dirty(&mut self) {
self.cached_wrap_width = -1.0;
}
fn line_for_cursor(&self, byte_pos: usize) -> usize {
for (i, l) in self.cached_lines.iter().enumerate() {
if byte_pos >= l.start && byte_pos <= l.end {
return i;
}
}
self.cached_lines.len().saturating_sub(1)
}
fn byte_offset_at(&self, local: Point) -> usize {
if self.cached_lines.is_empty() || self.cached_line_h <= 0.0 {
return 0;
}
let inner_top_y = self.bounds.height - self.padding;
let rel_from_top = inner_top_y - local.y;
let mut line_idx = (rel_from_top / self.cached_line_h).floor() as isize;
if line_idx < 0 {
line_idx = 0;
}
if line_idx as usize >= self.cached_lines.len() {
line_idx = self.cached_lines.len() as isize - 1;
}
let line = &self.cached_lines[line_idx as usize];
let pad_x = self.padding;
let rel_x = (local.x - pad_x).max(0.0);
let txt = &line.text;
let mut best_byte = 0usize;
let mut best_delta = f64::INFINITY;
let mut acc = 0.0_f64;
let mut prev_byte = 0usize;
for (i, _c) in txt.char_indices().chain(std::iter::once((txt.len(), ' '))) {
let w_here = if i > prev_byte {
measure_advance(&self.font, &txt[prev_byte..i], self.font_size)
} else {
0.0
};
acc += w_here;
let d = (acc - rel_x).abs();
if d < best_delta {
best_delta = d;
best_byte = i;
}
prev_byte = i;
}
line.start + best_byte
}
fn pos_for_cursor(&self, byte_pos: usize) -> Point {
if self.cached_lines.is_empty() {
return Point::ORIGIN;
}
let line_idx = self.line_for_cursor(byte_pos);
let line = &self.cached_lines[line_idx];
let offset = byte_pos.saturating_sub(line.start).min(line.text.len());
let x = self.padding + measure_advance(&self.font, &line.text[..offset], self.font_size);
let inner_top_y = self.bounds.height - self.padding;
let line_top = inner_top_y - line_idx as f64 * self.cached_line_h;
let line_bottom = line_top - self.cached_line_h;
Point::new(x, line_bottom)
}
fn insert_str(&mut self, s: &str) {
let mut st = self.edit.borrow_mut();
let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
let lo = lo.min(st.text.len());
let hi = hi.min(st.text.len());
st.text.replace_range(lo..hi, s);
st.cursor = lo + s.len();
st.anchor = st.cursor;
drop(st);
self.mark_dirty();
}
fn delete(&mut self, dir: i32) {
let mut st = self.edit.borrow_mut();
let (lo, hi) = (st.cursor.min(st.anchor), st.cursor.max(st.anchor));
if lo != hi {
st.text.replace_range(lo..hi, "");
st.cursor = lo;
st.anchor = lo;
} else if dir < 0 && st.cursor > 0 {
let cur = st.cursor;
let prev = prev_char_boundary(&st.text, cur);
st.text.replace_range(prev..cur, "");
st.cursor = prev;
st.anchor = prev;
} else if dir > 0 && st.cursor < st.text.len() {
let cur = st.cursor;
let next = next_char_boundary(&st.text, cur);
st.text.replace_range(cur..next, "");
}
drop(st);
self.mark_dirty();
}
fn move_cursor_to(&mut self, pos: usize, with_selection: bool) {
let mut st = self.edit.borrow_mut();
let p = pos.min(st.text.len());
st.cursor = p;
if !with_selection {
st.anchor = p;
}
}
fn move_char(&mut self, dir: i32, with_selection: bool) {
let st = self.edit.borrow();
let p = if dir < 0 {
prev_char_boundary(&st.text, st.cursor)
} else {
next_char_boundary(&st.text, st.cursor)
};
drop(st);
self.move_cursor_to(p, with_selection);
}
fn move_line(&mut self, dir: i32, with_selection: bool) {
if self.cached_lines.is_empty() {
return;
}
let cursor = self.edit.borrow().cursor;
let cur_line = self.line_for_cursor(cursor);
let target_line = if dir < 0 {
cur_line.saturating_sub(1)
} else {
(cur_line + 1).min(self.cached_lines.len() - 1)
};
if target_line == cur_line {
return;
}
let cur_x = self.pos_for_cursor(cursor).x - self.padding;
let line = &self.cached_lines[target_line];
let txt = &line.text;
let mut best_byte = 0usize;
let mut best_delta = f64::INFINITY;
let mut acc = 0.0_f64;
let mut prev_byte = 0usize;
for (i, _) in txt.char_indices().chain(std::iter::once((txt.len(), ' '))) {
let w = if i > prev_byte {
measure_advance(&self.font, &txt[prev_byte..i], self.font_size)
} else {
0.0
};
acc += w;
let d = (acc - cur_x).abs();
if d < best_delta {
best_delta = d;
best_byte = i;
}
prev_byte = i;
}
let target = line.start + best_byte;
self.move_cursor_to(target, with_selection);
}
}
mod widget_impl;