use crate::core::px;
use crate::view::keymap::Keymap;
use crate::view::prelude::*;
use crate::widgets::text_input::{InputChanged, InputLayout, TextInput};
use crate::widgets::text_input_element::InputElement;
macro_rules! input_actions {
($($name:ident = $label:literal => $key:literal, |$input:ident, $cx:ident| $body:expr;)*) => {
$( pub const $name: Action = Action(concat!("input::", $label)); )*
#[allow(unused_variables)]
const OPERATIONS: &[(Action, fn(&mut TextInput, &mut ListenerContext<TextInput>) -> bool)] = &[
$( ($name, |$input, $cx| $body), )*
];
pub fn bind_text_input_keys(keymap: &mut Keymap) {
$(
if let Err(error) = keymap.bind($key, Some($name), Some("TextInput")) {
eprintln!("atajo inválido: {error}");
}
)*
}
};
}
input_actions! {
BACKSPACE = "Backspace" => "backspace", |input, cx| { input.backspace(false); true };
DELETE_WORD_LEFT = "DeleteWordLeft" => "alt-backspace", |input, cx| { input.backspace(true); true };
DELETE = "Delete" => "delete", |input, cx| { input.delete(false); true };
LEFT = "Left" => "left", |input, cx| { input.move_left(false); false };
RIGHT = "Right" => "right", |input, cx| { input.move_right(false); false };
SELECT_LEFT = "SelectLeft" => "shift-left", |input, cx| { input.move_left(true); false };
SELECT_RIGHT = "SelectRight" => "shift-right", |input, cx| { input.move_right(true); false };
WORD_LEFT = "WordLeft" => "alt-left", |input, cx| { input.move_word(false, false); false };
WORD_RIGHT = "WordRight" => "alt-right", |input, cx| { input.move_word(true, false); false };
HOME = "Home" => "home", |input, cx| { input.move_home(false); false };
END = "End" => "end", |input, cx| { input.move_end(false); false };
SELECT_HOME = "SelectHome" => "shift-home", |input, cx| { input.move_home(true); false };
SELECT_END = "SelectEnd" => "shift-end", |input, cx| { input.move_end(true); false };
SELECT_ALL = "SelectAll" => "mod-a", |input, cx| { input.select_all(); false };
UNDO = "Undo" => "mod-z", |input, cx| { input.undo(); true };
REDO = "Redo" => "mod-shift-z", |input, cx| { input.redo(); true };
COPY = "Copy" => "mod-c", |input, cx| { copy(input, cx); false };
CUT = "Cut" => "mod-x", |input, cx| { copy(input, cx); input.insert(""); true };
PASTE = "Paste" => "mod-v", |input, cx| { paste(input, cx) };
}
fn copy(input: &TextInput, cx: &mut ListenerContext<TextInput>) {
let selected = input.selected_text().to_owned();
if selected.is_empty() {
return;
}
if let Err(error) = cx.clipboard.write(selected) {
eprintln!("no se pudo copiar: {error}");
}
}
fn paste(input: &mut TextInput, cx: &mut ListenerContext<TextInput>) -> bool {
match cx.clipboard.read() {
Some(text) => {
input.insert(&text);
true
}
None => false,
}
}
const READ_ONLY_ACTIONS: &[Action] = &[
LEFT,
RIGHT,
SELECT_LEFT,
SELECT_RIGHT,
WORD_LEFT,
WORD_RIGHT,
HOME,
END,
SELECT_HOME,
SELECT_END,
SELECT_ALL,
COPY,
];
impl Render for TextInput {
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement {
let focused = cx.is_focused(&self.focus);
let read_only = self.read_only;
let mut field = div()
.role(if read_only { Role::Label } else { Role::TextInput })
.label(if self.text.is_empty() { self.placeholder.clone() } else { self.text.clone() })
.key_context("TextInput")
.track_focus(&self.focus)
.flex_row()
.items_center()
.when(self.plain && !read_only, |field| field.w_full().h(px(32.0)))
.when(!self.plain && !self.compact, |field| {
field
.w_full()
.h(px(32.0))
.px_3()
.rounded_md()
.border_1()
.border_color(Token::Border)
.focus(|style| style.border_color(Token::BorderFocused))
.bg(Token::Canvas)
})
.when(self.compact, |field| {
field
.w_full()
.h(px(20.0))
.px(px(4.0))
.rounded_sm()
.border_1()
.border_color(Token::BorderFocused)
.bg(Token::Canvas)
})
.text_sm()
.on_text_input(cx.listener(|this: &mut TextInput, event: &TextInputEvent, cx| {
if this.read_only {
return;
}
match event {
TextInputEvent::Insert(text) => {
this.insert(text);
cx.emit(InputChanged(this.text.clone()));
}
TextInputEvent::Preedit { text, .. } => this.set_preedit(Some(text.clone())),
}
cx.notify();
}))
.cursor_text()
.on_mouse_down(cx.listener(|this: &mut TextInput, event: &ClickEvent, cx| {
let byte = this.byte_at(event.position.x.0);
this.click(byte, event.modifiers.shift, event.click_count);
cx.notify();
}))
.on_drag(cx.listener(|this: &mut TextInput, event: &DragEvent, cx| {
let byte = this.byte_at(event.position.x.0);
this.click(byte, true, 1);
cx.notify();
}));
for (action, operation) in
OPERATIONS.iter().filter(|(action, _)| !read_only || READ_ONLY_ACTIONS.contains(action))
{
let operation = *operation;
field = field.on_action(
*action,
cx.listener(move |this: &mut TextInput, _: &ActionEvent, cx| {
if operation(this, cx) {
cx.emit(InputChanged(this.text.clone()));
}
cx.notify();
}),
);
}
let layout: std::rc::Rc<std::cell::RefCell<InputLayout>> = self.layout.clone();
let (shown, selection, head) = if self.masked {
let chars = |end: usize| self.text[..end.min(self.text.len())].chars().count();
let selection = self.selection();
("*".repeat(self.text.chars().count()), chars(selection.start)..chars(selection.end), chars(self.head))
} else {
(self.text.clone(), self.selection(), self.head)
};
field
.child(
InputElement::new(
shown,
selection,
head,
self.preedit.clone(),
self.placeholder.clone(),
focused,
layout,
)
.read_only(read_only)
.appearance(self.appearance.clone()),
)
.into_element()
}
}