mod handlers;
use std::time::Instant;
use slate_reactive::Signal;
use slate_renderer::Lpx;
use slate_renderer::scene::RectInstance;
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};
use handlers::{
build_ime_commit_handler, build_ime_preedit_handler, build_key_down_handler,
build_mouse_down_handler, build_mouse_move_handler, build_mouse_up_handler,
build_text_input_handler,
};
#[derive(Clone, Debug)]
pub struct TextFieldStyle {
pub font_size: f32,
pub color: [f32; 4],
pub background: Option<[f32; 4]>,
pub caret_color: [f32; 4],
pub preedit_selection_color: [f32; 4],
pub width: f32,
}
impl Default for TextFieldStyle {
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],
preedit_selection_color: [0.4, 0.6, 1.0, 0.3],
width: 200.0,
}
}
}
pub struct TextField {
value: Signal<String>,
style: TextFieldStyle,
font: Option<PlatformFont>,
last_id: Option<ElementId>,
}
impl TextField {
pub fn new(value: Signal<String>) -> Self {
Self {
value,
style: TextFieldStyle::default(),
font: None,
last_id: None,
}
}
pub fn style(mut self, s: TextFieldStyle) -> Self {
self.style = s;
self
}
}
pub struct TextFieldLayoutState {
line_height: f32,
}
pub struct TextFieldPaintState {
element_id: ElementId,
focused: bool,
}
impl Sealed for TextField {}
impl Element for TextField {
type LayoutState = TextFieldLayoutState;
type PaintState = TextFieldPaintState;
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!("TextField: 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), TextFieldLayoutState { line_height: 0.0 });
}
}
}
let font = self.font.as_ref().unwrap();
let current = self.value.get_untracked();
let (intrinsic_w, line_height) = if current.is_empty() {
let shaped =
cx.text
.shape_line(font, "M")
.unwrap_or_else(|_| slate_text::types::ShapedLine {
glyphs: Vec::new(),
width_lpx: 0.0,
ascent_lpx: self.style.font_size,
descent_lpx: 0.0,
y_offset_lpx: 0.0,
base_direction: slate_text::Direction::Ltr,
runs: Vec::new(),
});
(self.style.width, shaped.ascent_lpx - shaped.descent_lpx)
} else {
match cx.text.shape_line_bidi(font, ¤t) {
Ok(shaped) => (
shaped.width_lpx.max(self.style.width),
shaped.ascent_lpx - shaped.descent_lpx,
),
Err(_) => (self.style.width, self.style.font_size),
}
};
let node_id = match cx.taffy.new_leaf(taffy::Style {
size: taffy::Size {
width: Dimension::length(intrinsic_w),
height: Dimension::length(line_height),
},
..Default::default()
}) {
Ok(id) => id,
Err(e) => {
log::error!("TextField: Taffy new_leaf failed: {e}");
taffy::NodeId::from(u64::MAX)
}
};
(LayoutId(node_id), TextFieldLayoutState { line_height })
}
fn prepaint(
&mut self,
bounds: Bounds,
_layout_state: &mut Self::LayoutState,
cx: &mut PrepaintCtx,
) -> Self::PaintState {
let element_id = cx.allocate_id::<TextField>();
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(build_key_down_handler(self.value.clone())),
on_key_up: None,
on_text_input: Some(build_text_input_handler(self.value.clone())),
},
);
cx.register_ime_handlers(
element_id,
ImeHandlers {
on_ime_preedit: Some(build_ime_preedit_handler()),
on_ime_commit: Some(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(build_mouse_down_handler()),
on_mouse_move: Some(build_mouse_move_handler()),
on_mouse_up: Some(build_mouse_up_handler()),
},
);
TextFieldPaintState {
element_id,
focused,
}
}
fn paint(
&mut self,
bounds: Bounds,
layout_state: &mut Self::LayoutState,
paint_state: &mut Self::PaintState,
cx: &mut PaintCtx,
) {
let element_id = paint_state.element_id;
let line_height = layout_state.line_height;
let scale = cx.scale_factor as f32;
if let Some(bg) = self.style.background {
cx.scene.push_rect(RectInstance {
rect: [
Lpx(bounds.origin.x),
Lpx(bounds.origin.y),
Lpx(bounds.size.width),
Lpx(line_height),
],
color: bg,
corner_radius: Lpx(0.0),
_pad: [0.0; 3],
});
}
let font = match &self.font {
Some(f) => f,
None => return,
};
let (committed_text, caret_byte, caret_affinity, preedit_snapshot, selection_anchor) = {
match cx.ime_registry.borrow().get(element_id) {
Some(rc) => {
let s = rc.borrow();
(
s.text.clone(),
s.caret,
s.caret_affinity,
s.preedit.clone(),
s.selection_anchor,
)
}
None => (
self.value.get_untracked(),
0,
slate_text::Affinity::Downstream,
None,
None,
),
}
};
let caret_safe = caret_byte.min(committed_text.len());
let display_string = if let Some(ref p) = preedit_snapshot {
format!(
"{}{}{}",
&committed_text[..caret_safe],
&p.text,
&committed_text[caret_safe..]
)
} else {
committed_text.clone()
};
let shaped = match cx.text.shape_line_bidi(font, &display_string) {
Ok(s) => s,
Err(e) => {
log::error!("TextField: shape_line_bidi failed: {e}");
return;
}
};
let baseline_x = bounds.origin.x;
let baseline_y = bounds.origin.y + shaped.ascent_lpx;
let display_caret = caret_safe;
let pixel_x =
|byte: usize| -> f32 { slate_text::pixel_x_at_byte(&shaped, &display_string, byte) };
let effective_affinity =
crate::ime::caret_affinity_for_display(preedit_snapshot.is_some(), caret_affinity);
let caret_display_byte =
crate::ime::caret_display_byte(display_caret, preedit_snapshot.as_ref());
let caret_pixel_x = if shaped.runs.is_empty() {
pixel_x(caret_display_byte)
} else {
slate_text::run_caret_x_at_affinity(&shaped, caret_display_byte, effective_affinity)
};
if preedit_snapshot.is_none()
&& let Some(anchor) = selection_anchor
{
debug_assert!(
committed_text.is_char_boundary(anchor.min(committed_text.len())),
"selection_anchor must land on a char boundary"
);
let anchor_safe = anchor.min(committed_text.len());
let (lo, hi) = if anchor_safe <= caret_safe {
(anchor_safe, caret_safe)
} else {
(caret_safe, anchor_safe)
};
if lo < hi {
let spans: Vec<(f32, f32)> = if !shaped.runs.is_empty() {
slate_text::run_selection_rects(&shaped, lo, hi)
} else {
let lo_px = pixel_x(lo);
let hi_px = pixel_x(hi);
let w = (hi_px - lo_px).max(0.0);
if w > 0.0 {
vec![(lo_px, w)]
} else {
Vec::new()
}
};
for (x_start, w) in spans {
cx.scene.push_rect(RectInstance {
rect: [
Lpx(bounds.origin.x + x_start),
Lpx(bounds.origin.y),
Lpx(w),
Lpx(line_height),
],
color: self.style.preedit_selection_color,
corner_radius: Lpx(0.0),
_pad: [0.0; 3],
});
}
}
}
match cx.text.rasterize_text_run(
font,
&shaped,
[baseline_x, baseline_y],
self.style.color,
cx.glyph_cache,
cx.glyph_atlas,
cx.queue,
) {
Ok(glyphs) => {
for glyph in glyphs {
cx.scene.push_glyph(glyph);
}
}
Err(e) => {
log::error!("TextField: rasterize_text_run failed: {e}");
return;
}
}
if let Some(ref preedit) = preedit_snapshot {
let preedit_end_byte = display_caret + preedit.text.len();
let preedit_start_px = pixel_x(display_caret);
let preedit_end_px = pixel_x(preedit_end_byte);
let preedit_width = (preedit_end_px - preedit_start_px).max(0.0);
if preedit_width > 0.0 {
let underline_y = bounds.origin.y + shaped.ascent_lpx + 1.0;
cx.scene.push_rect(RectInstance {
rect: [
Lpx(bounds.origin.x + preedit_start_px),
Lpx(underline_y),
Lpx(preedit_width),
Lpx(1.0),
],
color: self.style.color,
corner_radius: Lpx(0.0),
_pad: [0.0; 3],
});
}
if let Some(ref sel) = preedit.selection {
let sel_start_byte = display_caret + sel.start.min(preedit.text.len());
let sel_end_byte = display_caret + sel.end.min(preedit.text.len());
let sel_start_px = pixel_x(sel_start_byte);
let sel_end_px = pixel_x(sel_end_byte);
let sel_w = (sel_end_px - sel_start_px).max(0.0);
if sel_w > 0.0 {
cx.scene.push_rect(RectInstance {
rect: [
Lpx(bounds.origin.x + sel_start_px),
Lpx(bounds.origin.y),
Lpx(sel_w),
Lpx(line_height),
],
color: self.style.preedit_selection_color,
corner_radius: Lpx(0.0),
_pad: [0.0; 3],
});
}
}
}
let mut caret_visible = false;
if let Some(state_rc) = cx.ime_registry.borrow().get(element_id)
&& let Ok(mut state) = state_rc.try_borrow_mut()
{
let (visible, next_deadline) = crate::elements::text_edit::blink::advance_blink(
&mut state.blink,
paint_state.focused,
Instant::now(),
);
caret_visible = visible;
if let Some(deadline) = next_deadline {
cx.schedule_redraw_at(deadline);
}
}
if paint_state.focused && caret_visible {
cx.scene.push_rect(RectInstance {
rect: [
Lpx(bounds.origin.x + caret_pixel_x),
Lpx(bounds.origin.y),
Lpx(1.0),
Lpx(line_height),
],
color: self.style.caret_color,
corner_radius: Lpx(0.0),
_pad: [0.0; 3],
});
}
if let Some(state_rc) = cx.ime_registry.borrow().get(element_id)
&& let Ok(mut state) = state_rc.try_borrow_mut()
{
state.caret_client_rect = Some(slate_platform::PhysicalRect::from_lpx_rect(
bounds.origin.x + caret_pixel_x,
bounds.origin.y,
1.0,
line_height,
scale,
));
state.last_shaped = Some(std::rc::Rc::new(shaped));
state.paint_origin_x = bounds.origin.x;
}
}
fn id(&self) -> Option<ElementId> {
self.last_id
}
}
impl IntoElement for TextField {
type Element = Self;
fn into_element(self) -> Self {
self
}
}