use parley::editing::{Generation, PlainEditor, PlainEditorDriver};
use parley::{FontContext, LayoutContext, StyleProperty};
use peniko::Brush;
use std::time::Duration;
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
#[derive(Debug, Clone, PartialEq)]
pub enum TextKey {
Character(String),
Backspace,
Delete,
Enter,
Left,
Right,
Up,
Down,
Home,
End,
Escape,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TextModifiers {
pub shift: bool,
pub ctrl: bool,
pub alt: bool,
pub meta: bool,
}
impl TextModifiers {
pub fn action_mod(&self) -> bool {
if cfg!(target_os = "macos") {
self.meta
} else {
self.ctrl
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum TextEditResult {
Handled,
ExitEdit,
NotHandled,
}
pub struct TextEditState {
editor: PlainEditor<Brush>,
cursor_visible: bool,
start_time: Option<Instant>,
blink_period: Duration,
is_dragging: bool,
cached_width: f32,
cached_height: f32,
}
impl TextEditState {
pub fn new(text: &str, font_size: f32) -> Self {
use parley::GenericFamily;
let mut editor = PlainEditor::new(font_size);
editor.set_text(text);
editor.set_scale(1.0);
let styles = editor.edit_styles();
styles.insert(GenericFamily::SansSerif.into());
styles.insert(StyleProperty::Brush(Brush::Solid(peniko::Color::BLACK)));
Self {
editor,
cursor_visible: true,
start_time: None,
blink_period: Duration::ZERO,
is_dragging: false,
cached_width: 0.0,
cached_height: 0.0,
}
}
pub fn editor_mut(&mut self) -> &mut PlainEditor<Brush> {
&mut self.editor
}
pub fn editor(&self) -> &PlainEditor<Brush> {
&self.editor
}
pub fn driver<'a>(
&'a mut self,
font_cx: &'a mut FontContext,
layout_cx: &'a mut LayoutContext<Brush>,
) -> PlainEditorDriver<'a, Brush> {
self.editor.driver(font_cx, layout_cx)
}
pub fn text(&self) -> String {
self.editor.text().to_string()
}
pub fn set_text(&mut self, text: &str) {
self.editor.set_text(text);
}
pub fn set_brush(&mut self, brush: Brush) {
let styles = self.editor.edit_styles();
styles.insert(StyleProperty::Brush(brush));
}
pub fn set_font_size(&mut self, size: f32) {
let styles = self.editor.edit_styles();
styles.insert(StyleProperty::FontSize(size));
}
pub fn set_width(&mut self, width: Option<f32>) {
self.editor.set_width(width);
}
pub fn cursor_reset(&mut self) {
self.start_time = Some(Instant::now());
self.blink_period = Duration::from_millis(500);
self.cursor_visible = true;
}
pub fn disable_blink(&mut self) {
self.start_time = None;
}
pub fn next_blink_time(&self) -> Option<Instant> {
self.start_time.map(|start_time| {
let phase = Instant::now().duration_since(start_time);
start_time
+ Duration::from_nanos(
((phase.as_nanos() / self.blink_period.as_nanos() + 1)
* self.blink_period.as_nanos()) as u64,
)
})
}
pub fn cursor_blink(&mut self) {
self.cursor_visible = self.start_time.is_some_and(|start_time| {
let elapsed = Instant::now().duration_since(start_time);
(elapsed.as_millis() / self.blink_period.as_millis()) % 2 == 0
});
}
pub fn is_cursor_visible(&self) -> bool {
self.cursor_visible
}
pub fn generation(&self) -> Generation {
self.editor.generation()
}
pub fn is_composing(&self) -> bool {
self.editor.is_composing()
}
pub fn layout_size(&self) -> (f32, f32) {
(self.cached_width, self.cached_height)
}
pub fn update_layout_cache(&mut self, font_cx: &mut FontContext, layout_cx: &mut LayoutContext<Brush>) {
let layout = self.editor.layout(font_cx, layout_cx);
self.cached_width = layout.width();
self.cached_height = layout.height();
}
pub fn handle_key(
&mut self,
key: TextKey,
modifiers: TextModifiers,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Brush>,
) -> TextEditResult {
if self.editor.is_composing() {
return TextEditResult::NotHandled;
}
self.cursor_reset();
let action_mod = modifiers.action_mod();
let shift = modifiers.shift;
let mut drv = self.editor.driver(font_cx, layout_cx);
match key {
TextKey::Escape => {
return TextEditResult::ExitEdit;
}
TextKey::Backspace => {
if action_mod {
drv.backdelete_word();
} else {
drv.backdelete();
}
}
TextKey::Delete => {
if action_mod {
drv.delete_word();
} else {
drv.delete();
}
}
TextKey::Enter => {
drv.insert_or_replace_selection("\n");
}
TextKey::Left => {
if action_mod {
if shift { drv.select_word_left(); } else { drv.move_word_left(); }
} else if shift {
drv.select_left();
} else {
drv.move_left();
}
}
TextKey::Right => {
if action_mod {
if shift { drv.select_word_right(); } else { drv.move_word_right(); }
} else if shift {
drv.select_right();
} else {
drv.move_right();
}
}
TextKey::Up => {
if shift { drv.select_up(); } else { drv.move_up(); }
}
TextKey::Down => {
if shift { drv.select_down(); } else { drv.move_down(); }
}
TextKey::Home => {
if action_mod {
if shift { drv.select_to_text_start(); } else { drv.move_to_text_start(); }
} else if shift {
drv.select_to_line_start();
} else {
drv.move_to_line_start();
}
}
TextKey::End => {
if action_mod {
if shift { drv.select_to_text_end(); } else { drv.move_to_text_end(); }
} else if shift {
drv.select_to_line_end();
} else {
drv.move_to_line_end();
}
}
TextKey::Character(ref c) => {
if action_mod && (c == "a" || c == "A") {
if shift {
drv.collapse_selection();
} else {
drv.select_all();
}
} else if !action_mod {
drv.insert_or_replace_selection(c);
}
}
}
drop(drv);
self.update_layout_cache(font_cx, layout_cx);
TextEditResult::Handled
}
pub fn handle_mouse_down(
&mut self,
local_x: f32,
local_y: f32,
shift: bool,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Brush>,
) {
self.cursor_reset();
self.is_dragging = true;
let mut drv = self.editor.driver(font_cx, layout_cx);
if shift {
drv.extend_selection_to_point(local_x, local_y);
} else {
drv.move_to_point(local_x, local_y);
}
}
pub fn handle_mouse_drag(
&mut self,
local_x: f32,
local_y: f32,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Brush>,
) {
if !self.is_dragging {
return;
}
self.cursor_reset();
let mut drv = self.editor.driver(font_cx, layout_cx);
drv.extend_selection_to_point(local_x, local_y);
}
pub fn handle_mouse_up(&mut self) {
self.is_dragging = false;
}
pub fn is_dragging(&self) -> bool {
self.is_dragging
}
pub fn handle_double_click(
&mut self,
local_x: f32,
local_y: f32,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Brush>,
) {
self.cursor_reset();
let mut drv = self.editor.driver(font_cx, layout_cx);
drv.select_word_at_point(local_x, local_y);
}
pub fn handle_triple_click(
&mut self,
local_x: f32,
local_y: f32,
font_cx: &mut FontContext,
layout_cx: &mut LayoutContext<Brush>,
) {
self.cursor_reset();
let mut drv = self.editor.driver(font_cx, layout_cx);
drv.select_hard_line_at_point(local_x, local_y);
}
}
impl Default for TextEditState {
fn default() -> Self {
Self::new("", 32.0)
}
}