use std::cell::RefCell;
use std::rc::Rc;
use smudge_core::element::{Element, Value};
use smudge_core::geometry::{Color, Corners, Point, Rect, Size};
use smudge_core::input::{Handled, Key, KeyEvent};
use smudge_core::paint::PaintOp;
use smudge_core::reactive::{RwSignal, prelude::*};
use smudge_core::semantics::{Role, Semantics};
use smudge_core::style::Length;
use smudge_core::theme::ColorToken;
use smudge_core::tree::Dirty;
use smudge_core::widget::{AvailableSpace, MeasureInput, PaintCx, Widget};
use smudge_core::window::Cursor;
use crate::chrome::{
Direction, InputChrome, ParagraphCache, Placeholder, SELECTION_ALPHA, apply_edit_count,
apply_move, apply_outside_write, caret_node, copy_selection, cut_selection, handle_commit,
handle_focus, handle_move, handle_preedit, handle_press, handle_release, is_shortcut,
outside_write, paint_paragraphs, paint_selection, types_text,
};
use crate::style::{TextStyle, current_text_style};
pub struct TextInput {
chrome: Rc<InputChrome>,
placeholder: RefCell<Placeholder>,
style: TextStyle,
runs: RefCell<Option<ParagraphCache>>,
}
impl TextInput {
fn new(chrome: Rc<InputChrome>) -> TextInput {
TextInput {
chrome,
placeholder: RefCell::new(Placeholder::new()),
style: TextStyle::default(),
runs: RefCell::new(None),
}
}
fn text_color(&self, cx: &PaintCx<'_>) -> Color {
match &cx.style.paint.text_color {
Some(color) => color.resolve(cx.theme),
None => self.style.color.resolve(cx.theme),
}
}
}
impl Widget for TextInput {
fn semantics(&self) -> Option<Semantics> {
let label = (!self.placeholder.borrow().text().is_empty())
.then(|| self.placeholder.borrow().text().into());
Some(Semantics {
role: Role::TextInput,
label,
value: Some(self.chrome.editor.text()),
})
}
fn measure(&mut self, input: MeasureInput) -> Option<Size> {
let width = match input.available_width {
AvailableSpace::Definite(width) => width.max(0.0),
AvailableSpace::MinContent | AvailableSpace::MaxContent => 0.0,
};
Some(Size {
width,
height: self.style.size * self.style.line_height,
})
}
fn paint(&self, cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
let color = self.text_color(cx);
let inset = Point {
x: cx.content.x0 - cx.rect.x0,
y: cx.content.y0 - cx.rect.y0,
};
self.chrome.painted.set(Some((cx.content, color, inset)));
let (scroll_x, scroll_y) = self.chrome.place(cx.content, color);
self.chrome
.line_height
.set(self.style.size * self.style.line_height);
let text_origin = Point {
x: cx.content.x0 - scroll_x,
y: cx.content.y0 - scroll_y,
};
let selection_color = Color {
a: SELECTION_ALPHA,
..cx.theme.color(ColorToken::ACCENT)
};
paint_selection(
&self.chrome,
text_origin,
(f32::NEG_INFINITY, f32::INFINITY),
selection_color,
out,
);
for area in self.chrome.editor.compose_geometry() {
out.push(PaintOp::FillRect {
rect: Rect::new(
text_origin.x + area.x0,
text_origin.y + area.y0,
text_origin.x + area.x1,
text_origin.y + area.y1,
),
radius: Corners::ZERO,
color,
});
}
if self.chrome.editor.is_empty()
&& !self.chrome.editor.is_composing()
&& !self.placeholder.borrow().text().is_empty()
{
let muted = cx.theme.color(ColorToken::TEXT_MUTED);
self.placeholder
.borrow_mut()
.paint(&self.style, text_origin, muted, out);
} else {
paint_paragraphs(
&mut self.runs.borrow_mut(),
&self.chrome.editor,
text_origin,
color,
(f32::NEG_INFINITY, f32::INFINITY),
out,
);
}
}
fn ime_area(&self) -> Option<Rect> {
let (content, _, _) = self.chrome.painted.get()?;
let area = self.chrome.editor.ime_area();
let scroll_x = self.chrome.scroll_x.get();
let scroll_y = self.chrome.scroll_y.get();
Some(Rect::new(
content.x0 + area.x0 - scroll_x,
content.y0 + area.y0 - scroll_y,
content.x0 + area.x1 - scroll_x,
content.y0 + area.y1 - scroll_y,
))
}
}
pub fn text_input(value: RwSignal<String>) -> Element<TextInput> {
let chrome = Rc::new(InputChrome::new(value.clone(), false));
let key_chrome = Rc::clone(&chrome);
let press_chrome = Rc::clone(&chrome);
let move_chrome = Rc::clone(&chrome);
let up_chrome = Rc::clone(&chrome);
let focus_chrome = Rc::clone(&chrome);
let commit_chrome = Rc::clone(&chrome);
let preedit_chrome = Rc::clone(&chrome);
let edits_chrome = Rc::clone(&chrome);
let caret_chrome = Rc::clone(&chrome);
let value_chrome = Rc::clone(&chrome);
Element::new(TextInput::new(Rc::clone(&chrome)))
.clip(true)
.cursor(Cursor::Text)
.min_height(one_line)
.bind(AmbientStyleKey, current_text_style, apply_ambient_style)
.bind(
ValueKey,
move || outside_write(&value_chrome, &value),
apply_value,
)
.bind(EditsKey, move || edits_chrome.edits.get(), apply_edits)
.on_key(move |event| handle_key(&key_chrome, event))
.on_pointer_down(move |position| handle_press(&press_chrome, position))
.on_pointer_move(move |position| handle_move(&move_chrome, position))
.on_pointer_up(move |position| handle_release(&up_chrome, position))
.on_focus(move |focused| handle_focus(&focus_chrome, focused))
.on_ime_commit(move |text| handle_commit(&commit_chrome, text))
.on_ime_preedit(move |text, cursor| handle_preedit(&preedit_chrome, text, cursor))
.child(caret_node(&caret_chrome))
}
pub trait TextInputExt: Sized {
fn placeholder(self, text: impl Into<Value<String>>) -> Self;
}
struct AmbientStyleKey;
struct ValueKey;
struct EditsKey;
struct PlaceholderKey;
impl TextInputExt for Element<TextInput> {
fn placeholder(self, text: impl Into<Value<String>>) -> Self {
self.bind(PlaceholderKey, text, set_placeholder)
}
}
fn one_line() -> Length {
let style = current_text_style();
Length::px(style.size * style.line_height)
}
fn apply_ambient_style(input: &mut TextInput, ambient: TextStyle) -> Dirty {
if input.style == ambient {
return Dirty::Clean;
}
input.style = ambient;
input.chrome.editor.set_style(&input.style);
input.placeholder.borrow_mut().invalidate_style();
*input.runs.borrow_mut() = None;
Dirty::Layout
}
fn apply_value(input: &mut TextInput, value: Option<String>) -> Dirty {
match value {
Some(value) => apply_outside_write(&input.chrome, &value),
None => Dirty::Clean,
}
}
fn apply_edits(input: &mut TextInput, _edits: u64) -> Dirty {
apply_edit_count(&input.chrome)
}
fn set_placeholder(input: &mut TextInput, text: String) -> Dirty {
let mut placeholder = input.placeholder.borrow_mut();
if placeholder.text() == text {
return Dirty::Clean;
}
placeholder.set_text(text);
Dirty::Paint
}
fn handle_key(chrome: &InputChrome, event: &KeyEvent) -> Handled {
if !event.pressed {
return Handled::No;
}
let modifiers = event.modifiers;
let by_word = modifiers.control || modifiers.alt;
match &event.key {
Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("a") => {
chrome.editor.select_all();
chrome.touched();
Handled::Yes
}
Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("c") => {
copy_selection(chrome);
Handled::Yes
}
Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("x") => {
if cut_selection(chrome) {
chrome.touched();
}
Handled::Yes
}
Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("v") => {
paste_from_clipboard(chrome);
Handled::Yes
}
Key::Character(text) if is_shortcut(modifiers) && text.eq_ignore_ascii_case("z") => {
if modifiers.shift {
chrome.editor.redo();
} else {
chrome.editor.undo();
}
chrome.touched();
Handled::Yes
}
Key::Character(text)
if cfg!(not(target_os = "macos"))
&& is_shortcut(modifiers)
&& text.eq_ignore_ascii_case("y") =>
{
chrome.editor.redo();
chrome.touched();
Handled::Yes
}
Key::Backspace => {
if by_word {
chrome.editor.backspace_word();
} else {
chrome.editor.backspace();
}
chrome.touched();
Handled::Yes
}
Key::Delete => {
if by_word {
chrome.editor.delete_word();
} else {
chrome.editor.delete();
}
chrome.touched();
Handled::Yes
}
Key::ArrowLeft => {
apply_move(chrome, modifiers.shift, by_word, Direction::Left);
Handled::Yes
}
Key::ArrowRight => {
apply_move(chrome, modifiers.shift, by_word, Direction::Right);
Handled::Yes
}
Key::Home => {
if modifiers.shift {
chrome.editor.extend_to_start();
} else {
chrome.editor.move_to_start();
}
chrome.touched();
Handled::Yes
}
Key::End => {
if modifiers.shift {
chrome.editor.extend_to_end();
} else {
chrome.editor.move_to_end();
}
chrome.touched();
Handled::Yes
}
Key::Character(text) => {
if !types_text(modifiers) {
return Handled::No;
}
chrome.editor.insert(text);
chrome.touched();
Handled::Yes
}
Key::Space => {
if !types_text(modifiers) {
return Handled::No;
}
chrome.editor.insert(" ");
chrome.touched();
Handled::Yes
}
_ => Handled::No,
}
}
fn paste_from_clipboard(chrome: &InputChrome) {
if let Some(text) = smudge_core::clipboard::with(|slot| slot.get()).flatten() {
chrome.editor.paste(&text);
chrome.touched();
}
}