mod handlers;
mod layout;
mod mouse;
mod nav;
mod paint;
use std::rc::Rc;
use slate_reactive::Signal;
use slate_text::MultilineLayout;
use taffy::prelude::*;
use crate::context::{LayoutCtx, PaintCtx, PrepaintCtx};
use crate::element::{Element, IntoElement, Sealed};
use crate::event::{ImeHandlers, KeyHandlers, MouseHandlers};
use crate::focus::FocusableEntry;
use crate::hit_test::{CursorStyle, HitRegion};
use crate::text_system::PlatformFont;
use crate::types::{Bounds, ElementId, LayoutId};
#[derive(Clone, Debug)]
pub struct TextAreaStyle {
pub font_size: f32,
pub color: [f32; 4],
pub background: Option<[f32; 4]>,
pub caret_color: [f32; 4],
pub selection_color: [f32; 4],
pub preedit_underline_color: [f32; 4],
pub preedit_selection_color: [f32; 4],
pub width: f32,
pub min_lines: usize,
}
impl Default for TextAreaStyle {
fn default() -> Self {
Self {
font_size: 14.0,
color: [1.0, 1.0, 1.0, 1.0],
background: None,
caret_color: [1.0, 1.0, 1.0, 1.0],
selection_color: [0.4, 0.6, 1.0, 0.3],
preedit_underline_color: [1.0, 1.0, 1.0, 1.0],
preedit_selection_color: [0.4, 0.6, 1.0, 0.3],
width: 300.0,
min_lines: 1,
}
}
}
pub struct TextArea {
value: Signal<String>,
style: TextAreaStyle,
font: Option<PlatformFont>,
last_id: Option<ElementId>,
}
impl TextArea {
pub fn new(value: Signal<String>) -> Self {
Self {
value,
style: TextAreaStyle::default(),
font: None,
last_id: None,
}
}
pub fn style(mut self, s: TextAreaStyle) -> Self {
self.style = s;
self
}
}
pub struct TextAreaLayoutState {
layout: Rc<MultilineLayout>,
}
pub struct TextAreaPaintState {
element_id: ElementId,
focused: bool,
}
impl Sealed for TextArea {}
impl Element for TextArea {
type LayoutState = TextAreaLayoutState;
type PaintState = TextAreaPaintState;
fn request_layout(&mut self, cx: &mut LayoutCtx) -> (LayoutId, Self::LayoutState) {
let scale = cx.scale_factor as f32;
if self.font.is_none() {
match cx
.text
.load_font_from_bytes(slate_text::TEST_FONT, self.style.font_size, scale)
{
Ok(f) => self.font = Some(f),
Err(e) => {
log::error!("TextArea: font load failed: {e}; rendering zero-size");
let node_id = cx
.taffy
.new_leaf(taffy::Style::default())
.unwrap_or_else(|_| taffy::NodeId::from(u64::MAX));
return (
LayoutId(node_id),
TextAreaLayoutState {
layout: Rc::new(empty_layout()),
},
);
}
}
}
let font = self.font.as_ref().unwrap();
let current = self.value.get_untracked();
let (layout, height) = match layout::build_layout(
cx.text,
font,
¤t,
self.style.width,
self.style.min_lines,
) {
Ok(pair) => pair,
Err(e) => {
log::error!("TextArea: layout build failed: {e}; rendering zero-size");
(Rc::new(empty_layout()), 0.0)
}
};
let node_id = match cx.taffy.new_leaf(taffy::Style {
size: taffy::Size {
width: Dimension::length(self.style.width),
height: Dimension::length(height),
},
..Default::default()
}) {
Ok(id) => id,
Err(e) => {
log::error!("TextArea: Taffy new_leaf failed: {e}");
taffy::NodeId::from(u64::MAX)
}
};
(LayoutId(node_id), TextAreaLayoutState { layout })
}
fn prepaint(
&mut self,
bounds: Bounds,
_layout_state: &mut Self::LayoutState,
cx: &mut PrepaintCtx,
) -> Self::PaintState {
let element_id = cx.allocate_id::<TextArea>();
self.last_id = Some(element_id);
cx.register_hit_region(
HitRegion::new(element_id, bounds, 0).with_cursor(CursorStyle::Text),
);
cx.register_focusable(
FocusableEntry {
id: element_id,
tab_index: 0,
focus_ring: true,
},
bounds,
0.0,
);
let state_rc = cx.register_ime_state(element_id);
{
let mut state = state_rc.borrow_mut();
if state.text.is_empty() {
let v = self.value.get_untracked();
if !v.is_empty() {
state.caret = v.len();
state.text = v;
}
}
state.seed_undo_baseline();
}
let focused = cx.focused_element() == Some(element_id);
cx.register_key_handlers(
element_id,
KeyHandlers {
on_key_down: Some(handlers::build_key_down_handler(self.value.clone())),
on_key_up: None,
on_text_input: Some(handlers::build_text_input_handler(self.value.clone())),
},
);
cx.register_ime_handlers(
element_id,
ImeHandlers {
on_ime_preedit: Some(handlers::build_ime_preedit_handler()),
on_ime_commit: Some(handlers::build_ime_commit_handler(self.value.clone())),
on_ime_enabled: None,
on_ime_disabled: None,
},
);
cx.register_mouse_handlers(
element_id,
MouseHandlers {
on_mouse_down: Some(mouse::build_mouse_down_handler()),
on_mouse_move: Some(mouse::build_mouse_move_handler()),
on_mouse_up: Some(mouse::build_mouse_up_handler()),
},
);
TextAreaPaintState {
element_id,
focused,
}
}
fn paint(
&mut self,
bounds: Bounds,
layout_state: &mut Self::LayoutState,
paint_state: &mut Self::PaintState,
cx: &mut PaintCtx,
) {
let font = match &self.font {
Some(f) => f,
None => return,
};
paint::paint(
&self.style,
font,
&layout_state.layout,
paint_state.element_id,
paint_state.focused,
bounds,
cx,
);
}
fn id(&self) -> Option<ElementId> {
self.last_id
}
}
#[cfg(feature = "test-hooks")]
pub fn build_key_down_handler_for_test(value: Signal<String>) -> crate::event::ElementKeyHandler {
handlers::build_key_down_handler(value)
}
#[cfg(feature = "test-hooks")]
pub fn build_mouse_handlers_for_test() -> MouseHandlers {
MouseHandlers {
on_mouse_down: Some(mouse::build_mouse_down_handler()),
on_mouse_move: Some(mouse::build_mouse_move_handler()),
on_mouse_up: Some(mouse::build_mouse_up_handler()),
}
}
#[cfg(feature = "test-hooks")]
pub fn build_text_input_handler_for_test(
value: Signal<String>,
) -> crate::event::ElementTextInputHandler {
handlers::build_text_input_handler(value)
}
#[cfg(feature = "test-hooks")]
pub fn build_ime_handlers_for_test(value: Signal<String>) -> ImeHandlers {
ImeHandlers {
on_ime_preedit: Some(handlers::build_ime_preedit_handler()),
on_ime_commit: Some(handlers::build_ime_commit_handler(value)),
on_ime_enabled: None,
on_ime_disabled: None,
}
}
fn empty_layout() -> MultilineLayout {
MultilineLayout {
lines: Vec::new(),
total_height_lpx: 0.0,
line_height_lpx: 0.0,
}
}
impl IntoElement for TextArea {
type Element = Self;
fn into_element(self) -> Self {
self
}
}