mod buffer;
pub(crate) mod clipboard;
mod editing;
pub(crate) mod highlight;
mod history;
pub(crate) mod keys;
pub(crate) mod layout;
pub(crate) mod markers;
mod motion;
mod navigate;
mod pointer;
mod view;
use std::cell::{Cell, RefCell};
use concinnity_core::components::ColorRun;
pub(crate) use buffer::Pos;
use buffer::{Buffer, ordered};
use highlight::Highlighter;
use highlight::states::LineStates;
use history::{Cursor, History};
pub(crate) use view::{Scroll, ViewSize};
const MULTI_CLICK_S: f64 = 0.4;
#[derive(Debug, Clone, Copy)]
struct ClickRun {
at: f64,
pos: Pos,
count: u8,
}
#[derive(Debug)]
struct Highlight {
language: &'static dyn Highlighter,
states: RefCell<LineStates>,
}
#[derive(Debug, Default)]
pub(crate) struct TextArea {
buffer: Buffer,
caret: Pos,
anchor: Pos,
goal: Option<usize>,
history: History,
saved: u64,
scroll: Scroll,
view: ViewSize,
dragging: bool,
bar: Option<pointer::Bar>,
wheel_carry: f32,
clicks: Option<ClickRun>,
widest: Cell<Option<usize>>,
highlight: Option<Highlight>,
}
impl TextArea {
pub(crate) fn from_text(text: &str) -> Self {
Self {
buffer: Buffer::from_text(text),
..Self::default()
}
}
pub(crate) fn highlighted(mut self, language: &'static dyn Highlighter) -> Self {
self.highlight = Some(Highlight {
language,
states: RefCell::default(),
});
self
}
pub(crate) fn line_runs(
&self,
line: usize,
left: usize,
width: usize,
out: &mut Vec<ColorRun>,
) {
let Some(hl) = &self.highlight else {
return;
};
let mut states = hl.states.borrow_mut();
let start = states.start_of(line, hl.language, |i| self.buffer.line(i));
let mut spans = Vec::new();
let text = self.buffer.line(line);
let after = hl.language.line(text, start, &mut spans);
states.learn(line, after);
highlight::window::window_runs(text, &spans, left, width, out);
}
fn edited(&self, line: usize) {
if let Some(hl) = &self.highlight {
hl.states.borrow_mut().edited(line);
}
}
pub(crate) fn text(&self) -> String {
self.buffer.text()
}
pub(crate) fn mark_saved(&mut self) {
self.history.seal();
self.saved = self.history.state();
}
pub(crate) fn is_dirty(&self) -> bool {
self.history.state() != self.saved
}
pub(crate) fn line_count(&self) -> usize {
self.buffer.line_count()
}
pub(crate) fn line(&self, i: usize) -> &str {
self.buffer.line(i)
}
pub(crate) fn caret(&self) -> Pos {
self.caret
}
pub(crate) fn selection(&self) -> Option<(Pos, Pos)> {
(self.anchor != self.caret).then(|| ordered(self.anchor, self.caret))
}
pub(crate) fn scroll(&self) -> Scroll {
self.scroll
}
pub(crate) fn widest(&self) -> usize {
if let Some(w) = self.widest.get() {
return w;
}
let w = (0..self.buffer.line_count())
.map(|i| view::line_width(self.buffer.line(i)))
.max()
.unwrap_or(0);
self.widest.set(Some(w));
w
}
pub(crate) fn caret_cell(&self) -> usize {
view::visual_col(self.buffer.line(self.caret.line), self.caret.col)
}
pub(crate) fn set_view(&mut self, view: ViewSize) {
self.view = view;
self.clamp_scroll();
}
pub(crate) fn go_to(&mut self, line: usize, col: usize) {
self.history.seal();
self.caret = self.buffer.clamp(Pos::new(line, col));
self.anchor = self.caret;
self.goal = None;
let rows = self.view.rows.max(1);
let top = self.scroll.top;
if self.caret.line < top || self.caret.line >= top + rows {
self.scroll.top = self.caret.line.saturating_sub(rows / 2);
}
self.reveal_caret();
}
pub(crate) fn scroll_by(&mut self, lines: isize, cols: isize) {
self.scroll.top = self.scroll.top.saturating_add_signed(lines);
self.scroll.left = self.scroll.left.saturating_add_signed(cols);
self.clamp_scroll();
}
pub(crate) fn set_scroll(&mut self, scroll: Scroll) {
self.scroll = scroll;
self.clamp_scroll();
}
pub(crate) fn press(&mut self, pos: Pos, extend: bool, now: f64) {
self.history.seal();
let pos = self.buffer.clamp(pos);
let count = match self.clicks {
Some(run) if run.pos == pos && now - run.at <= MULTI_CLICK_S => run.count % 3 + 1,
_ => 1,
};
self.clicks = Some(ClickRun {
at: now,
pos,
count,
});
self.goal = None;
match count {
1 => {
self.caret = pos;
if !extend {
self.anchor = pos;
}
self.dragging = true;
}
2 => {
let (start, end) = motion::word_at(&self.buffer, pos);
self.anchor = start;
self.caret = end;
self.dragging = false;
}
_ => {
self.anchor = Pos::new(pos.line, 0);
self.caret = if pos.line + 1 < self.buffer.line_count() {
Pos::new(pos.line + 1, 0)
} else {
motion::line_end(&self.buffer, pos)
};
self.dragging = false;
}
}
self.reveal_caret();
}
pub(crate) fn drag_to(&mut self, pos: Pos) {
if self.dragging {
self.caret = self.buffer.clamp(pos);
self.reveal_caret();
}
}
pub(crate) fn release(&mut self) {
self.dragging = false;
}
pub(crate) fn dragging(&self) -> bool {
self.dragging
}
fn cursor(&self) -> Cursor {
Cursor {
anchor: self.anchor,
caret: self.caret,
}
}
fn set_cursor(&mut self, c: Cursor) {
let clamp = |p| self.buffer.clamp(p);
self.anchor = clamp(c.anchor);
self.caret = clamp(c.caret);
}
fn reveal_caret(&mut self) {
let cell = self.caret_cell();
self.scroll.reveal(self.caret.line, cell, self.view);
self.clamp_scroll();
}
fn clamp_scroll(&mut self) {
let (lines, widest) = (self.buffer.line_count(), self.widest());
self.scroll
.clamp(lines, widest.max(self.caret_cell()), self.view);
}
}
#[cfg(test)]
mod tests;