use crate::core::FocusHandle;
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
const UNDO_LIMIT: usize = 64;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InputChanged(pub String);
#[derive(Clone, Default)]
pub struct InputLayout {
pub text_left: f32,
pub shaped: Option<Rc<crate::gpu::ShapedText>>,
pub scroll_x: f32,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct InputAppearance {
pub font_size: f32,
pub family: String,
pub text: Option<crate::core::Rgba>,
pub caret: Option<crate::core::Rgba>,
}
pub struct TextInput {
pub(crate) text: String,
pub(crate) anchor: usize,
pub(crate) head: usize,
pub(crate) preedit: Option<String>,
pub(crate) placeholder: String,
pub(crate) focus: FocusHandle,
pub(crate) read_only: bool,
pub(crate) plain: bool,
pub(crate) masked: bool,
pub(crate) compact: bool,
pub(crate) layout: Rc<RefCell<InputLayout>>,
pub(crate) appearance: Option<InputAppearance>,
undo: Vec<(String, usize)>,
redo: Vec<(String, usize)>,
}
impl TextInput {
pub fn new(focus: FocusHandle) -> Self {
Self {
text: String::new(),
anchor: 0,
head: 0,
preedit: None,
placeholder: String::new(),
focus,
read_only: false,
plain: false,
masked: false,
compact: false,
layout: Rc::default(),
appearance: None,
undo: Vec::new(),
redo: Vec::new(),
}
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn embedded(mut self) -> Self {
self.plain = true;
self
}
pub fn compact(mut self) -> Self {
self.compact = true;
self
}
pub fn selectable_label(focus: FocusHandle, text: impl Into<String>) -> Self {
let mut input = Self::new(focus);
(input.read_only, input.plain) = (true, true);
input.set_text(text);
input.anchor = 0;
input.head = 0;
input
}
pub fn set_appearance(&mut self, appearance: Option<InputAppearance>) {
self.appearance = appearance;
}
pub fn text(&self) -> &str {
&self.text
}
pub fn focus_handle(&self) -> FocusHandle {
self.focus
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.text = text.into();
self.anchor = self.text.len();
self.head = self.text.len();
}
pub fn selection(&self) -> Range<usize> {
self.anchor.min(self.head)..self.anchor.max(self.head)
}
fn previous_boundary(&self, index: usize) -> usize {
self.text[..index].char_indices().next_back().map_or(0, |(position, _)| position)
}
fn next_boundary(&self, index: usize) -> usize {
self.text[index..].chars().next().map_or(index, |character| index + character.len_utf8())
}
fn word_left(&self, index: usize) -> usize {
let prefix = &self.text[..index];
let trimmed = prefix.trim_end_matches(|c: char| !c.is_alphanumeric());
trimmed.trim_end_matches(|c: char| c.is_alphanumeric() || c == '_').len()
}
fn word_right(&self, index: usize) -> usize {
let suffix = &self.text[index..];
let skipped = suffix.len() - suffix.trim_start_matches(|c: char| !c.is_alphanumeric()).len();
let rest = &suffix[skipped..];
index + skipped + (rest.len() - rest.trim_start_matches(|c: char| c.is_alphanumeric() || c == '_').len())
}
fn move_to(&mut self, index: usize, extend: bool) {
self.head = index.min(self.text.len());
if !extend {
self.anchor = self.head;
}
}
pub fn move_left(&mut self, extend: bool) {
let selection = self.selection();
let target = if !extend && !selection.is_empty() { selection.start } else { self.previous_boundary(self.head) };
self.move_to(target, extend);
}
pub fn move_right(&mut self, extend: bool) {
let selection = self.selection();
let target = if !extend && !selection.is_empty() { selection.end } else { self.next_boundary(self.head) };
self.move_to(target, extend);
}
pub fn move_word(&mut self, forward: bool, extend: bool) {
let target = if forward { self.word_right(self.head) } else { self.word_left(self.head) };
self.move_to(target, extend);
}
pub fn move_home(&mut self, extend: bool) {
self.move_to(0, extend);
}
pub fn move_end(&mut self, extend: bool) {
self.move_to(self.text.len(), extend);
}
pub fn select_all(&mut self) {
self.anchor = 0;
self.head = self.text.len();
}
pub fn select_range(&mut self, range: Range<usize>) {
let clamp = |index: usize| {
let index = index.min(self.text.len());
(0..=index).rev().find(|&candidate| self.text.is_char_boundary(candidate)).unwrap_or(0)
};
(self.anchor, self.head) = (clamp(range.start), clamp(range.end));
}
fn remember(&mut self) {
self.undo.push((self.text.clone(), self.head));
if self.undo.len() > UNDO_LIMIT {
self.undo.remove(0);
}
self.redo.clear();
}
pub fn insert(&mut self, inserted: &str) {
let inserted: String = inserted.chars().filter(|c| *c != '\n' && *c != '\r').collect();
self.remember();
let selection = self.selection();
self.text.replace_range(selection.clone(), &inserted);
self.move_to(selection.start + inserted.len(), false);
self.preedit = None;
}
pub fn backspace(&mut self, by_word: bool) {
if self.selection().is_empty() {
let start = if by_word { self.word_left(self.head) } else { self.previous_boundary(self.head) };
self.anchor = start;
}
self.insert("");
}
pub fn delete(&mut self, by_word: bool) {
if self.selection().is_empty() {
self.anchor = if by_word { self.word_right(self.head) } else { self.next_boundary(self.head) };
}
self.insert("");
}
pub fn selected_text(&self) -> &str {
if self.masked { "" } else { &self.text[self.selection()] }
}
pub fn set_masked(&mut self, masked: bool) {
self.masked = masked;
}
pub fn set_preedit(&mut self, preedit: Option<String>) {
self.preedit = preedit.filter(|text| !text.is_empty());
}
pub fn undo(&mut self) {
if let Some((text, head)) = self.undo.pop() {
self.redo.push((std::mem::replace(&mut self.text, text), self.head));
self.move_to(head, false);
}
}
pub fn redo(&mut self) {
if let Some((text, head)) = self.redo.pop() {
self.undo.push((std::mem::replace(&mut self.text, text), self.head));
self.move_to(head, false);
}
}
pub fn byte_at(&self, window_x: f32) -> usize {
let layout = self.layout.borrow();
let Some(shaped) = layout.shaped.as_ref() else { return self.text.len() };
shaped.byte_at_x(window_x - layout.text_left).min(self.text.len())
}
pub fn click(&mut self, byte: usize, extend: bool, click_count: u32) {
match click_count {
2 => {
self.anchor = self.word_left(self.next_boundary(byte).min(self.text.len()));
self.head = self.word_right(self.anchor);
}
3 => self.select_all(),
_ => self.move_to(byte, extend),
}
}
}
#[cfg(test)]
#[path = "text_input_tests.rs"]
mod tests;