mod cursor;
use std::{sync::Arc, time::Instant};
use glyphon::{
Cursor, Edit,
cosmic_text::{self, Selection},
};
use parking_lot::RwLock;
use tessera_ui::{
Clipboard, Color, ComputedData, DimensionValue, Dp, Px, PxPosition, focus_state::Focus,
tessera, winit,
};
use winit::keyboard::NamedKey;
use crate::{
pipelines::{TextCommand, TextConstraint, TextData, write_font_system},
selection_highlight_rect::selection_highlight_rect,
text_edit_core::cursor::CURSOR_WIDRH,
};
#[derive(Clone, Debug)]
pub struct RectDef {
pub x: Px,
pub y: Px,
pub width: Px,
pub height: Px,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ClickType {
Single,
Double,
Triple,
}
pub struct TextEditorStateInner {
line_height: Px,
pub(crate) editor: glyphon::Editor<'static>,
blink_timer: Instant,
focus_handler: Focus,
pub(crate) selection_color: Color,
pub(crate) current_selection_rects: Vec<RectDef>,
last_click_time: Option<Instant>,
last_click_position: Option<PxPosition>,
click_count: u32,
is_dragging: bool,
pub(crate) preedit_string: Option<String>,
}
#[derive(Clone)]
pub struct TextEditorState {
inner: Arc<RwLock<TextEditorStateInner>>,
}
impl TextEditorState {
pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
Self {
inner: Arc::new(RwLock::new(TextEditorStateInner::new(size, line_height))),
}
}
pub fn read(&self) -> parking_lot::RwLockReadGuard<'_, TextEditorStateInner> {
self.inner.read()
}
pub fn write(&self) -> parking_lot::RwLockWriteGuard<'_, TextEditorStateInner> {
self.inner.write()
}
}
impl TextEditorStateInner {
pub fn new(size: Dp, line_height: Option<Dp>) -> Self {
Self::with_selection_color(size, line_height, Color::new(0.5, 0.7, 1.0, 0.4))
}
pub fn with_selection_color(size: Dp, line_height: Option<Dp>, selection_color: Color) -> Self {
let final_line_height = line_height.unwrap_or(Dp(size.0 * 1.2));
let line_height_px: Px = final_line_height.into();
let mut buffer = glyphon::Buffer::new(
&mut write_font_system(),
glyphon::Metrics::new(size.to_pixels_f32(), line_height_px.to_f32()),
);
buffer.set_wrap(&mut write_font_system(), glyphon::Wrap::Glyph);
let editor = glyphon::Editor::new(buffer);
Self {
line_height: line_height_px,
editor,
blink_timer: Instant::now(),
focus_handler: Focus::new(),
selection_color,
current_selection_rects: Vec::new(),
last_click_time: None,
last_click_position: None,
click_count: 0,
is_dragging: false,
preedit_string: None,
}
}
pub fn line_height(&self) -> Px {
self.line_height
}
pub fn text_data(&mut self, constraint: TextConstraint) -> TextData {
self.editor.with_buffer_mut(|buffer| {
buffer.set_size(
&mut write_font_system(),
constraint.max_width,
constraint.max_height,
);
buffer.shape_until_scroll(&mut write_font_system(), false);
});
let text_buffer = match self.editor.buffer_ref() {
glyphon::cosmic_text::BufferRef::Owned(buffer) => buffer.clone(),
glyphon::cosmic_text::BufferRef::Borrowed(buffer) => (**buffer).to_owned(),
glyphon::cosmic_text::BufferRef::Arc(buffer) => (**buffer).clone(),
};
TextData::from_buffer(text_buffer)
}
pub fn focus_handler(&self) -> &Focus {
&self.focus_handler
}
pub fn focus_handler_mut(&mut self) -> &mut Focus {
&mut self.focus_handler
}
pub fn editor(&self) -> &glyphon::Editor<'static> {
&self.editor
}
pub fn editor_mut(&mut self) -> &mut glyphon::Editor<'static> {
&mut self.editor
}
pub fn blink_timer(&self) -> Instant {
self.blink_timer
}
pub fn update_blink_timer(&mut self) {
self.blink_timer = Instant::now();
}
pub fn selection_color(&self) -> Color {
self.selection_color
}
pub fn current_selection_rects(&self) -> &Vec<RectDef> {
&self.current_selection_rects
}
pub fn set_selection_color(&mut self, color: Color) {
self.selection_color = color;
}
pub fn handle_click(&mut self, position: PxPosition, timestamp: Instant) -> ClickType {
const DOUBLE_CLICK_TIME_MS: u128 = 500; const CLICK_DISTANCE_THRESHOLD: Px = Px(5);
let click_type = if let (Some(last_time), Some(last_pos)) =
(self.last_click_time, self.last_click_position)
{
let time_diff = timestamp.duration_since(last_time).as_millis();
let distance = (position.x - last_pos.x).abs() + (position.y - last_pos.y).abs();
if time_diff <= DOUBLE_CLICK_TIME_MS && distance <= CLICK_DISTANCE_THRESHOLD.abs() {
self.click_count += 1;
match self.click_count {
2 => ClickType::Double,
3 => {
self.click_count = 0; ClickType::Triple
}
_ => ClickType::Single,
}
} else {
self.click_count = 1;
ClickType::Single
}
} else {
self.click_count = 1;
ClickType::Single
};
self.last_click_time = Some(timestamp);
self.last_click_position = Some(position);
self.is_dragging = false;
click_type
}
pub fn start_drag(&mut self) {
self.is_dragging = true;
}
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
pub fn stop_drag(&mut self) {
self.is_dragging = false;
}
pub fn last_click_position(&self) -> Option<PxPosition> {
self.last_click_position
}
pub fn update_last_click_position(&mut self, position: PxPosition) {
self.last_click_position = Some(position);
}
pub fn map_key_event_to_action(
&mut self,
key_event: winit::event::KeyEvent,
key_modifiers: winit::keyboard::ModifiersState,
clipboard: &mut Clipboard,
) -> Option<Vec<glyphon::Action>> {
let editor = &mut self.editor;
match key_event.state {
winit::event::ElementState::Pressed => {}
winit::event::ElementState::Released => return None,
}
match key_event.logical_key {
winit::keyboard::Key::Named(named_key) => match named_key {
NamedKey::Backspace => Some(vec![glyphon::Action::Backspace]),
NamedKey::Delete => Some(vec![glyphon::Action::Delete]),
NamedKey::Enter => Some(vec![glyphon::Action::Enter]),
NamedKey::Escape => Some(vec![glyphon::Action::Escape]),
NamedKey::Tab => Some(vec![glyphon::Action::Insert(' '); 4]),
NamedKey::ArrowLeft => {
if key_modifiers.control_key() {
editor.set_selection(Selection::None);
Some(vec![glyphon::Action::Motion(cosmic_text::Motion::LeftWord)])
} else {
if editor.selection_bounds().is_some() {
editor.set_selection(Selection::None);
return None;
}
Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Left)])
}
}
NamedKey::ArrowRight => {
if key_modifiers.control_key() {
editor.set_selection(Selection::None);
Some(vec![glyphon::Action::Motion(
cosmic_text::Motion::RightWord,
)])
} else {
if editor.selection_bounds().is_some() {
editor.set_selection(Selection::None);
return None;
}
Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Right)])
}
}
NamedKey::ArrowUp => {
if editor.cursor().line == 0 {
editor.set_cursor(Cursor::new(0, 0));
return None;
}
Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Up)])
}
NamedKey::ArrowDown => {
let last_line_index =
editor.with_buffer(|buffer| buffer.lines.len().saturating_sub(1));
if editor.cursor().line >= last_line_index {
let last_col =
editor.with_buffer(|buffer| buffer.lines[last_line_index].text().len());
editor.set_cursor(Cursor::new(last_line_index, last_col));
return None;
}
Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Down)])
}
NamedKey::Home => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::Home)]),
NamedKey::End => Some(vec![glyphon::Action::Motion(cosmic_text::Motion::End)]),
NamedKey::Space => Some(vec![glyphon::Action::Insert(' ')]),
_ => None,
},
winit::keyboard::Key::Character(s) => {
let is_ctrl = key_modifiers.control_key() || key_modifiers.super_key();
if is_ctrl {
match s.to_lowercase().as_str() {
"c" => {
if let Some(text) = editor.copy_selection() {
clipboard.set_text(&text);
}
return None;
}
"v" => {
if let Some(text) = clipboard.get_text() {
return Some(text.chars().map(glyphon::Action::Insert).collect());
}
return None;
}
"x" => {
if let Some(text) = editor.copy_selection() {
clipboard.set_text(&text);
return Some(vec![glyphon::Action::Backspace]);
}
return None;
}
_ => {}
}
}
Some(s.chars().map(glyphon::Action::Insert).collect::<Vec<_>>())
}
_ => None,
}
}
}
fn compute_selection_rects(editor: &glyphon::Editor) -> Vec<RectDef> {
let mut selection_rects: Vec<RectDef> = Vec::new();
let (selection_start, selection_end) = editor.selection_bounds().unwrap_or_default();
editor.with_buffer(|buffer| {
for run in buffer.layout_runs() {
let line_top = Px(run.line_top as i32);
let line_height = Px(run.line_height as i32);
if let Some((x, w)) = run.highlight(selection_start, selection_end) {
selection_rects.push(RectDef {
x: Px(x as i32),
y: line_top,
width: Px(w as i32),
height: line_height,
});
}
}
});
selection_rects
}
fn clip_and_take_visible(rects: Vec<RectDef>, visible_x1: Px, visible_y1: Px) -> Vec<RectDef> {
let visible_x0 = Px(0);
let visible_y0 = Px(0);
rects
.into_iter()
.filter_map(|mut rect| {
let rect_x1 = rect.x + rect.width;
let rect_y1 = rect.y + rect.height;
if rect_x1 <= visible_x0
|| rect.y >= visible_y1
|| rect.x >= visible_x1
|| rect_y1 <= visible_y0
{
None
} else {
let new_x = rect.x.max(visible_x0);
let new_y = rect.y.max(visible_y0);
let new_x1 = rect_x1.min(visible_x1);
let new_y1 = rect_y1.min(visible_y1);
rect.x = new_x;
rect.y = new_y;
rect.width = (new_x1 - new_x).max(Px(0));
rect.height = (new_y1 - new_y).max(Px(0));
Some(rect)
}
})
.collect()
}
#[tessera]
pub fn text_edit_core(state: TextEditorState) {
{
let state_clone = state.clone();
measure(Box::new(move |input| {
input.enable_clipping();
let max_width_pixels: Option<Px> = match input.parent_constraint.width {
DimensionValue::Fixed(w) => Some(w),
DimensionValue::Wrap { max, .. } => max,
DimensionValue::Fill { max, .. } => max,
};
let max_height_pixels: Option<Px> = match input.parent_constraint.height {
DimensionValue::Fixed(h) => Some(h), DimensionValue::Wrap { max, .. } => max, DimensionValue::Fill { max, .. } => max,
};
let text_data = state_clone.write().text_data(TextConstraint {
max_width: max_width_pixels.map(|px| px.to_f32()),
max_height: max_height_pixels.map(|px| px.to_f32()),
});
let mut selection_rects = compute_selection_rects(state_clone.read().editor());
let selection_rects_len = selection_rects.len();
for (i, rect_def) in selection_rects.iter().enumerate() {
if let Some(rect_node_id) = input.children_ids.get(i).copied() {
input.measure_child(rect_node_id, input.parent_constraint)?;
input.place_child(rect_node_id, PxPosition::new(rect_def.x, rect_def.y));
}
}
let visible_x1 = max_width_pixels.unwrap_or(Px(i32::MAX));
let visible_y1 = max_height_pixels.unwrap_or(Px(i32::MAX));
selection_rects = clip_and_take_visible(selection_rects, visible_x1, visible_y1);
state_clone.write().current_selection_rects = selection_rects;
if let Some(cursor_pos_raw) = state_clone.read().editor().cursor_position() {
let cursor_pos = PxPosition::new(Px(cursor_pos_raw.0), Px(cursor_pos_raw.1));
let cursor_node_index = selection_rects_len;
if let Some(cursor_node_id) = input.children_ids.get(cursor_node_index).copied() {
input.measure_child(cursor_node_id, input.parent_constraint)?;
input.place_child(cursor_node_id, cursor_pos);
}
}
let drawable = TextCommand {
data: text_data.clone(),
};
input.metadata_mut().push_draw_command(drawable);
let constrained_height = if let Some(max_h) = max_height_pixels {
text_data.size[1].min(max_h.abs())
} else {
text_data.size[1]
};
Ok(ComputedData {
width: Px::from(text_data.size[0]) + CURSOR_WIDRH.to_px(), height: constrained_height.into(),
})
}));
}
{
let (rect_definitions, color_for_selection) = {
let guard = state.read();
(guard.current_selection_rects.clone(), guard.selection_color)
};
for def in rect_definitions {
selection_highlight_rect(def.width, def.height, color_for_selection);
}
}
if state.read().focus_handler().is_focused() {
cursor::cursor(state.read().line_height(), state.read().blink_timer());
}
}