use crate::core::{Bounds, Point, Size, px};
use crate::view::NodeId;
use crate::view::context::ElementContext;
use crate::view::element::Element;
use crate::view::style::{Length, Style};
use crate::view::theme::Token;
use crate::widgets::text_input::{InputAppearance, InputLayout};
use std::cell::RefCell;
use std::ops::Range;
use std::rc::Rc;
pub struct InputElement {
text: String,
selection: Range<usize>,
head: usize,
preedit: Option<String>,
placeholder: String,
focused: bool,
layout: Rc<RefCell<InputLayout>>,
read_only: bool,
appearance: Option<InputAppearance>,
node: Option<NodeId>,
}
impl InputElement {
pub fn new(
text: String,
selection: Range<usize>,
head: usize,
preedit: Option<String>,
placeholder: String,
focused: bool,
layout: Rc<RefCell<InputLayout>>,
) -> Self {
Self {
text,
selection,
head,
preedit,
placeholder,
focused,
layout,
read_only: false,
appearance: None,
node: None,
}
}
pub fn read_only(mut self, read_only: bool) -> Self {
self.read_only = read_only;
self
}
pub fn appearance(mut self, appearance: Option<InputAppearance>) -> Self {
self.appearance = appearance;
self
}
fn display_text(&self) -> String {
match &self.preedit {
Some(preedit) => {
let mut shown = self.text.clone();
shown.insert_str(self.head, preedit);
shown
}
None => self.text.clone(),
}
}
}
impl Element for InputElement {
fn request_layout(&mut self, cx: &mut ElementContext) -> NodeId {
let mut style = Style { flex_grow: Some(1.0), height: Some(Length::Percent(1.0)), ..Default::default() };
if self.read_only {
let size = cx.text_style().size;
let shaped = cx.shape(&self.text, size, false);
style = Style {
width: Some(Length::Px(px(shaped.size.width.0.ceil() + 1.0))),
height: Some(Length::Px(px(shaped.line_height))),
flex_shrink: Some(0.0),
..Default::default()
};
}
let node = cx.request_node(&style, &[]);
self.node = Some(node);
node
}
fn prepaint(&mut self, _cx: &mut ElementContext) {}
fn paint(&mut self, cx: &mut ElementContext) {
let Some(node) = self.node else { return };
let bounds = cx.node_bounds(node);
let shown = self.display_text();
let empty = shown.is_empty();
let content = if empty { &self.placeholder } else { &shown };
let shaped = match &self.appearance {
Some(look) => {
let family = cx.font_family(&look.family);
cx.shape_in(content, px(look.font_size), true, family)
}
None => cx.shape(content, cx.text_style().size, false),
};
let line_height = shaped.line_height;
let top = bounds.origin.y.0 + (bounds.size.height.0 - line_height) / 2.0;
let caret_byte = self.head + self.preedit.as_ref().map_or(0, String::len);
let caret_x = if empty { 0.0 } else { shaped.caret_x(caret_byte) };
let width = bounds.size.width.0;
let mut layout = self.layout.borrow_mut();
if caret_x - layout.scroll_x > width - 2.0 {
layout.scroll_x = caret_x - width + 2.0;
} else if caret_x < layout.scroll_x {
layout.scroll_x = caret_x;
}
let left = bounds.origin.x.0 - layout.scroll_x;
cx.with_mask(bounds, |cx| {
if !self.selection.is_empty() && !empty {
let start = left + shaped.caret_x(self.selection.start);
let end = left + shaped.caret_x(self.selection.end);
let rect = Bounds::new(Point::new(px(start), px(top)), Size::new(px(end - start), px(line_height)));
let color = cx.theme.color(Token::Selection);
cx.fill_rect(rect, color);
}
let custom = self.appearance.as_ref().and_then(|look| look.text).filter(|_| !empty);
let color = custom.unwrap_or_else(|| cx.theme.color(if empty { Token::TextMuted } else { Token::Text }));
cx.paint_text(&shaped, Point::new(px(left), px(top)), color);
if let (Some(preedit), false) = (&self.preedit, empty) {
let start = left + shaped.caret_x(self.head);
let end = left + shaped.caret_x(self.head + preedit.len());
let underline = Bounds::new(
Point::new(px(start), px(top + line_height - 2.0)),
Size::new(px(end - start), px(1.0)),
);
let color = cx.theme.color(Token::Text);
cx.fill_rect(underline, color);
}
if self.focused && !self.read_only {
let caret = Bounds::new(Point::new(px(left + caret_x), px(top)), Size::new(px(1.5), px(line_height)));
let custom = self.appearance.as_ref().and_then(|look| look.caret);
let color = custom.unwrap_or_else(|| cx.theme.color(Token::Accent));
cx.fill_rect(caret, color);
cx.set_ime_area(caret);
}
});
layout.text_left = left;
layout.shaped = if empty { None } else { Some(shaped) };
}
}