use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::time::Duration;
use smudge_core::clipboard;
use smudge_core::element::Element;
use smudge_core::geometry::{Color, Corners, Point, Rect};
use smudge_core::input::{Handled, Modifiers};
use smudge_core::paint::GlyphRun;
use smudge_core::paint::PaintOp;
use smudge_core::reactive::{RwSignal, prelude::*};
use smudge_core::timer::{self, TimerHandle};
use smudge_core::tree::Dirty;
use smudge_core::widget::{PaintCx, Widget};
use web_time::Instant;
use crate::input::Editor;
use crate::layout::TextLayout;
use crate::style::TextStyle;
pub(crate) const BLINK_PERIOD: Duration = Duration::from_millis(530);
pub(crate) const DOUBLE_CLICK_TIME: Duration = Duration::from_millis(400);
pub(crate) const DOUBLE_CLICK_DISTANCE: f32 = 4.0;
pub(crate) const CARET_WIDTH: f32 = 1.0;
pub(crate) const SELECTION_ALPHA: u8 = 96;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct CaretPaint {
pub(crate) rect: Rect,
pub(crate) color: Color,
}
pub(crate) struct InputChrome {
pub(crate) editor: Editor,
pub(crate) edits: RwSignal<u64>,
pub(crate) caret: RwSignal<Option<CaretPaint>>,
pub(crate) caret_on: RwSignal<bool>,
pub(crate) caret_shown: RwSignal<bool>,
pub(crate) painted: Cell<Option<(Rect, Color, Point)>>,
pub(crate) scroll_x: Cell<f32>,
pub(crate) scroll_y: Cell<f32>,
pub(crate) scrolls_vertically: bool,
pub(crate) line_height: Cell<f32>,
pub(crate) dragging: Cell<bool>,
pub(crate) last_click: Cell<Option<(Instant, Point)>>,
pub(crate) blink: RefCell<Option<TimerHandle>>,
pub(crate) follow_caret: Cell<bool>,
pub(crate) selection: RefCell<Vec<Rect>>,
}
impl InputChrome {
pub(crate) fn new(value: RwSignal<String>, multi_line: bool) -> InputChrome {
InputChrome {
editor: if multi_line {
Editor::new(value)
} else {
Editor::single_line(value)
},
edits: RwSignal::new(0),
caret: RwSignal::new(None),
caret_on: RwSignal::new(true),
caret_shown: RwSignal::new(false),
painted: Cell::new(None),
scroll_x: Cell::new(0.0),
scroll_y: Cell::new(0.0),
scrolls_vertically: multi_line,
line_height: Cell::new(0.0),
dragging: Cell::new(false),
last_click: Cell::new(None),
blink: RefCell::new(None),
follow_caret: Cell::new(true),
selection: RefCell::new(Vec::new()),
}
}
pub(crate) fn touched(&self) {
self.follow_caret.set(true);
self.caret_on.set(true);
let mut blink = self.blink.borrow_mut();
if blink.is_some() {
let on = self.caret_on.clone();
*blink = Some(blink_timer(on));
}
drop(blink);
self.edits.update(|edits| *edits += 1);
}
pub(crate) fn start_blink(&self) {
let mut blink = self.blink.borrow_mut();
if blink.is_some() {
return;
}
*blink = Some(blink_timer(self.caret_on.clone()));
}
pub(crate) fn stop_blink(&self) {
*self.blink.borrow_mut() = None;
}
pub(crate) fn place(&self, content: Rect, color: Color) -> (f32, f32) {
let caret = self.editor.cursor_geometry(CARET_WIDTH);
let follow = caret.filter(|_| self.follow_caret.get());
let (max_scroll_x, max_scroll_y) = self.max_scroll(content);
let mut scroll_x = self.scroll_x.get();
if let Some(caret) = follow {
if caret.x0 + CARET_WIDTH > scroll_x + content.width() {
scroll_x = caret.x0 + CARET_WIDTH - content.width();
}
if caret.x0 < scroll_x {
scroll_x = caret.x0;
}
}
let scroll_x = scroll_x.clamp(0.0, max_scroll_x);
self.scroll_x.set(scroll_x);
let mut scroll_y = self.scroll_y.get();
if let Some(caret) = follow {
if caret.y1 > scroll_y + content.height() {
scroll_y = caret.y1 - content.height();
}
if caret.y0 < scroll_y {
scroll_y = caret.y0;
}
}
let scroll_y = scroll_y.clamp(0.0, max_scroll_y);
self.scroll_y.set(scroll_y);
let paint = caret.map(|caret| CaretPaint {
rect: Rect::new(
content.x0 + caret.x0 - scroll_x,
content.y0 + caret.y0 - scroll_y,
content.x0 + caret.x0 - scroll_x + CARET_WIDTH,
content.y0 + caret.y1 - scroll_y,
),
color,
});
if self.caret.read(|current| *current) != paint {
self.caret.set(paint);
}
(scroll_x, scroll_y)
}
fn max_scroll(&self, content: Rect) -> (f32, f32) {
let layout = self.editor.layout_size();
let x = (layout.width + CARET_WIDTH - content.width()).max(0.0);
let y = if self.scrolls_vertically {
(layout.height - content.height()).max(0.0)
} else {
0.0
};
(x, y)
}
pub(crate) fn place_before_paint(&self) {
if let Some((content, color, _)) = self.painted.get() {
self.place(content, color);
}
}
}
fn blink_timer(on: RwSignal<bool>) -> TimerHandle {
timer::every(BLINK_PERIOD, move || {
on.update(|shown| *shown = !*shown);
})
}
pub(crate) struct Caret {
paint: Option<CaretPaint>,
shown: bool,
}
impl Widget for Caret {
fn paint(&self, _cx: &PaintCx<'_>, out: &mut Vec<PaintOp>) {
if let (true, Some(paint)) = (self.shown, self.paint) {
out.push(PaintOp::FillRect {
rect: paint.rect,
radius: Corners::ZERO,
color: paint.color,
});
}
}
}
fn apply_caret_paint(caret: &mut Caret, paint: Option<CaretPaint>) -> Dirty {
if caret.paint == paint {
return Dirty::Clean;
}
caret.paint = paint;
Dirty::Paint
}
fn apply_caret_shown(caret: &mut Caret, shown: bool) -> Dirty {
if caret.shown == shown {
return Dirty::Clean;
}
caret.shown = shown;
Dirty::Paint
}
struct CaretPaintKey;
struct CaretShownKey;
pub(crate) fn caret_node(chrome: &Rc<InputChrome>) -> Element<Caret> {
let paint_chrome = Rc::clone(chrome);
let shown_chrome = Rc::clone(chrome);
Element::new(Caret {
paint: None,
shown: false,
})
.absolute()
.bind(
CaretPaintKey,
move || paint_chrome.caret.get(),
apply_caret_paint,
)
.bind(
CaretShownKey,
move || shown_chrome.caret_on.get() && shown_chrome.caret_shown.get(),
apply_caret_shown,
)
}
pub(crate) fn handle_press(chrome: &InputChrome, position: Point) {
let now = timer::now();
let double_click = chrome
.last_click
.get()
.is_some_and(|(last_at, last_at_point)| {
now.duration_since(last_at) <= DOUBLE_CLICK_TIME
&& distance_squared(position, last_at_point)
<= DOUBLE_CLICK_DISTANCE * DOUBLE_CLICK_DISTANCE
});
chrome.last_click.set(Some((now, position)));
chrome.dragging.set(true);
let (x, y) = layout_position(chrome, position);
if double_click {
chrome.editor.select_word_at_point(x, y);
} else {
chrome.editor.move_to_point(x, y);
}
chrome.touched();
}
pub(crate) fn handle_move(chrome: &InputChrome, position: Point) {
if !chrome.dragging.get() {
return;
}
if chrome.last_click.get().is_some_and(|(_, pressed_at)| {
distance_squared(position, pressed_at) > DOUBLE_CLICK_DISTANCE * DOUBLE_CLICK_DISTANCE
}) {
chrome.last_click.set(None);
}
let (x, y) = layout_position(chrome, position);
chrome.editor.extend_to_point(x, y);
chrome.touched();
}
pub(crate) fn handle_release(chrome: &InputChrome, _position: Point) {
chrome.dragging.set(false);
}
pub(crate) fn layout_position(chrome: &InputChrome, position: Point) -> (f32, f32) {
let origin = chrome
.painted
.get()
.map(|(_, _, inset)| inset)
.unwrap_or_default();
(
position.x - origin.x + chrome.scroll_x.get(),
position.y - origin.y + chrome.scroll_y.get(),
)
}
pub(crate) fn handle_wheel(chrome: &InputChrome, delta: Point) -> Handled {
let Some((content, _, _)) = chrome.painted.get() else {
return Handled::No;
};
let (max_x, max_y) = chrome.max_scroll(content);
let (old_x, old_y) = (chrome.scroll_x.get(), chrome.scroll_y.get());
let step = |delta: f32| if delta.is_finite() { delta } else { 0.0 };
let x = (old_x + step(delta.x)).clamp(0.0, max_x);
let y = (old_y + step(delta.y)).clamp(0.0, max_y);
if x == old_x && y == old_y {
return Handled::No;
}
chrome.follow_caret.set(false);
chrome.scroll_x.set(x);
chrome.scroll_y.set(y);
chrome.edits.update(|edits| *edits += 1);
Handled::Yes
}
pub(crate) fn handle_focus(chrome: &InputChrome, focused: bool) {
if focused {
chrome.follow_caret.set(true);
} else {
chrome.editor.compose("", None);
}
chrome.caret_shown.set(focused);
if focused {
chrome.caret_on.set(true);
chrome.start_blink();
} else {
chrome.stop_blink();
}
chrome.edits.update(|edits| *edits += 1);
}
pub(crate) fn handle_commit(chrome: &InputChrome, text: &str) {
if text.is_empty() {
return;
}
chrome.editor.commit(text);
chrome.touched();
}
pub(crate) fn handle_preedit(chrome: &InputChrome, text: &str, cursor: Option<(usize, usize)>) {
chrome.editor.compose(text, cursor);
chrome.touched();
}
pub(crate) fn is_shortcut(modifiers: Modifiers) -> bool {
modifiers.command() && !types_text(modifiers)
}
pub(crate) fn types_text(modifiers: Modifiers) -> bool {
#[cfg(target_os = "macos")]
{
!modifiers.meta && !modifiers.control
}
#[cfg(not(target_os = "macos"))]
{
let alt_gr = modifiers.control && modifiers.alt;
!modifiers.meta && (alt_gr || (!modifiers.control && !modifiers.alt))
}
}
#[derive(Copy, Clone)]
pub(crate) enum Direction {
Left,
Right,
}
pub(crate) fn apply_move(chrome: &InputChrome, extend: bool, by_word: bool, direction: Direction) {
let editor = &chrome.editor;
match (extend, by_word, direction) {
(false, false, Direction::Left) => editor.move_left(),
(false, false, Direction::Right) => editor.move_right(),
(false, true, Direction::Left) => editor.move_word_left(),
(false, true, Direction::Right) => editor.move_word_right(),
(true, false, Direction::Left) => editor.extend_left(),
(true, false, Direction::Right) => editor.extend_right(),
(true, true, Direction::Left) => editor.extend_word_left(),
(true, true, Direction::Right) => editor.extend_word_right(),
}
chrome.touched();
}
pub(crate) fn copy_selection(chrome: &InputChrome) {
if let Some(text) = chrome.editor.selected_text() {
clipboard::with(|slot| slot.set(text));
}
}
pub(crate) fn cut_selection(chrome: &InputChrome) -> bool {
let Some(text) = chrome.editor.selected_text() else {
return false;
};
let mut copied = false;
clipboard::with(|slot| {
slot.set(text);
copied = true;
});
if copied {
chrome.editor.delete_selection();
}
copied
}
pub(crate) fn apply_outside_write(chrome: &InputChrome, value: &str) -> Dirty {
if chrome.editor.has_text(value) {
return Dirty::Clean;
}
chrome.editor.set_text(value);
chrome.follow_caret.set(true);
chrome.place_before_paint();
Dirty::Paint
}
pub(crate) fn outside_write(chrome: &InputChrome, value: &RwSignal<String>) -> Option<String> {
value.with(|text| (!chrome.editor.has_text(text)).then(|| text.clone()))
}
pub(crate) fn apply_edit_count(chrome: &InputChrome) -> Dirty {
chrome.place_before_paint();
Dirty::Paint
}
pub(crate) fn distance_squared(a: Point, b: Point) -> f32 {
let dx = a.x - b.x;
let dy = a.y - b.y;
dx * dx + dy * dy
}
pub(crate) fn paint_selection(
chrome: &InputChrome,
origin: Point,
visible: (f32, f32),
color: Color,
out: &mut Vec<PaintOp>,
) {
let mut rects = chrome.selection.borrow_mut();
chrome.editor.selection_rects(visible, &mut rects);
out.extend(rects.iter().map(|area| PaintOp::FillRect {
rect: Rect::new(
origin.x + area.x0,
origin.y + area.y0,
origin.x + area.x1,
origin.y + area.y1,
),
radius: Corners::ZERO,
color,
}));
}
pub(crate) struct ParagraphCache {
paragraphs: Vec<PaintedParagraph>,
scratch: Vec<PaintedParagraph>,
origin: Point,
color: Color,
}
struct PaintedParagraph {
index: usize,
y: f32,
stamp: u64,
runs: Rc<Vec<Rc<GlyphRun>>>,
}
pub(crate) fn paint_paragraphs(
cache: &mut Option<ParagraphCache>,
editor: &Editor,
origin: Point,
color: Color,
visible: (f32, f32),
out: &mut Vec<PaintOp>,
) {
let stale = match cache {
Some(cached) => cached.origin != origin || cached.color != color,
None => true,
};
if stale {
*cache = Some(ParagraphCache {
paragraphs: Vec::new(),
scratch: Vec::new(),
origin,
color,
});
}
let cache = cache.as_mut().expect("the cache is present");
std::mem::swap(&mut cache.paragraphs, &mut cache.scratch);
cache.paragraphs.clear();
let previous = &cache.scratch;
let painted = &mut cache.paragraphs;
let mut index = 0usize;
editor.paragraphs(|paragraph| {
let paragraph_index = index;
index += 1;
let offset = paragraph.offset();
let top = offset.y;
let bottom = top + paragraph.size().height;
if bottom < visible.0 || top > visible.1 {
return;
}
let stamp = paragraph.stamp();
let cached = previous
.iter()
.find(|entry| entry.index == paragraph_index && entry.y == top && entry.stamp == stamp)
.map(|entry| entry.runs.clone());
let runs = cached.unwrap_or_else(|| {
let absolute = Point {
x: origin.x + offset.x,
y: origin.y + offset.y,
};
Rc::new(
paragraph
.glyph_runs_at(absolute, color)
.into_iter()
.map(Rc::new)
.collect::<Vec<_>>(),
)
});
painted.push(PaintedParagraph {
index: paragraph_index,
y: top,
stamp,
runs,
});
});
for paragraph in &cache.paragraphs {
out.extend(paragraph.runs.iter().cloned().map(PaintOp::Glyphs));
}
}
pub(crate) struct Placeholder {
text: String,
layout: Option<TextLayout>,
runs: Option<CachedRuns>,
}
struct CachedRuns {
origin: Point,
color: Color,
runs: Vec<Rc<GlyphRun>>,
}
impl Placeholder {
pub(crate) fn new() -> Placeholder {
Placeholder {
text: String::new(),
layout: None,
runs: None,
}
}
pub(crate) fn text(&self) -> &str {
&self.text
}
pub(crate) fn set_text(&mut self, text: String) {
if self.text == text {
return;
}
self.text = text;
self.layout = None;
self.runs = None;
}
pub(crate) fn invalidate_style(&mut self) {
self.layout = None;
self.runs = None;
}
pub(crate) fn paint(
&mut self,
style: &TextStyle,
origin: Point,
color: Color,
out: &mut Vec<PaintOp>,
) {
if self.layout.is_none() {
self.layout = Some(TextLayout::new(&self.text, style, None));
}
let stale = match &self.runs {
Some(cached) => cached.origin != origin || cached.color != color,
None => true,
};
if stale {
let runs = self
.layout
.as_ref()
.expect("the layout was just built or is cached")
.glyph_runs_at(origin, color)
.into_iter()
.map(Rc::new)
.collect();
self.runs = Some(CachedRuns {
origin,
color,
runs,
});
}
if let Some(cached) = &self.runs {
out.extend(cached.runs.iter().cloned().map(PaintOp::Glyphs));
}
}
}