use std::cell::RefCell;
use std::rc::Rc;
use std::time::{SystemTime, UNIX_EPOCH};
use sequoia_buffer::{SpanAttr, StyledSpan};
use sequoia_core::{Color, Edges, Length, Rect, Size, Vec2};
use sequoia_input::{
CursorIcon, Event, EventPhase, ImeEvent, Key, KeyEvent, Modifiers, PointerButton, PointerEvent,
};
use sequoia_layout::{LayoutCtx, LayoutStyle};
use sequoia_reactive::Signal;
use sequoia_style::{TextAreaStyle, Theme, VisualState};
use sequoia_view::{AnyView, EventCtx, FontWeight, PaintCtx, Unit, View, WrapMode};
use crate::buffer::{BufferEngine, StyledDoc};
use crate::clipboard::SharedClipboard;
#[derive(Clone)]
pub(crate) enum DocSource {
Plain(Signal<String>),
Styled(Signal<StyledDoc>),
}
impl DocSource {
fn ensure(&self, engine: &mut BufferEngine) -> bool {
match self {
DocSource::Plain(s) => engine.ensure(s),
DocSource::Styled(s) => engine.ensure_styled(s),
}
}
fn commit(&self, engine: &mut BufferEngine) {
match self {
DocSource::Plain(s) => engine.commit(s),
DocSource::Styled(s) => engine.commit_styled(s),
}
}
fn get_text(&self) -> String {
match self {
DocSource::Plain(s) => s.get_untracked(),
DocSource::Styled(s) => s.get_untracked().text,
}
}
fn set_text(&self, text: String) {
match self {
DocSource::Plain(s) => s.set(text),
DocSource::Styled(s) => s.set(StyledDoc {
text,
spans: Vec::new(),
}),
}
}
fn track(&self) {
match self {
DocSource::Plain(s) => {
let _ = s.get();
}
DocSource::Styled(s) => {
let _ = s.get();
}
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct Caret {
offset: usize,
anchor: Option<usize>,
}
impl Caret {
fn collapsed(pos: usize) -> Self {
Self {
offset: pos,
anchor: None,
}
}
fn ends(&self) -> (usize, usize) {
match self.anchor {
Some(a) => (a.min(self.offset), a.max(self.offset)),
None => (self.offset, self.offset),
}
}
fn selected(&self) -> Option<(usize, usize)> {
let (a, b) = self.ends();
if a < b { Some((a, b)) } else { None }
}
fn move_to(&mut self, pos: usize, extend: bool) {
if extend {
let anchor = self.anchor.unwrap_or(self.offset);
self.anchor = Some(anchor);
self.offset = pos;
if self.anchor == Some(self.offset) {
self.anchor = None;
}
} else {
self.offset = pos;
self.anchor = None;
}
}
}
fn now_ms() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis())
.unwrap_or(0)
}
pub struct TextArea {
source: DocSource,
placeholder: String,
max_length: Option<usize>,
max_lines: Option<usize>,
min_lines: usize,
font_size: Option<f32>,
weight: Option<FontWeight>,
text_color: Option<Color>,
placeholder_color: Option<Color>,
background: Option<Color>,
border_color: Option<Color>,
border_width: Option<f32>,
radius: Option<f32>,
line_height_factor: Option<f32>,
current_line: Option<Color>,
highlight_current_line: bool,
pad_x: f32,
pad_y: f32,
indent: String,
auto_indent: bool,
editable: bool,
soft_wrap: bool,
wrap_mode: WrapMode,
disabled: bool,
error: bool,
accept_tab: bool,
appearance: Option<TextAreaStyle>,
style: LayoutStyle,
margin: Edges,
revision: Signal<u32>,
state: Rc<RefCell<AreaState>>,
on_edit_start: Option<Rc<dyn Fn()>>,
on_edit_end: Option<Rc<dyn Fn(String)>>,
on_scroll: Option<Rc<dyn Fn(Vec2)>>,
}
const DEFAULT_PAD_X: f32 = 6.0;
const DEFAULT_PAD_Y: f32 = 6.0;
const DEFAULT_LINE_HEIGHT: f32 = 1.35;
const DEFAULT_INDENT: &str = " ";
const DEFAULT_MIN_LINES: usize = 3;
const CARET_W: f32 = 1.5;
const SCROLL_MARGIN: f32 = 2.0;
#[derive(Clone)]
struct AreaState {
carets: Vec<Caret>,
goal_x: Option<f32>,
scroll: Vec2,
viewport: Size,
content: Size,
layout: Option<Rc<VisualLayout>>,
preedit: String,
preedit_cursor: Option<(usize, usize)>,
caret_rect: Option<Rect>,
was_focused: bool,
session_text: Option<String>,
escalate: u8,
escalate_base: usize,
last_click_ms: Option<u128>,
last_click_pos: usize,
click_count: u32,
alt_drag: Option<usize>,
engine: BufferEngine,
}
impl Default for AreaState {
fn default() -> Self {
Self {
carets: vec![Caret::default()],
goal_x: None,
scroll: Vec2::default(),
viewport: Size::default(),
content: Size::default(),
layout: None,
preedit: String::new(),
preedit_cursor: None,
caret_rect: None,
was_focused: false,
session_text: None,
escalate: 0,
escalate_base: 0,
last_click_ms: None,
last_click_pos: 0,
click_count: 0,
alt_drag: None,
engine: BufferEngine::new(),
}
}
}
fn merge_byte_ranges(ranges: &[(usize, usize)]) -> (Vec<(usize, usize)>, Vec<usize>) {
if ranges.is_empty() {
return (Vec::new(), Vec::new());
}
let mut order: Vec<usize> = (0..ranges.len()).collect();
order.sort_by_key(|&i| (ranges[i].0, ranges[i].1));
let mut merged: Vec<(usize, usize)> = Vec::new();
let mut owner = vec![0usize; ranges.len()];
for i in order {
let (s, e) = ranges[i];
if let Some(last) = merged.last_mut()
&& s <= last.1
{
last.1 = last.1.max(e);
owner[i] = merged.len() - 1;
continue;
}
owner[i] = merged.len();
merged.push((s, e));
}
(merged, owner)
}
fn map_through_deletes(unions: &[(usize, usize)], x: usize) -> usize {
let mut sub = 0usize;
for (s, e) in unions {
if *e <= x {
sub += e - s;
} else {
break;
}
}
x.saturating_sub(sub)
}
fn dedupe_carets(carets: &mut Vec<Caret>) {
let mut seen = std::collections::HashSet::new();
carets.retain(|c| seen.insert(c.offset));
if carets.is_empty() {
carets.push(Caret::default());
}
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub(crate) enum StyleFlag {
Bold,
Italic,
Underline,
Strike,
}
impl StyleFlag {
fn get(self, a: &SpanAttr) -> bool {
match self {
StyleFlag::Bold => a.bold,
StyleFlag::Italic => a.italic,
StyleFlag::Underline => a.underline,
StyleFlag::Strike => a.strike,
}
}
fn set(self, a: &mut SpanAttr, v: bool) {
match self {
StyleFlag::Bold => a.bold = v,
StyleFlag::Italic => a.italic = v,
StyleFlag::Underline => a.underline = v,
StyleFlag::Strike => a.strike = v,
}
}
}
fn line_starts(chars: &[char]) -> Vec<usize> {
let mut starts = vec![0usize];
for (i, &c) in chars.iter().enumerate() {
if c == '\n' {
starts.push(i + 1);
}
}
starts
}
#[cfg(test)]
fn line_len(chars: &[char], starts: &[usize], line: usize) -> usize {
let start = starts.get(line).copied().unwrap_or(chars.len());
match starts.get(line + 1) {
Some(&next) => next.saturating_sub(start + 1),
None => chars.len().saturating_sub(start),
}
}
fn line_col_of(chars: &[char], starts: &[usize], caret: usize) -> (usize, usize) {
let caret = caret.min(chars.len());
let line = starts.partition_point(|&s| s <= caret).saturating_sub(1);
let line = line.min(starts.len().saturating_sub(1));
(line, caret - starts[line])
}
#[cfg(test)]
fn caret_of_line_col(chars: &[char], starts: &[usize], line: usize, col: usize) -> usize {
let line = line.min(starts.len().saturating_sub(1));
starts[line] + col.min(line_len(chars, starts, line))
}
fn split_lines(text: &str) -> Vec<&str> {
text.split('\n').collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct RowSpan {
line: usize,
start: usize,
end: usize,
}
#[derive(Clone, Debug)]
struct VisualLayout {
text: String,
width: f32,
size: f32,
weight: FontWeight,
wrap: bool,
mode: WrapMode,
lines: Vec<String>,
line_starts: Vec<usize>,
rows: Vec<RowSpan>,
}
trait LineMeasure {
fn measure_positions(&mut self, text: &str, size: f32, weight: FontWeight) -> Vec<f32>;
}
impl LineMeasure for EventCtx<'_> {
fn measure_positions(&mut self, text: &str, size: f32, weight: FontWeight) -> Vec<f32> {
self.caret_positions(text, size, weight)
}
}
impl LineMeasure for PaintCtx<'_> {
fn measure_positions(&mut self, text: &str, size: f32, weight: FontWeight) -> Vec<f32> {
self.caret_positions(text, size, weight)
}
}
fn build_layout<M: LineMeasure>(
m: &mut M,
text: String,
width: f32,
size: f32,
weight: FontWeight,
wrap: bool,
mode: WrapMode,
) -> VisualLayout {
let lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
let mut line_starts = Vec::with_capacity(lines.len());
let mut acc = 0usize;
for line in &lines {
line_starts.push(acc);
acc += line.chars().count() + 1;
}
let mut rows: Vec<RowSpan> = Vec::with_capacity(lines.len());
for (li, line) in lines.iter().enumerate() {
let line_start = line_starts[li];
let lchars: Vec<char> = line.chars().collect();
let count = lchars.len();
if !wrap || width <= 0.0 || count == 0 {
rows.push(RowSpan {
line: li,
start: line_start,
end: line_start + count,
});
continue;
}
let pos = m.measure_positions(line, size, weight);
let full = pos.get(count).copied().unwrap_or(0.0);
let x_at = |i: usize| pos.get(i).copied().unwrap_or(full);
if full <= width {
rows.push(RowSpan {
line: li,
start: line_start,
end: line_start + count,
});
continue;
}
let mut row_start = 0usize;
let mut last_break: Option<usize> = None;
let mut i = 0usize;
while i < count {
let row_w = x_at(i + 1) - x_at(row_start);
if row_w > width && i > row_start {
let break_at = match last_break {
Some(b) if matches!(mode, WrapMode::Word) && b > row_start => b,
_ => i,
};
rows.push(RowSpan {
line: li,
start: line_start + row_start,
end: line_start + break_at,
});
row_start = if break_at == i {
i
} else {
(break_at + 1).min(count)
};
last_break = None;
i = row_start;
continue;
}
if lchars[i] == ' ' {
last_break = Some(i);
}
i += 1;
}
if row_start < count {
rows.push(RowSpan {
line: li,
start: line_start + row_start,
end: line_start + count,
});
}
}
if rows.is_empty() {
rows.push(RowSpan {
line: 0,
start: 0,
end: 0,
});
}
VisualLayout {
text,
width,
size,
weight,
wrap,
mode,
lines,
line_starts,
rows,
}
}
impl VisualLayout {
fn row_text(&self, row: &RowSpan) -> String {
let base = self.line_starts[row.line];
self.lines[row.line]
.chars()
.skip(row.start - base)
.take(row.end - row.start)
.collect()
}
fn row_of(&self, caret: usize) -> usize {
let mut last = 0usize;
for (i, row) in self.rows.iter().enumerate() {
last = i;
if caret <= row.end {
return i;
}
}
last
}
}
fn row_prefix_x<M: LineMeasure>(
m: &mut M,
layout: &VisualLayout,
row: &RowSpan,
col: usize,
size: f32,
weight: FontWeight,
) -> f32 {
let base = row.start - layout.line_starts[row.line];
let pos = m.measure_positions(&layout.lines[row.line], size, weight);
let x0 = pos.get(base).copied().unwrap_or(0.0);
let x1 = pos
.get(base + col.min(row.end - row.start))
.copied()
.unwrap_or(x0);
x1 - x0
}
fn leading_indent_len(line: &str) -> usize {
line.chars().take_while(|&c| c == ' ' || c == '\t').count()
}
fn sanitize_multiline(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\r' => {
out.push('\n');
if chars.peek() == Some(&'\n') {
chars.next();
}
}
'\n' | '\t' => out.push(c),
c if c.is_control() => {}
c => out.push(c),
}
}
out
}
impl TextArea {
pub fn new(value: Signal<String>) -> Self {
Self::with_source(DocSource::Plain(value))
}
pub fn new_styled(value: Signal<StyledDoc>) -> Self {
Self::with_source(DocSource::Styled(value))
}
fn with_source(source: DocSource) -> Self {
Self {
source,
placeholder: String::new(),
max_length: None,
max_lines: None,
min_lines: DEFAULT_MIN_LINES,
font_size: None,
weight: None,
text_color: None,
placeholder_color: None,
background: None,
border_color: None,
border_width: None,
radius: None,
line_height_factor: None,
current_line: None,
highlight_current_line: false,
pad_x: DEFAULT_PAD_X,
pad_y: DEFAULT_PAD_Y,
indent: DEFAULT_INDENT.to_owned(),
auto_indent: true,
editable: true,
soft_wrap: false,
wrap_mode: WrapMode::Word,
disabled: false,
error: false,
accept_tab: true,
appearance: None,
style: LayoutStyle {
width: Length::Fill(1.0),
height: Length::Fill(1.0),
..LayoutStyle::default()
},
margin: Edges::ZERO,
revision: Signal::new(0),
state: Rc::new(RefCell::new(AreaState::default())),
on_edit_start: None,
on_edit_end: None,
on_scroll: None,
}
}
pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
self.placeholder = placeholder.into();
self
}
pub fn max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn max_lines(mut self, max: usize) -> Self {
self.max_lines = Some(max.max(1));
self
}
pub fn min_lines(mut self, min: usize) -> Self {
self.min_lines = min.max(1);
self
}
pub fn on_edit_start(mut self, f: impl Fn() + 'static) -> Self {
self.on_edit_start = Some(Rc::new(f));
self
}
pub fn on_edit_end(mut self, f: impl Fn(String) + 'static) -> Self {
self.on_edit_end = Some(Rc::new(f));
self
}
pub fn on_scroll(mut self, f: impl Fn(Vec2) + 'static) -> Self {
self.on_scroll = Some(Rc::new(f));
self
}
pub fn font_size(mut self, size: f32) -> Self {
self.font_size = Some(size);
self
}
pub fn weight(mut self, weight: FontWeight) -> Self {
self.weight = Some(weight);
self
}
pub fn bold(mut self, bold: bool) -> Self {
self.weight = Some(if bold {
FontWeight::BOLD
} else {
FontWeight::NORMAL
});
self
}
pub fn color(mut self, color: Color) -> Self {
self.text_color = Some(color);
self
}
pub fn placeholder_color(mut self, color: Color) -> Self {
self.placeholder_color = Some(color);
self
}
pub fn background(mut self, color: Color) -> Self {
self.background = Some(color);
self
}
pub fn border_color(mut self, color: Color) -> Self {
self.border_color = Some(color);
self
}
pub fn border_width(mut self, width: f32) -> Self {
self.border_width = Some(width.max(0.0));
self
}
pub fn border(mut self, width: f32, color: Color) -> Self {
self.border_width = Some(width.max(0.0));
self.border_color = Some(color);
self
}
pub fn radius(mut self, radius: f32) -> Self {
self.radius = Some(radius.max(0.0));
self
}
pub fn line_height_factor(mut self, factor: f32) -> Self {
self.line_height_factor = Some(factor);
self
}
pub fn current_line(mut self, color: Color) -> Self {
self.current_line = Some(color);
self.highlight_current_line = true;
self
}
pub fn highlight_current_line(mut self, on: bool) -> Self {
self.highlight_current_line = on;
self
}
pub fn padding(mut self, padding: impl Into<Edges>) -> Self {
let p = padding.into();
self.pad_x = (p.left + p.right) * 0.5;
self.pad_y = (p.top + p.bottom) * 0.5;
self
}
pub fn padding_xy(mut self, x: f32, y: f32) -> Self {
self.pad_x = x.max(0.0);
self.pad_y = y.max(0.0);
self
}
pub fn indent(mut self, indent: impl Into<String>) -> Self {
self.indent = indent.into();
self
}
pub fn auto_indent(mut self, on: bool) -> Self {
self.auto_indent = on;
self
}
pub fn editable(mut self, editable: bool) -> Self {
self.editable = editable;
self
}
pub fn readonly(mut self, readonly: bool) -> Self {
self.editable = !readonly;
self
}
pub fn soft_wrap(mut self, on: bool) -> Self {
self.soft_wrap = on;
self
}
pub fn wrap_mode(mut self, mode: WrapMode) -> Self {
self.wrap_mode = mode;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn error(mut self, error: bool) -> Self {
self.error = error;
self
}
pub fn auto_size(mut self, auto: bool) -> Self {
self.style.height = if auto {
Length::Fit
} else {
Length::Fill(1.0)
};
self
}
pub fn accept_tab(mut self, on: bool) -> Self {
self.accept_tab = on;
self
}
pub fn appearance(mut self, style: TextAreaStyle) -> Self {
self.appearance = Some(style);
self
}
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.style = self.style.width(width.into());
self
}
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.style = self.style.height(height.into());
self
}
pub fn min_width(mut self, v: f32) -> Self {
self.style = self.style.min_width(v);
self
}
pub fn max_width(mut self, v: f32) -> Self {
self.style = self.style.max_width(v);
self
}
pub fn min_height(mut self, v: f32) -> Self {
self.style = self.style.min_height(v);
self
}
pub fn max_height(mut self, v: f32) -> Self {
self.style = self.style.max_height(v);
self
}
pub fn flex(mut self, weight: f32) -> Self {
self.style = self.style.flex(weight);
self
}
pub fn expand(self) -> Self {
self.flex(1.0)
}
pub fn margin(mut self, margin: impl Into<Edges>) -> Self {
let m = margin.into();
self.margin = Edges::only(
sanitize_margin(m.left),
sanitize_margin(m.top),
sanitize_margin(m.right),
sanitize_margin(m.bottom),
);
self
}
pub fn margin_xy(self, x: f32, y: f32) -> Self {
self.margin(Edges::xy(x, y))
}
pub fn margin_x(mut self, x: f32) -> Self {
let x = sanitize_margin(x);
self.margin.left = x;
self.margin.right = x;
self
}
pub fn margin_y(mut self, y: f32) -> Self {
let y = sanitize_margin(y);
self.margin.top = y;
self.margin.bottom = y;
self
}
pub fn margin_left(mut self, v: f32) -> Self {
self.margin.left = sanitize_margin(v);
self
}
pub fn margin_right(mut self, v: f32) -> Self {
self.margin.right = sanitize_margin(v);
self
}
pub fn margin_top(mut self, v: f32) -> Self {
self.margin.top = sanitize_margin(v);
self
}
pub fn margin_bottom(mut self, v: f32) -> Self {
self.margin.bottom = sanitize_margin(v);
self
}
fn with_state<R>(&self, f: impl FnOnce(&mut AreaState) -> R) -> R {
f(&mut self.state.borrow_mut())
}
fn main_mut(s: &mut AreaState) -> &mut Caret {
if s.carets.is_empty() {
s.carets.push(Caret::default());
}
&mut s.carets[0]
}
fn main_of(s: &AreaState) -> Caret {
s.carets.first().copied().unwrap_or_default()
}
fn reset_escalate(s: &mut AreaState) {
s.escalate = 0;
s.escalate_base = 0;
}
fn chars_via(source: &DocSource) -> Vec<char> {
source.get_text().chars().collect()
}
fn chars(&self) -> Vec<char> {
Self::chars_via(&self.source)
}
fn layout_for<M: LineMeasure>(
&self,
m: &mut M,
width: f32,
size: f32,
weight: FontWeight,
) -> Rc<VisualLayout> {
let text = self.source.get_text();
if let Some(layout) = self.with_state(|s| s.layout.clone())
&& layout.text == text
&& (layout.width - width).abs() < 0.5
&& (layout.size - size).abs() < 0.01
&& layout.weight == weight
&& layout.wrap == self.soft_wrap
&& layout.mode == self.wrap_mode
{
return layout;
}
let layout = Rc::new(build_layout(
m,
text,
width.max(0.0),
size,
weight,
self.soft_wrap,
self.wrap_mode,
));
self.with_state(|s| s.layout = Some(layout.clone()));
layout
}
fn event_layout(&self, ctx: &mut EventCtx) -> Rc<VisualLayout> {
let theme = ctx.environment.get::<Theme>().unwrap_or_else(Theme::dark);
let size = self.resolved_font_size(&theme);
let weight = self.font_weight();
let width = (ctx.rect.size.width - self.pad_x * 2.0).max(0.0);
self.layout_for(ctx, width, size, weight)
}
fn starts(&self, chars: &[char]) -> Vec<usize> {
line_starts(chars)
}
pub fn caret(&self) -> usize {
self.with_state(|s| Self::main_of(s).offset)
}
pub fn caret_count(&self) -> usize {
self.with_state(|s| s.carets.len().max(1))
}
pub fn set_caret(&self, caret: usize) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let caret = caret.min(s.engine.len_chars());
s.carets = vec![Caret::collapsed(caret)];
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
pub fn selection(&self) -> Option<(usize, usize)> {
self.with_state(|s| {
let m = Self::main_of(s);
m.anchor.map(|a| (a.min(m.offset), a.max(m.offset)))
})
}
pub fn selections(&self) -> Vec<(usize, usize)> {
self.with_state(|s| s.carets.iter().filter_map(|c| c.selected()).collect())
}
pub fn caret_line_col(&self) -> (usize, usize) {
let chars = self.chars();
let starts = self.starts(&chars);
let caret = self.caret();
line_col_of(&chars, &starts, caret)
}
pub fn line_count(&self) -> usize {
let chars = self.chars();
self.starts(&chars).len()
}
pub fn scroll_offset(&self) -> Vec2 {
self.with_state(|s| s.scroll)
}
pub fn scroll_to(&self, offset: Vec2) {
let (viewport, content) = self.with_state(|s| (s.viewport, s.content));
let next = clamp_scroll(offset, content.width, content.height, viewport, self.pad());
let changed = self.with_state(|s| {
if s.scroll != next {
s.scroll = next;
true
} else {
false
}
});
if changed {
if let Some(f) = &self.on_scroll {
f(next);
}
self.bump();
}
}
fn pad(&self) -> (f32, f32) {
(self.pad_x, self.pad_y)
}
fn begin_session(&self) {
let cur = self.source.get_text();
self.with_state(|s| {
s.session_text = Some(cur);
s.was_focused = true;
});
if let Some(f) = &self.on_edit_start {
f();
}
}
fn note_focus(&self, focused: bool) {
enum Trans {
Start,
End(String),
None,
}
let cur = self.source.get_text();
let trans = self.with_state(|s| {
let was = s.was_focused;
s.was_focused = focused;
if !was && focused {
s.session_text = Some(cur.clone());
Trans::Start
} else if was && !focused {
match s.session_text.take() {
Some(before) if before != cur => Trans::End(cur.clone()),
_ => Trans::None,
}
} else {
Trans::None
}
});
match trans {
Trans::Start => {
if let Some(f) = &self.on_edit_start {
f();
}
}
Trans::End(text) => {
if let Some(f) = &self.on_edit_end {
f(text);
}
}
Trans::None => {}
}
}
fn set_caret_extend(&self, caret: usize, extend: bool) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let caret = caret.min(s.engine.len_chars());
let main = Self::main_of(s);
s.carets = vec![main];
Self::main_mut(s).move_to(caret, extend);
Self::reset_escalate(s);
s.goal_x = None;
});
}
fn bump(&self) {
self.revision
.set(self.revision.get_untracked().wrapping_add(1));
}
fn font_weight(&self) -> FontWeight {
self.weight
.or_else(|| {
self.appearance
.and_then(|s| s.font_weight)
.map(FontWeight::new)
})
.unwrap_or_default()
}
fn resolved_font_size(&self, theme: &Theme) -> f32 {
self.font_size
.or_else(|| self.appearance.and_then(|s| s.font_size))
.unwrap_or(theme.font_size)
}
fn line_height(&self) -> f32 {
let size = self
.font_size
.or_else(|| self.appearance.and_then(|s| s.font_size))
.unwrap_or(14.0);
size * self
.line_height_factor
.or_else(|| self.appearance.and_then(|s| s.line_height))
.unwrap_or(DEFAULT_LINE_HEIGHT)
.clamp(1.0, 3.0)
}
fn resolved_line_height(&self, theme: &Theme) -> f32 {
self.resolved_font_size(theme)
* self
.line_height_factor
.or_else(|| self.appearance.and_then(|s| s.line_height))
.unwrap_or(DEFAULT_LINE_HEIGHT)
.clamp(1.0, 3.0)
}
fn resolved_text_color(&self, theme: &Theme) -> Color {
self.text_color
.or_else(|| self.appearance.and_then(|s| s.text))
.unwrap_or(theme.text)
}
fn resolved_placeholder_color(&self, theme: &Theme) -> Color {
self.placeholder_color
.or_else(|| self.appearance.and_then(|s| s.placeholder))
.unwrap_or(theme.text_secondary)
}
fn resolved_background(&self, theme: &Theme) -> Color {
self.background
.or_else(|| self.appearance.and_then(|s| s.background))
.unwrap_or(theme.surface)
}
fn resolved_border(&self, theme: &Theme) -> Option<(f32, Color)> {
let width = self
.border_width
.or_else(|| self.appearance.and_then(|s| s.border_width))
.unwrap_or(1.0)
.max(0.0);
if width <= 0.0 {
return None;
}
let color = self
.border_color
.or_else(|| self.appearance.and_then(|s| s.border))
.unwrap_or(theme.border);
if color.a <= 0.0 {
return None;
}
Some((width, color))
}
fn resolved_radius(&self, theme: &Theme) -> f32 {
self.radius
.or_else(|| self.appearance.and_then(|s| s.radius))
.unwrap_or(theme.radius)
.max(0.0)
}
fn delete_selection(&self) -> bool {
let source = self.source.clone();
let mut deleted = false;
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
if s.carets.len() <= 1 {
let m = Self::main_of(s);
let Some((mut a, mut b)) = m.anchor.map(|x| (x.min(m.offset), x.max(m.offset)))
else {
return;
};
if a == b {
Self::main_mut(s).anchor = None;
return;
}
(a, b) = (a.min(len), b.min(len));
let (ab, bb) = (s.engine.to_bytes(a), s.engine.to_bytes(b));
s.engine.delete(ab..bb);
source.commit(&mut s.engine);
let offset = s.engine.to_chars(ab);
let main = Self::main_mut(s);
main.offset = offset;
main.anchor = None;
s.goal_x = None;
Self::reset_escalate(s);
deleted = true;
return;
}
let mut pre: Vec<(usize, usize, usize)> = Vec::new();
for (i, c) in s.carets.iter().enumerate() {
if let Some((a, b)) = c.selected() {
let (a, b) = (a.min(len), b.min(len));
if a < b {
pre.push((i, s.engine.to_bytes(a), s.engine.to_bytes(b)));
}
} else if c.anchor.is_some() {
}
}
for c in s.carets.iter_mut() {
if c.selected().is_none() {
c.anchor = None;
}
}
if pre.is_empty() {
return;
}
let raw: Vec<(usize, usize)> = pre.iter().map(|&(_, a, b)| (a, b)).collect();
let (merged, owner) = merge_byte_ranges(&raw);
let ranges: Vec<std::ops::Range<usize>> = merged.iter().map(|&(a, b)| a..b).collect();
s.engine.delete_many(ranges);
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
let mut new_offsets = vec![0usize; s.carets.len()];
let mut touched = vec![false; s.carets.len()];
for (k, &(i, _, _)) in pre.iter().enumerate() {
let (us, _) = merged[owner[k]];
let mapped = map_through_deletes(&merged, us);
new_offsets[i] = s.engine.to_chars(mapped).min(new_len);
touched[i] = true;
}
for (i, c) in s.carets.iter_mut().enumerate() {
if touched[i] {
c.offset = new_offsets[i];
c.anchor = None;
}
}
dedupe_carets(&mut s.carets);
s.goal_x = None;
Self::reset_escalate(s);
deleted = true;
});
if deleted {
self.bump();
}
deleted
}
pub fn selected_text(&self) -> Option<String> {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let mut parts = Vec::new();
for c in &s.carets {
if let Some((a, b)) = c.selected() {
let (a, b) = (a.min(len), b.min(len));
if a < b {
let text = s
.engine
.buffer()
.substring(s.engine.to_bytes(a)..s.engine.to_bytes(b));
if !text.is_empty() {
parts.push(text);
}
}
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n"))
}
})
}
pub fn select_range(&self, a: usize, b: usize) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let (a, b) = (a.min(len), b.min(len));
s.carets = vec![Caret {
offset: b,
anchor: if a == b { None } else { Some(a) },
}];
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
pub fn clear_selection(&self) -> bool {
let had = self.with_state(|s| {
let had = s.carets.iter().any(|c| c.anchor.is_some());
for c in s.carets.iter_mut() {
c.anchor = None;
}
Self::reset_escalate(s);
had
});
if had {
self.bump();
}
had
}
pub fn select_all(&self) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
s.carets = vec![Caret {
offset: len,
anchor: Some(0),
}];
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn insert_str(&self, text: &str) {
if !self.editable || text.is_empty() {
return;
}
let source = self.source.clone();
let single = self.with_state(|s| s.carets.len() <= 1);
if single {
self.with_state(|st| {
source.ensure(&mut st.engine);
let total = st.engine.len_chars();
let sel_bytes = match Self::main_of(st).anchor.map(|a| {
(
a.min(Self::main_of(st).offset),
a.max(Self::main_of(st).offset),
)
}) {
None => None,
Some((a, b)) if a == b => {
Self::main_mut(st).anchor = None;
None
}
Some((a, b)) => {
let (a, b) = (a.min(total), b.min(total));
Some((st.engine.to_bytes(a), st.engine.to_bytes(b)))
}
};
let allowed = match self.max_length {
Some(max) => max.saturating_sub(total),
None => usize::MAX,
};
let ins = BufferEngine::fit_prefix(text, allowed);
let caret_b = match sel_bytes {
Some((ab, bb)) => {
st.engine.replace(ab..bb, ins);
ab + ins.len()
}
None => {
let caret_b = st.engine.to_bytes(Self::main_of(st).offset.min(total));
st.engine.insert(caret_b, ins);
caret_b + ins.len()
}
};
if let Some(max) = self.max_lines {
st.engine.enforce_max_lines(max);
}
source.commit(&mut st.engine);
let offset = st.engine.to_chars(caret_b).min(st.engine.len_chars());
let main = Self::main_mut(st);
main.offset = offset;
main.anchor = None;
st.goal_x = None;
Self::reset_escalate(st);
});
self.bump();
return;
}
self.with_state(|st| {
source.ensure(&mut st.engine);
let total = st.engine.len_chars();
let mut sel_pre: Vec<(usize, usize, usize)> = Vec::new();
for (i, c) in st.carets.iter().enumerate() {
if let Some((a, b)) = c.selected() {
let (a, b) = (a.min(total), b.min(total));
if a < b {
sel_pre.push((i, st.engine.to_bytes(a), st.engine.to_bytes(b)));
}
}
}
let raw: Vec<(usize, usize)> = sel_pre.iter().map(|&(_, a, b)| (a, b)).collect();
let (merged, owner) = merge_byte_ranges(&raw);
if !merged.is_empty() {
let ranges: Vec<std::ops::Range<usize>> =
merged.iter().map(|&(a, b)| a..b).collect();
st.engine.delete_many(ranges);
}
let mut points: Vec<usize> = Vec::with_capacity(st.carets.len());
let mut caret_union: Vec<Option<usize>> = vec![None; st.carets.len()];
for (k, &(i, _, _)) in sel_pre.iter().enumerate() {
caret_union[i] = Some(owner[k]);
}
for (i, c) in st.carets.iter().enumerate() {
let pre_b = match caret_union[i] {
Some(u) => merged[u].0,
None => st.engine.to_bytes(c.offset.min(total)),
};
let mut u_hit = caret_union[i];
if u_hit.is_none() {
for (u, &(us, ue)) in merged.iter().enumerate() {
if pre_b >= us && pre_b < ue {
u_hit = Some(u);
break;
}
}
}
let post_b = match u_hit {
Some(u) => map_through_deletes(&merged, merged[u].0),
None => map_through_deletes(&merged, pre_b),
};
points.push(post_b);
}
let allowed = match self.max_length {
Some(max) => max.saturating_sub(total),
None => usize::MAX,
};
let ins = BufferEngine::fit_prefix(text, allowed);
if ins.is_empty() {
source.commit(&mut st.engine);
let new_len = st.engine.len_chars();
for (i, c) in st.carets.iter_mut().enumerate() {
c.offset = st.engine.to_chars(points[i]).min(new_len);
c.anchor = None;
}
dedupe_carets(&mut st.carets);
st.goal_x = None;
Self::reset_escalate(st);
return;
}
let mut uniq = points.clone();
uniq.sort_unstable();
uniq.dedup();
let mut pcs: Vec<usize> = uniq.iter().map(|&p| st.engine.to_chars(p)).collect();
pcs.sort_unstable();
let ins_chars = ins.chars().count();
let at: Vec<usize> = uniq.clone();
st.engine.insert_at_many(at, ins);
if let Some(max) = self.max_lines {
st.engine.enforce_max_lines(max);
}
source.commit(&mut st.engine);
let new_len = st.engine.len_chars();
let mut new_offsets = vec![0usize; st.carets.len()];
for (i, &p) in points.iter().enumerate() {
let pc = st.engine.to_chars(p);
let below = pcs.iter().filter(|&&q| q < pc).count();
new_offsets[i] = (pc + ins_chars * (1 + below)).min(new_len);
}
for (i, c) in st.carets.iter_mut().enumerate() {
c.offset = new_offsets[i];
c.anchor = None;
}
dedupe_carets(&mut st.carets);
st.goal_x = None;
Self::reset_escalate(st);
});
self.bump();
}
fn delete_range_for(
engine: &BufferEngine,
caret_b: usize,
back: bool,
word: bool,
) -> (usize, usize) {
let buf = engine.buffer();
if back {
let from_b = if word {
sequoia_buffer::word_start_before(buf, caret_b)
} else {
sequoia_buffer::prev_grapheme(buf, caret_b)
};
(from_b, caret_b)
} else {
let to_b = if word {
sequoia_buffer::word_end_after(buf, caret_b)
} else {
sequoia_buffer::next_grapheme(buf, caret_b)
};
(caret_b, to_b)
}
}
fn range_styled(engine: &BufferEngine, s: usize, e: usize) -> bool {
s < e
&& engine
.styled_buffer()
.spans()
.iter()
.any(|sp| sp.start < e && s < sp.end)
}
fn backspace(&self, word: bool) {
if !self.editable {
return;
}
if self.delete_selection() {
return;
}
let source = self.source.clone();
if self.with_state(|s| s.carets.len() <= 1) {
if !word {
let stripped = self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let caret = Self::main_of(s).offset.min(len);
let caret_b = s.engine.to_bytes(caret);
let total_b = s.engine.to_bytes(len);
if caret == 0 {
return false;
}
let from_b = sequoia_buffer::prev_grapheme(s.engine.buffer(), caret_b);
if from_b == caret_b || !Self::range_styled(&s.engine, from_b, caret_b) {
return false;
}
let next_styled = if caret_b < total_b {
let to_b = sequoia_buffer::next_grapheme(s.engine.buffer(), caret_b);
Self::range_styled(&s.engine, caret_b, to_b)
} else {
false
};
if next_styled {
return false;
}
s.engine.clear_attr(from_b..caret_b);
source.commit(&mut s.engine);
Self::main_mut(s).offset = s.engine.to_chars(caret_b).min(s.engine.len_chars());
Self::main_mut(s).anchor = None;
s.goal_x = None;
Self::reset_escalate(s);
true
});
if stripped {
self.bump();
return;
}
}
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let caret = Self::main_of(s).offset.min(len);
if caret == 0 {
return;
}
let caret_b = s.engine.to_bytes(caret);
let (from_b, _) = Self::delete_range_for(&s.engine, caret_b, true, word);
s.engine.delete(from_b..caret_b);
source.commit(&mut s.engine);
let offset = s.engine.to_chars(from_b);
let main = Self::main_mut(s);
main.offset = offset;
main.anchor = None;
s.goal_x = None;
Self::reset_escalate(s);
});
self.bump();
return;
}
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let total_b = s.engine.to_bytes(len);
let pre_bytes: Vec<usize> = s
.carets
.iter()
.map(|c| s.engine.to_bytes(c.offset.min(len)))
.collect();
let mut stripped_bytes: Vec<usize> = Vec::new();
if !word {
for c in s.carets.iter() {
debug_assert!(c.selected().is_none());
let caret = c.offset.min(len);
if caret == 0 {
continue;
}
let caret_b = s.engine.to_bytes(caret);
let from_b = sequoia_buffer::prev_grapheme(s.engine.buffer(), caret_b);
if from_b == caret_b || !Self::range_styled(&s.engine, from_b, caret_b) {
continue;
}
let next_styled = if caret_b < total_b {
let to_b = sequoia_buffer::next_grapheme(s.engine.buffer(), caret_b);
Self::range_styled(&s.engine, caret_b, to_b)
} else {
false
};
if next_styled {
continue;
}
s.engine.clear_attr(from_b..caret_b);
stripped_bytes.push(caret_b);
}
if !stripped_bytes.is_empty() {
source.commit(&mut s.engine);
}
}
let mut ranges: Vec<(usize, usize)> = Vec::new();
let mut owners: Vec<usize> = Vec::new();
for (i, c) in s.carets.iter().enumerate() {
let caret = c.offset.min(len);
let caret_b = s.engine.to_bytes(caret);
if stripped_bytes.contains(&caret_b) {
continue;
}
if caret == 0 {
continue;
}
let (from_b, to_b) = Self::delete_range_for(&s.engine, caret_b, true, word);
if from_b < to_b {
ranges.push((from_b, to_b));
owners.push(i);
}
}
if ranges.is_empty() {
let new_len = s.engine.len_chars();
for c in s.carets.iter_mut() {
c.offset = c.offset.min(new_len);
c.anchor = None;
}
Self::reset_escalate(s);
return;
}
let raw = ranges.clone();
let (merged, owner) = merge_byte_ranges(&raw);
let dranges: Vec<std::ops::Range<usize>> = merged.iter().map(|&(a, b)| a..b).collect();
s.engine.delete_many(dranges);
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
let mut new_offsets = vec![0usize; s.carets.len()];
let mut touched = vec![false; s.carets.len()];
for (k, &i) in owners.iter().enumerate() {
let (us, _) = merged[owner[k]];
new_offsets[i] = s
.engine
.to_chars(map_through_deletes(&merged, us))
.min(new_len);
touched[i] = true;
}
for (i, c) in s.carets.iter_mut().enumerate() {
if !touched[i] {
let pb = pre_bytes[i];
c.offset = s
.engine
.to_chars(map_through_deletes(&merged, pb))
.min(new_len);
} else {
c.offset = new_offsets[i];
}
c.anchor = None;
}
s.goal_x = None;
Self::reset_escalate(s);
dedupe_carets(&mut s.carets);
});
self.bump();
}
fn delete(&self, word: bool) {
if !self.editable {
return;
}
if self.delete_selection() {
return;
}
let source = self.source.clone();
if self.with_state(|s| s.carets.len() <= 1) {
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let caret = Self::main_of(s).offset.min(len);
if caret >= len {
return;
}
let caret_b = s.engine.to_bytes(caret);
let (_, to_b) = Self::delete_range_for(&s.engine, caret_b, false, word);
s.engine.delete(caret_b..to_b);
source.commit(&mut s.engine);
let offset = s.engine.to_chars(caret_b);
let main = Self::main_mut(s);
main.offset = offset;
main.anchor = None;
s.goal_x = None;
Self::reset_escalate(s);
});
self.bump();
return;
}
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let pre_bytes: Vec<usize> = s
.carets
.iter()
.map(|c| s.engine.to_bytes(c.offset.min(len)))
.collect();
let mut ranges: Vec<(usize, usize)> = Vec::new();
let mut owners: Vec<usize> = Vec::new();
for (i, &cb) in pre_bytes.iter().enumerate() {
let caret = s.carets[i].offset.min(len);
if caret >= len {
continue;
}
let (_, to_b) = Self::delete_range_for(&s.engine, cb, false, word);
if cb < to_b {
ranges.push((cb, to_b));
owners.push(i);
}
}
if ranges.is_empty() {
Self::reset_escalate(s);
return;
}
let (merged, owner) = merge_byte_ranges(&ranges);
let dranges: Vec<std::ops::Range<usize>> = merged.iter().map(|&(a, b)| a..b).collect();
s.engine.delete_many(dranges);
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
let mut new_offsets = vec![0usize; s.carets.len()];
let mut touched = vec![false; s.carets.len()];
for (k, &i) in owners.iter().enumerate() {
let (us, _) = merged[owner[k]];
new_offsets[i] = s
.engine
.to_chars(map_through_deletes(&merged, us))
.min(new_len);
touched[i] = true;
}
for (i, c) in s.carets.iter_mut().enumerate() {
if touched[i] {
c.offset = new_offsets[i];
} else {
c.offset = s
.engine
.to_chars(map_through_deletes(&merged, pre_bytes[i]))
.min(new_len);
}
c.anchor = None;
}
s.goal_x = None;
Self::reset_escalate(s);
dedupe_carets(&mut s.carets);
});
self.bump();
}
fn move_caret(&self, delta: isize, extend: bool) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
for c in s.carets.iter_mut() {
let mut caret_b = s.engine.to_bytes(c.offset.min(len));
let steps = delta.unsigned_abs().max(1);
for _ in 0..steps {
let b = if delta < 0 {
sequoia_buffer::prev_grapheme(s.engine.buffer(), caret_b)
} else {
sequoia_buffer::next_grapheme(s.engine.buffer(), caret_b)
};
if b == caret_b {
break;
}
caret_b = b;
}
c.move_to(s.engine.to_chars(caret_b), extend);
}
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn move_word(&self, dir: isize, extend: bool) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
for c in s.carets.iter_mut() {
let caret_b = s.engine.to_bytes(c.offset.min(len));
let next_b = if dir < 0 {
sequoia_buffer::word_start_before(s.engine.buffer(), caret_b)
} else {
sequoia_buffer::word_end_after(s.engine.buffer(), caret_b)
};
c.move_to(s.engine.to_chars(next_b), extend);
}
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn move_home(&self, extend: bool, doc: bool, ctx: &mut EventCtx) {
let layout = self.event_layout(ctx);
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
for c in s.carets.iter_mut() {
let next = if doc {
0
} else {
let ri = layout.row_of(c.offset.min(len));
layout.rows.get(ri).map(|r| r.start).unwrap_or(0)
};
c.move_to(next, extend);
}
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn move_end(&self, extend: bool, doc: bool, ctx: &mut EventCtx) {
let layout = self.event_layout(ctx);
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let total = s.engine.len_chars();
for c in s.carets.iter_mut() {
let next = if doc {
total
} else {
let ri = layout.row_of(c.offset.min(total));
layout.rows.get(ri).map(|r| r.end).unwrap_or(total)
};
c.move_to(next, extend);
}
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn row_at_y(line_h: f32, rows: usize, cy: f32) -> usize {
if line_h <= 0.0 || rows == 0 {
return 0;
}
((cy / line_h).floor() as isize).clamp(0, rows.saturating_sub(1) as isize) as usize
}
fn move_visual(&self, dir: isize, steps: usize, extend: bool, ctx: &mut EventCtx) {
let layout = self.event_layout(ctx);
let theme = ctx.environment.get::<Theme>().unwrap_or_else(Theme::dark);
let size = self.resolved_font_size(&theme);
let weight = self.font_weight();
let total = layout.rows.last().map(|r| r.end).unwrap_or(0);
let mains: Vec<usize> = self.with_state(|s| {
s.carets
.iter()
.map(|c| c.offset.min(total))
.collect()
});
let main_goal = self.with_state(|s| s.goal_x);
let mut main_goal_next = main_goal;
let mut nexts: Vec<Option<usize>> = Vec::with_capacity(mains.len());
let last = layout.rows.len().saturating_sub(1);
for (idx, caret) in mains.iter().enumerate() {
let ri = layout.row_of(*caret);
let target = if dir < 0 {
ri.saturating_sub(steps)
} else {
(ri + steps).min(last)
};
if target == ri || layout.rows.is_empty() {
nexts.push(None);
continue;
}
let current = layout.rows[ri];
let col = caret
.saturating_sub(current.start)
.min(current.end - current.start);
let caret_x = row_prefix_x(ctx, &layout, ¤t, col, size, weight);
let goal = if idx == 0 {
match main_goal {
Some(g) => g,
None => {
main_goal_next = Some(caret_x);
caret_x
}
}
} else {
caret_x
};
let target_row = layout.rows[target];
let text = layout.row_text(&target_row);
let target_col = ctx
.char_index_from_x(&text, size, weight, goal)
.min(target_row.end - target_row.start);
nexts.push(Some(target_row.start + target_col));
}
if nexts.iter().all(|n| n.is_none()) {
if mains.len() == 1 && !extend {
self.with_state(|s| s.goal_x = None);
}
return;
}
self.with_state(|s| {
for (c, next) in s.carets.iter_mut().zip(nexts.iter()) {
if let Some(next) = next {
c.move_to(*next, extend);
}
}
s.goal_x = main_goal_next;
Self::reset_escalate(s);
});
}
fn move_page(
&self,
dir: isize,
extend: bool,
viewport_h: f32,
line_h: f32,
ctx: &mut EventCtx,
) {
let step = if viewport_h <= 0.0 || line_h <= 0.0 {
1
} else {
((viewport_h / line_h).floor() as usize).max(1)
};
self.move_visual(dir, step, extend, ctx);
self.with_state(|s| {
let max_y = (s.content.height + self.pad_y * 2.0 - viewport_h).max(0.0);
s.scroll.y = (s.scroll.y + dir as f32 * viewport_h).clamp(0.0, max_y);
});
self.bump();
}
fn on_enter(&self) {
if !self.editable {
return;
}
if self.with_state(|s| s.carets.len() <= 1) {
self.delete_selection();
let source = self.source.clone();
let indent = self.with_state(|s| {
source.ensure(&mut s.engine);
if !self.auto_indent {
return String::new();
}
let caret_b = s
.engine
.to_bytes(Self::main_of(s).offset.min(s.engine.len_chars()));
let lines = s.engine.buffer().lines();
let (ls, le) = lines.line_range(lines.line_of(caret_b));
let line_text = s.engine.buffer().substring(ls..le);
line_text
.chars()
.take(leading_indent_len(&line_text))
.collect()
});
self.insert_str(&format!("\n{indent}"));
return;
}
self.delete_selection();
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let total = s.engine.len_chars();
let mut items: Vec<(usize, String)> = Vec::with_capacity(s.carets.len());
for c in s.carets.iter() {
let caret_b = s.engine.to_bytes(c.offset.min(total));
let indent = if self.auto_indent {
let lines = s.engine.buffer().lines();
let (ls, le) = lines.line_range(lines.line_of(caret_b));
let line_text = s.engine.buffer().substring(ls..le);
line_text
.chars()
.take(leading_indent_len(&line_text))
.collect::<String>()
} else {
String::new()
};
items.push((caret_b, indent));
}
let mut uniq: Vec<usize> = items.iter().map(|(b, _)| *b).collect();
uniq.sort_unstable();
uniq.dedup();
s.engine.insert_at_many(uniq.clone(), "\n");
if let Some(max) = self.max_lines {
s.engine.enforce_max_lines(max);
}
let post_nl = |pb: usize| -> usize { pb + uniq.iter().filter(|&&q| q <= pb).count() };
let cur_chars = s.engine.len_chars();
let indent_total: usize = items.iter().map(|(_, it)| it.chars().count()).sum();
let allow_indent = match self.max_length {
Some(max) => cur_chars + indent_total <= max,
None => true,
};
let mut phase2: Vec<(usize, String)> = items
.iter()
.map(|(pb, indent)| (post_nl(*pb), indent.clone()))
.filter(|(_, indent)| !indent.is_empty())
.collect();
if !allow_indent {
phase2.clear();
}
phase2.sort_by_key(|b| std::cmp::Reverse(b.0));
let mut k = 0usize;
while k < phase2.len() {
let mut j = k + 1;
while j < phase2.len() && phase2[j].1 == phase2[k].1 {
j += 1;
}
let mut pts: Vec<usize> = phase2[k..j].iter().map(|(p, _)| *p).collect();
pts.sort_unstable();
pts.dedup();
let text = phase2[k].1.clone();
s.engine.insert_at_many(pts, &text);
k = j;
}
if let Some(max) = self.max_lines {
s.engine.enforce_max_lines(max);
}
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
let mut finals: Vec<usize> = Vec::with_capacity(items.len());
for (pb, indent) in &items {
let q = post_nl(*pb);
let own = if allow_indent { indent.len() } else { 0 };
let below: usize = phase2
.iter()
.filter(|(p, _)| *p < q)
.map(|(_, t)| t.len())
.sum();
finals.push(s.engine.to_chars(q + own + below).min(new_len));
}
for (c, f) in s.carets.iter_mut().zip(finals.iter()) {
c.offset = *f;
c.anchor = None;
}
dedupe_carets(&mut s.carets);
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn on_tab(&self, extend_shift: bool) {
if !self.editable {
return;
}
if extend_shift {
self.outdent();
} else if !self.indent_selection() {
let indent = self.indent.clone();
if self.with_state(|s| s.carets.len() <= 1) {
self.insert_str(&indent);
} else {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let total = s.engine.len_chars();
let mut pts: Vec<usize> = s
.carets
.iter()
.map(|c| s.engine.to_bytes(c.offset.min(total)))
.collect();
pts.sort_unstable();
pts.dedup();
let allowed = match self.max_length {
Some(max) => max.saturating_sub(s.engine.len_chars()),
None => usize::MAX,
};
let ins = BufferEngine::fit_prefix(&indent, allowed / s.carets.len().max(1));
if ins.is_empty() {
return;
}
let mut pcs: Vec<usize> = pts.iter().map(|&p| s.engine.to_chars(p)).collect();
pcs.sort_unstable();
let n = ins.chars().count();
let pre: Vec<usize> = s.carets.iter().map(|c| c.offset.min(total)).collect();
s.engine.insert_at_many(pts, ins);
if let Some(max) = self.max_lines {
s.engine.enforce_max_lines(max);
}
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
for (c, &old) in s.carets.iter_mut().zip(pre.iter()) {
let below = pcs.iter().filter(|&&q| q < old).count();
c.offset = (old + n * (1 + below)).min(new_len);
c.anchor = None;
}
dedupe_carets(&mut s.carets);
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
}
}
fn indent_selection(&self) -> bool {
if self.indent.is_empty() {
return false;
}
let source = self.source.clone();
let indent = self.indent.clone();
let per = indent.len();
let take_block = self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let buf = s.engine.buffer();
let lines = buf.lines();
let total_b = buf.len_bytes();
let mut touched: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut any_multi = false;
for c in s.carets.iter() {
match c.selected() {
Some((a, b)) => {
let (a, b) = (a.min(len), b.min(len));
if a >= b {
continue;
}
let (ab, bb) = (s.engine.to_bytes(a), s.engine.to_bytes(b));
let l0 = lines.line_of(ab);
let (mut l1, c1) = lines.line_col(bb.min(total_b));
if c1 == 0 && l1 > l0 {
l1 -= 1;
}
if l1 > l0 {
any_multi = true;
}
for l in l0..=l1 {
touched.insert(l);
}
}
None => {
let cb = s.engine.to_bytes(c.offset.min(len));
touched.insert(lines.line_of(cb));
}
}
}
if touched.is_empty() {
return false;
}
if s.carets.len() <= 1 && !any_multi {
return false;
}
true
});
if !take_block {
return false;
}
self.with_state(|s| {
source.ensure(&mut s.engine);
let buf = s.engine.buffer();
let lines = buf.lines();
let total_b = buf.len_bytes();
let len = s.engine.len_chars();
let mut touched: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
for c in s.carets.iter() {
match c.selected() {
Some((a, b)) => {
let (a, b) = (a.min(len), b.min(len));
if a >= b {
continue;
}
let (ab, bb) = (s.engine.to_bytes(a), s.engine.to_bytes(b));
let l0 = lines.line_of(ab);
let (mut l1, c1) = lines.line_col(bb.min(total_b));
if c1 == 0 && l1 > l0 {
l1 -= 1;
}
for l in l0..=l1 {
touched.insert(l);
}
}
None => {
let cb = s.engine.to_bytes(c.offset.min(len));
touched.insert(lines.line_of(cb));
}
}
}
let at: Vec<usize> = touched.iter().map(|&l| lines.line_start(l)).collect();
let shift = |pos: usize| -> usize {
let mut k = 0usize;
for &l in touched.iter() {
if lines.line_start(l) <= pos {
k += 1;
} else {
break;
}
}
pos + per * k
};
let ends: Vec<(usize, usize)> = s
.carets
.iter()
.map(|c| {
let (a, b) = c.ends();
let (a, b) = (a.min(len), b.min(len));
(shift(s.engine.to_bytes(a)), shift(s.engine.to_bytes(b)))
})
.collect();
s.engine.insert_at_many(at, &indent);
if let Some(max) = self.max_lines {
s.engine.enforce_max_lines(max);
}
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
for (c, (na, nb)) in s.carets.iter_mut().zip(ends.iter()) {
let (na, nb) = (
s.engine.to_chars(*na).min(new_len),
s.engine.to_chars(*nb).min(new_len),
);
let was_fwd = c.offset >= c.anchor.unwrap_or(c.offset);
if na == nb {
c.offset = na;
c.anchor = None;
} else if was_fwd {
c.offset = nb;
c.anchor = Some(na);
} else {
c.offset = na;
c.anchor = Some(nb);
}
}
dedupe_carets(&mut s.carets);
s.goal_x = None;
Self::reset_escalate(s);
});
self.bump();
true
}
fn outdent(&self) {
let indent_w = self.indent.chars().count().max(1);
let source = self.source.clone();
let take_block = self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let buf = s.engine.buffer();
let lines = buf.lines();
let total_b = buf.len_bytes();
let mut touched: std::collections::BTreeSet<usize> = std::collections::BTreeSet::new();
let mut any_multi = false;
for c in s.carets.iter() {
match c.selected() {
Some((a, b)) => {
let (a, b) = (a.min(len), b.min(len));
if a >= b {
continue;
}
let (ab, bb) = (s.engine.to_bytes(a), s.engine.to_bytes(b));
let l0 = lines.line_of(ab);
let (mut l1, c1) = lines.line_col(bb.min(total_b));
if c1 == 0 && l1 > l0 {
l1 -= 1;
}
if l1 > l0 {
any_multi = true;
}
for l in l0..=l1 {
touched.insert(l);
}
}
None => {
let cb = s.engine.to_bytes(c.offset.min(len));
touched.insert(lines.line_of(cb));
}
}
}
s.carets.len() > 1 || any_multi || touched.len() > 1
});
if take_block {
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let buf = s.engine.buffer();
let lines = buf.lines();
let total_b = buf.len_bytes();
let mut touched: std::collections::BTreeSet<usize> =
std::collections::BTreeSet::new();
for c in s.carets.iter() {
match c.selected() {
Some((a, b)) => {
let (a, b) = (a.min(len), b.min(len));
if a >= b {
continue;
}
let (ab, bb) = (s.engine.to_bytes(a), s.engine.to_bytes(b));
let l0 = lines.line_of(ab);
let (mut l1, c1) = lines.line_col(bb.min(total_b));
if c1 == 0 && l1 > l0 {
l1 -= 1;
}
for l in l0..=l1 {
touched.insert(l);
}
}
None => {
let cb = s.engine.to_bytes(c.offset.min(len));
touched.insert(lines.line_of(cb));
}
}
}
let mut ranges = Vec::new();
let mut take_of: std::collections::BTreeMap<usize, usize> =
std::collections::BTreeMap::new();
for &line in touched.iter() {
let (ls, le) = lines.line_range(line);
let head = buf.substring(ls..le.min(ls + indent_w + 1));
let take = head
.chars()
.take_while(|&c| c == ' ' || c == '\t')
.take(indent_w)
.map(|c| c.len_utf8())
.sum::<usize>();
take_of.insert(line, take);
if take > 0 {
ranges.push(ls..ls + take);
}
}
let map = |pos: usize| -> usize {
let (line, _) = lines.line_col(pos.min(total_b));
let mut sub = 0usize;
for (&l, &take) in take_of.iter() {
if l > line {
break;
}
if l < line || lines.line_start(l) + take <= pos {
sub += take;
}
}
pos.saturating_sub(sub)
};
let ends: Vec<(usize, usize)> = s
.carets
.iter()
.map(|c| {
let (a, b) = c.ends();
let (a, b) = (a.min(len), b.min(len));
(map(s.engine.to_bytes(a)), map(s.engine.to_bytes(b)))
})
.collect();
if !ranges.is_empty() {
s.engine.delete_many(ranges);
source.commit(&mut s.engine);
}
let new_len = s.engine.len_chars();
for (c, (na, nb)) in s.carets.iter_mut().zip(ends.iter()) {
let (na, nb) = (
s.engine.to_chars(*na).min(new_len),
s.engine.to_chars(*nb).min(new_len),
);
let was_fwd = c.offset >= c.anchor.unwrap_or(c.offset);
if na == nb {
c.offset = na;
c.anchor = None;
} else if was_fwd {
c.offset = nb;
c.anchor = Some(na);
} else {
c.offset = na;
c.anchor = Some(nb);
}
}
dedupe_carets(&mut s.carets);
s.goal_x = None;
Self::reset_escalate(s);
});
self.bump();
return;
}
let moved = self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let caret = Self::main_of(s).offset.min(len);
let caret_b = s.engine.to_bytes(caret);
let lines = s.engine.buffer().lines();
let start = lines.line_start(lines.line_of(caret_b));
let prefix = s.engine.buffer().substring(start..caret_b);
let mut strip = 0usize;
for c in prefix.chars().rev().take(indent_w) {
if c == ' ' || c == '\t' {
strip += c.len_utf8();
} else {
break;
}
}
if strip == 0 || caret_b == start {
return None;
}
s.engine.delete(caret_b - strip..caret_b);
source.commit(&mut s.engine);
Some(s.engine.to_chars(caret_b - strip))
});
if let Some(caret) = moved {
self.with_state(|s| {
Self::main_mut(s).offset = caret;
Self::main_mut(s).anchor = None;
s.goal_x = None;
Self::reset_escalate(s);
});
self.bump();
}
}
pub const LINK_PLACEHOLDER: &'static str = "https://example.com";
fn word_at(engine: &BufferEngine, text: &str, caret: usize) -> Option<(usize, usize)> {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let c = caret.min(len);
let on_word = |i: usize| chars.get(i).is_some_and(|c| is_word_char(*c));
if !(on_word(c) || (c > 0 && on_word(c - 1))) {
return None;
}
let buf = engine.buffer();
let b = engine.to_bytes(c);
let sb = sequoia_buffer::word_start_before(buf, b);
let eb = sequoia_buffer::word_end_after(buf, b);
if sb >= eb {
return None;
}
let (a, z) = (engine.to_chars(sb), engine.to_chars(eb));
if a < z { Some((a, z)) } else { None }
}
fn line_at(engine: &BufferEngine, len_chars: usize, caret: usize) -> (usize, usize) {
let cb = engine.to_bytes(caret.min(len_chars));
let lines = engine.buffer().lines();
let (bs, be) = lines.line_range(lines.line_of(cb));
let total_b = engine.to_bytes(len_chars);
let s = engine.to_chars(bs);
let e = if be < total_b {
engine.to_chars(be) + 1
} else {
engine.to_chars(be)
};
(s, e.min(len_chars))
}
fn toggle_flag(&self, flag: StyleFlag) {
if !self.editable {
return;
}
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let snapshot = s.engine.styled_buffer().to_string();
let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(s.carets.len());
for c in s.carets.iter() {
let r = match c.selected() {
Some((a, b)) => {
let (a, b) = (a.min(len), b.min(len));
if a < b { Some((a, b)) } else { None }
}
None => Self::word_at(&s.engine, &snapshot, c.offset),
};
if let Some(r) = r {
ranges.push(r);
}
}
if ranges.is_empty() {
return;
}
for (a, b) in ranges {
let sb = s.engine.to_bytes(a);
let eb = s.engine.to_bytes(b);
if sb >= eb {
continue;
}
let spans: Vec<StyledSpan> = s.engine.styled_buffer().spans().to_vec();
let mut covered = sb;
let mut all_set = true;
for sp in spans.iter() {
if sp.end <= covered {
continue;
}
if sp.start > covered {
all_set = false;
break;
}
if !flag.get(&sp.attr) {
all_set = false;
break;
}
covered = covered.max(sp.end);
if covered >= eb {
break;
}
}
if covered < eb {
all_set = false;
}
if all_set {
let pieces: Vec<(usize, usize, SpanAttr)> = spans
.iter()
.filter(|sp| sp.start < eb && sb < sp.end)
.map(|sp| {
let ps = sp.start.max(sb);
let pe = sp.end.min(eb);
let mut attr = sp.attr.clone();
flag.set(&mut attr, false);
(ps, pe, attr)
})
.collect();
for (ps, pe, attr) in pieces {
s.engine.set_attr(ps..pe, attr);
}
} else {
let mut bounds = vec![sb, eb];
for sp in spans.iter() {
if sp.start > sb && sp.start < eb {
bounds.push(sp.start);
}
if sp.end > sb && sp.end < eb {
bounds.push(sp.end);
}
}
bounds.sort_unstable();
bounds.dedup();
for w in bounds.windows(2) {
let (ps, pe) = (w[0], w[1]);
let mut attr = spans
.iter()
.find(|sp| sp.start <= ps && pe <= sp.end)
.map(|sp| sp.attr.clone())
.unwrap_or_else(SpanAttr::plain);
flag.set(&mut attr, true);
s.engine.set_attr(ps..pe, attr);
}
}
}
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
for c in s.carets.iter_mut() {
c.offset = c.offset.min(new_len);
if let Some(an) = c.anchor {
c.anchor = Some(an.min(new_len));
if c.anchor == Some(c.offset) {
c.anchor = None;
}
}
}
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
pub fn toggle_bold(&self) {
self.toggle_flag(StyleFlag::Bold);
}
pub fn toggle_italic(&self) {
self.toggle_flag(StyleFlag::Italic);
}
pub fn toggle_underline(&self) {
self.toggle_flag(StyleFlag::Underline);
}
pub fn toggle_strike(&self) {
self.toggle_flag(StyleFlag::Strike);
}
pub fn apply_link(&self) {
self.apply_link_url(Self::LINK_PLACEHOLDER);
}
pub fn apply_link_url(&self, url: &str) {
if !self.editable {
return;
}
let source = self.source.clone();
let url = url.to_string();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let snapshot = s.engine.styled_buffer().to_string();
let mut ranges: Vec<(usize, usize)> = Vec::new();
for c in s.carets.iter() {
let r = match c.selected() {
Some((a, b)) => {
let (a, b) = (a.min(len), b.min(len));
if a < b { Some((a, b)) } else { None }
}
None => Self::word_at(&s.engine, &snapshot, c.offset),
};
if let Some(r) = r {
ranges.push(r);
}
}
if ranges.is_empty() {
return;
}
let mut mids: Vec<usize> = Vec::with_capacity(ranges.len());
for (a, b) in &ranges {
let sb = s.engine.to_bytes(*a);
let eb = s.engine.to_bytes(*b);
if sb < eb {
s.engine.set_attr(sb..eb, SpanAttr::link(url.clone()));
}
mids.push(a + (b - a) / 2);
}
source.commit(&mut s.engine);
let new_len = s.engine.len_chars();
for (c, m) in s.carets.iter_mut().zip(mids.iter()) {
c.offset = (*m).min(new_len);
c.anchor = None;
}
dedupe_carets(&mut s.carets);
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
}
fn on_style_shortcut(&self, key: &KeyEvent) -> bool {
let cmd =
key.modifiers.contains(Modifiers::CONTROL) || key.modifiers.contains(Modifiers::SUPER);
if !cmd || key.modifiers.contains(Modifiers::ALT) {
return false;
}
let Some(c) = key.key.char().map(|c| c.to_ascii_lowercase()) else {
return false;
};
match c {
'b' => {
if self.editable {
self.toggle_bold();
}
true
}
'i' => {
if self.editable {
self.toggle_italic();
}
true
}
'u' => {
if self.editable {
self.toggle_underline();
}
true
}
'k' => {
if self.editable {
self.apply_link();
}
true
}
_ => false,
}
}
fn try_auto_close(&self, c: char) -> bool {
let (open, close): (&str, &str) = match c {
'*' => ("**", "**"),
'_' => ("_", "_"),
'`' => ("`", "`"),
_ => return false,
};
if !self.editable {
return false;
}
let source = self.source.clone();
enum Role {
Hop(usize),
Wrap(usize, usize, usize),
Plain(usize),
}
let plan = self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let snapshot = s.engine.styled_buffer().to_string();
let snap_chars: Vec<char> = snapshot.chars().collect();
let mut roles: Vec<Role> = Vec::with_capacity(s.carets.len());
for (i, caret) in s.carets.iter().enumerate() {
if let Some((a, b)) = caret.selected() {
let (a, b) = (a.min(len), b.min(len));
if a < b {
roles.push(Role::Wrap(i, a, b));
continue;
}
}
let p = caret.offset.min(len);
let tail: String = snap_chars[p..].iter().collect();
if tail.starts_with(close) {
roles.push(Role::Hop(i));
continue;
}
match Self::word_at(&s.engine, &snapshot, p) {
Some((w0, w1)) => roles.push(Role::Wrap(i, w0, w1)),
None => roles.push(Role::Plain(i)),
}
}
let _ = open;
Some(roles)
});
let Some(roles) = plan else { return false };
let has_wrap = roles.iter().any(|r| matches!(r, Role::Wrap(..)));
let has_hop = roles.iter().any(|r| matches!(r, Role::Hop(_)));
if !has_wrap && !has_hop {
return false;
}
let single = c.to_string();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
if !has_wrap {
let snapshot = s.engine.styled_buffer().to_string();
let snap_chars: Vec<char> = snapshot.chars().collect();
for (i, caret) in s.carets.iter_mut().enumerate() {
let still_hop = roles.iter().any(|r| matches!(r, Role::Hop(j) if *j == i));
if !still_hop {
continue;
}
let p = caret.offset.min(len);
let tail: String = snap_chars[p..].iter().collect();
let step = if tail.starts_with(close) {
close.chars().count()
} else {
0
};
caret.offset = (p + step).min(len);
caret.anchor = None;
}
Self::reset_escalate(s);
s.goal_x = None;
return;
}
let mut pairs: Vec<(usize, String, usize)> = Vec::new();
let mut bases: Vec<(usize, usize)> = Vec::with_capacity(roles.len());
for r in roles.iter() {
match r {
Role::Wrap(i, a, b) => {
let ab = s.engine.to_bytes((*a).min(len));
let bb = s.engine.to_bytes((*b).min(len));
pairs.push((ab, open.to_string(), *a));
pairs.push((bb, close.to_string(), *b));
bases.push((*i, *b));
}
Role::Plain(i) => {
let c = s.carets[*i].offset.min(len);
pairs.push((s.engine.to_bytes(c), single.clone(), c));
bases.push((*i, c));
}
Role::Hop(i) => {
bases.push((*i, s.carets[*i].offset.min(len)));
}
}
}
let added: usize = pairs.iter().map(|(_, t, _)| t.chars().count()).sum();
if let Some(max) = self.max_length
&& s.engine.len_chars() + added > max
{
return;
}
let mut byte_to_char: std::collections::HashMap<usize, usize> =
std::collections::HashMap::new();
for (pb, _, pc) in pairs.iter() {
byte_to_char.entry(*pb).or_insert(*pc);
}
let mut ordered = pairs.clone();
ordered.sort_by_key(|b| std::cmp::Reverse(b.0));
let mut applied: Vec<(usize, usize)> = Vec::new();
let mut used: std::collections::HashSet<usize> = std::collections::HashSet::new();
let mut k = 0usize;
while k < ordered.len() {
let mut j = k + 1;
while j < ordered.len() && ordered[j].1 == ordered[k].1 {
j += 1;
}
let mut pts: Vec<usize> = ordered[k..j].iter().map(|(p, _, _)| *p).collect();
pts.sort_unstable();
pts.dedup();
let fresh: Vec<usize> = pts.into_iter().filter(|p| used.insert(*p)).collect();
if !fresh.is_empty() {
let text = ordered[k].1.clone();
let clen = text.chars().count();
for p in fresh.iter() {
if let Some(&pc) = byte_to_char.get(p) {
applied.push((pc, clen));
}
}
s.engine.insert_at_many(fresh, &text);
}
k = j;
}
if let Some(max) = self.max_lines {
s.engine.enforce_max_lines(max);
}
source.commit(&mut s.engine);
applied.sort_unstable();
let new_len = s.engine.len_chars();
for (i, base_ch) in bases {
let add: usize = applied
.iter()
.filter(|(pc, _)| *pc <= base_ch)
.map(|(_, l)| l)
.sum();
s.carets[i].offset = (base_ch + add).min(new_len);
s.carets[i].anchor = None;
}
dedupe_carets(&mut s.carets);
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
true
}
fn undo(&self) {
if !self.editable {
return;
}
let source = self.source.clone();
let restored = self.with_state(|s| {
source.ensure(&mut s.engine);
let text_before = s.engine.styled_buffer().to_string();
match s.engine.undo() {
Some((a, b)) => {
source.commit(&mut s.engine);
if s.engine.styled_buffer().to_string() == text_before {
let len = s.engine.len_chars();
for c in s.carets.iter_mut() {
c.offset = c.offset.min(len);
if let Some(an) = c.anchor {
c.anchor = Some(an.min(len));
if c.anchor == Some(c.offset) {
c.anchor = None;
}
}
}
} else {
let at = s.engine.to_chars(a).min(s.engine.len_chars());
s.carets = vec![Caret::collapsed(at)];
let _ = b;
}
s.goal_x = None;
Self::reset_escalate(s);
true
}
None => false,
}
});
if restored {
self.bump();
}
}
fn redo(&self) {
if !self.editable {
return;
}
let source = self.source.clone();
let restored = self.with_state(|s| {
source.ensure(&mut s.engine);
let text_before = s.engine.styled_buffer().to_string();
match s.engine.redo() {
Some((a, b)) => {
source.commit(&mut s.engine);
if s.engine.styled_buffer().to_string() == text_before {
let len = s.engine.len_chars();
for c in s.carets.iter_mut() {
c.offset = c.offset.min(len);
if let Some(an) = c.anchor {
c.anchor = Some(an.min(len));
if c.anchor == Some(c.offset) {
c.anchor = None;
}
}
}
let _ = a;
} else {
let at = s.engine.to_chars(b).min(s.engine.len_chars());
s.carets = vec![Caret::collapsed(at)];
}
s.goal_x = None;
Self::reset_escalate(s);
true
}
None => false,
}
});
if restored {
self.bump();
}
}
fn escalate_select(&self) {
let source = self.source.clone();
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
if s.carets.is_empty() {
s.carets.push(Caret::default());
}
let snapshot = s.engine.styled_buffer().to_string();
let main = s.carets[0];
let cur_sel = main.selected().map(|(a, b)| (a.min(len), b.min(len)));
let (ns, ne, level) = match s.escalate {
0 => match cur_sel {
None => match Self::word_at(&s.engine, &snapshot, main.offset) {
Some((w0, w1)) => (w0, w1, 1),
None => {
let (l0, l1) = Self::line_at(&s.engine, len, main.offset);
(l0, l1, 2)
}
},
Some((a, b)) if a == b => match Self::word_at(&s.engine, &snapshot, a) {
Some((w0, w1)) => (w0, w1, 1),
None => {
let (l0, l1) = Self::line_at(&s.engine, len, a);
(l0, l1, 2)
}
},
Some(_) => (0, len, 3),
},
1 => {
let (l0, l1) = Self::line_at(&s.engine, len, s.escalate_base);
(l0, l1, 2)
}
_ => (0, len, 3),
};
let base = if s.escalate == 0 {
main.offset.min(len)
} else {
s.escalate_base
};
s.carets = vec![Caret {
offset: ne,
anchor: if ns == ne { None } else { Some(ns) },
}];
s.escalate = level;
s.escalate_base = base;
s.goal_x = None;
});
self.bump();
}
fn handle_click(&self, caret: usize, extend: bool, alt: bool, now_ms: u128) {
const TRIPLE_MS: u128 = 500;
let source = self.source.clone();
if alt {
self.with_state(|s| {
source.ensure(&mut s.engine);
let caret = caret.min(s.engine.len_chars());
if let Some(idx) = s.carets.iter().position(|c| c.offset == caret) {
if s.carets.len() > 1 {
s.carets.remove(idx);
if let Some(ad) = s.alt_drag {
if ad == idx {
s.alt_drag = None;
} else if ad > idx {
s.alt_drag = Some(ad - 1);
}
}
} else {
s.carets[0].move_to(caret, extend);
}
} else {
s.carets.push(Caret::collapsed(caret));
s.alt_drag = Some(s.carets.len() - 1);
}
Self::reset_escalate(s);
s.goal_x = None;
});
self.bump();
return;
}
if extend {
self.set_caret_extend(caret, true);
return;
}
self.with_state(|s| {
source.ensure(&mut s.engine);
let len = s.engine.len_chars();
let caret = caret.min(len);
let chained = match s.last_click_ms {
Some(t) => now_ms.saturating_sub(t) <= TRIPLE_MS,
None => false,
};
s.click_count = if chained { s.click_count + 1 } else { 1 };
s.last_click_ms = Some(now_ms);
s.last_click_pos = caret;
s.alt_drag = None;
match s.click_count % 3 {
1 => {
s.carets = vec![Caret::collapsed(caret)];
Self::reset_escalate(s);
}
2 => {
let snapshot = s.engine.styled_buffer().to_string();
match Self::word_at(&s.engine, &snapshot, caret) {
Some((w0, w1)) => {
s.carets = vec![Caret {
offset: w1,
anchor: Some(w0),
}];
s.escalate = 1;
s.escalate_base = caret;
}
None => {
s.carets = vec![Caret::collapsed(caret)];
Self::reset_escalate(s);
}
}
}
_ => {
let (l0, l1) = Self::line_at(&s.engine, len, caret);
s.carets = vec![Caret {
offset: l1,
anchor: if l0 == l1 { None } else { Some(l0) },
}];
s.escalate = 2;
s.escalate_base = caret;
}
}
s.goal_x = None;
});
self.bump();
}
fn on_key(&self, key: &KeyEvent, ctx: &mut EventCtx) {
let extend = key.modifiers.contains(Modifiers::SHIFT);
let composing = self.with_state(|s| !s.preedit.is_empty());
let ctrl =
key.modifiers.contains(Modifiers::CONTROL) || key.modifiers.contains(Modifiers::SUPER);
let alt = key.modifiers.contains(Modifiers::ALT);
match key.key {
Key::BACKSPACE => {
if !ctrl {
self.backspace(false);
} else {
self.backspace(true);
}
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::DELETE => {
if !ctrl {
self.delete(false);
} else {
self.delete(true);
}
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::ARROW_LEFT => {
if ctrl || alt {
self.move_word(-1, extend);
} else {
self.move_caret(-1, extend);
}
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::ARROW_RIGHT => {
if ctrl || alt {
self.move_word(1, extend);
} else {
self.move_caret(1, extend);
}
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::ARROW_UP => {
self.move_vertical_ctx(-1, extend, ctx);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::ARROW_DOWN => {
self.move_vertical_ctx(1, extend, ctx);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::HOME => {
self.move_home(extend, ctrl, ctx);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::END => {
self.move_end(extend, ctrl, ctx);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::PAGE_UP => {
let vp = self.with_state(|s| s.viewport);
self.move_page(-1, extend, vp.height, self.line_height(), ctx);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::PAGE_DOWN => {
let vp = self.with_state(|s| s.viewport);
self.move_page(1, extend, vp.height, self.line_height(), ctx);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::ENTER => {
self.on_enter();
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::TAB => {
self.on_tab(extend);
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
Key::ESCAPE => {
let had = self.with_state(|s| {
let had = s.carets.iter().any(|c| c.anchor.is_some())
|| s.carets.len() > 1
|| !s.preedit.is_empty();
s.carets.truncate(1);
if s.carets.is_empty() {
s.carets.push(Caret::default());
}
s.carets[0].anchor = None;
s.preedit.clear();
s.goal_x = None;
Self::reset_escalate(s);
had
});
if had {
self.bump();
}
ctx.handled();
}
Key::SPACE => {
if composing {
ctx.handled();
return;
}
if extend || ctrl || alt {
if extend && !ctrl && !alt {
self.move_caret(1, true);
self.ensure_caret_visible_ctx(ctx);
}
ctx.handled();
} else if self.editable {
self.insert_str(" ");
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
} else {
ctx.handled();
}
}
other => {
if let Some(c) = other.char()
&& !c.is_control()
{
if ctrl || alt {
ctx.handled();
return;
}
if composing {
ctx.handled();
return;
}
if self.editable {
if !self.try_auto_close(c) {
let mut s = String::new();
s.push(c);
self.insert_str(&s);
}
self.ensure_caret_visible_ctx(ctx);
}
ctx.handled();
} else {
ctx.handled();
}
}
}
}
fn move_vertical_ctx(&self, dir: isize, extend: bool, ctx: &mut EventCtx) {
self.move_visual(dir, 1, extend, ctx);
self.bump();
}
fn ensure_caret_visible_ctx(&self, ctx: &mut EventCtx) {
let theme = ctx.environment.get::<Theme>().unwrap_or_else(Theme::dark);
let size = self.resolved_font_size(&theme);
let weight = self.font_weight();
let line_h = self.resolved_line_height(&theme);
let layout = self.event_layout(ctx);
let caret = self.caret();
let ri = layout.row_of(caret);
let Some(row) = layout.rows.get(ri) else {
return;
};
let col = caret
.saturating_sub(row.start)
.min(row.end - row.start);
let x = row_prefix_x(ctx, &layout, row, col, size, weight);
self.ensure_visible(x, ri, line_h);
}
fn ensure_visible(&self, caret_x: f32, line: usize, line_h: f32) {
let changed = self.with_state(|s| {
if s.viewport.is_empty() || line_h <= 0.0 {
return false;
}
let before = s.scroll;
apply_ensure_visible(s, self.pad_x, self.pad_y, caret_x, line, line_h);
if self.soft_wrap {
s.scroll.x = 0.0;
}
s.scroll != before
});
if changed {
let s = self.with_state(|s| s.scroll);
if let Some(f) = &self.on_scroll {
f(s);
}
self.bump();
}
}
fn on_ime(&self, ime: &ImeEvent) {
if self.disabled {
return;
}
if !self.editable {
if ime == &ImeEvent::Disabled {
let had = self.with_state(|s| {
let had = !s.preedit.is_empty();
s.preedit.clear();
had
});
if had {
self.bump();
}
}
return;
}
match ime {
ImeEvent::Enabled => {}
ImeEvent::Preedit { value, cursor } => {
self.with_state(|s| {
s.preedit = value.clone();
s.preedit_cursor = *cursor;
});
self.bump();
}
ImeEvent::Commit(value) => {
self.with_state(|s| {
s.preedit.clear();
s.preedit_cursor = None;
});
self.insert_str(&sanitize_multiline(value));
}
ImeEvent::Disabled => {
let had = self.with_state(|s| {
let had = !s.preedit.is_empty();
s.preedit.clear();
s.preedit_cursor = None;
had
});
if had {
self.bump();
}
}
_ => {}
}
}
fn on_clipboard_key(&self, key: &KeyEvent, ctx: &mut EventCtx) -> bool {
let cmd =
key.modifiers.contains(Modifiers::CONTROL) || key.modifiers.contains(Modifiers::SUPER);
if !cmd {
return false;
}
let Some(c) = key.key.char().map(|c| c.to_ascii_lowercase()) else {
return false;
};
if c == 'z' && !key.modifiers.contains(Modifiers::SHIFT) {
self.undo();
return true;
}
if c == 'y' || (c == 'z' && key.modifiers.contains(Modifiers::SHIFT)) {
self.redo();
return true;
}
if !matches!(c, 'c' | 'x' | 'v' | 'a') {
return false;
}
if c == 'a' {
self.escalate_select();
return true;
}
let Some(clip) = ctx.environment.get::<SharedClipboard>() else {
return true;
};
match c {
'c' => {
if let Some(sel) = self.selected_text() {
clip.0.set_text(&sel);
} else {
clip.0.set_text(&self.source.get_text());
}
}
'x' => {
if !self.editable {
return true;
}
if let Some(sel) = self.selected_text() {
clip.0.set_text(&sel);
self.delete_selection();
} else {
clip.0.set_text(&self.source.get_text());
self.source.set_text(String::new());
self.with_state(|s| {
s.carets = vec![Caret::collapsed(0)];
s.goal_x = None;
});
self.bump();
}
}
_ => {
if !self.editable {
return true;
}
if let Some(paste) = clip.0.get_text() {
self.insert_str(&sanitize_multiline(&paste));
}
}
}
true
}
fn caret_at_visual(
&self,
ctx: &mut EventCtx,
layout: &VisualLayout,
row_idx: usize,
cx: f32,
size: f32,
weight: FontWeight,
) -> usize {
let Some(row) = layout.rows.get(row_idx) else {
return 0;
};
let text = layout.row_text(row);
let col = ctx
.char_index_from_x(&text, size, weight, cx)
.min(row.end - row.start);
row.start + col
}
}
fn clamp_scroll(
scroll: Vec2,
content_w: f32,
content_h: f32,
viewport: Size,
pad: (f32, f32),
) -> Vec2 {
let max_x = (content_w + pad.0 * 2.0 - viewport.width).max(0.0);
let max_y = (content_h + pad.1 * 2.0 - viewport.height).max(0.0);
Vec2::new(
if viewport.width <= 0.0 {
scroll.x.max(0.0)
} else {
scroll.x.clamp(0.0, max_x)
},
if viewport.height <= 0.0 {
scroll.y.max(0.0)
} else {
scroll.y.clamp(0.0, max_y)
},
)
}
fn apply_ensure_visible(
s: &mut AreaState,
pad_x: f32,
pad_y: f32,
caret_x: f32,
line: usize,
line_h: f32,
) {
let vp = s.viewport;
if vp.is_empty() {
return;
}
let cx = pad_x + caret_x;
if cx < s.scroll.x + SCROLL_MARGIN {
s.scroll.x = (cx - SCROLL_MARGIN).max(0.0);
} else if cx + CARET_W + SCROLL_MARGIN > s.scroll.x + vp.width {
s.scroll.x = (cx + CARET_W + SCROLL_MARGIN - vp.width).max(0.0);
}
let top = pad_y + line as f32 * line_h;
if top < s.scroll.y {
s.scroll.y = top.max(0.0);
} else if top + line_h > s.scroll.y + vp.height {
s.scroll.y = (top + line_h - vp.height).max(0.0);
}
}
impl View for TextArea {
fn body(&self) -> impl View + 'static {
Unit
}
fn is_leaf(&self) -> bool {
true
}
fn focusable(&self) -> bool {
!self.disabled
}
fn accepts_text_input(&self) -> bool {
!self.disabled
}
fn accessibility(&self) -> Option<sequoia_view::Accessible> {
let text = self.source.get_text();
let value: String = text.chars().take(4096).collect();
Some(
sequoia_view::Accessible::new(
sequoia_view::AccessibleRole::TextArea,
if self.placeholder.is_empty() {
"pole tekstowe".to_string()
} else {
self.placeholder.clone()
},
)
.value(value)
.caret(Some(self.caret()))
.selection(self.selection())
.disabled(self.disabled),
)
}
fn ime_caret_rect(&self) -> Option<Rect> {
self.with_state(|s| s.caret_rect)
}
fn accepts_tab(&self) -> bool {
self.accept_tab && !self.disabled
}
fn has_visual_state(&self) -> bool {
true
}
fn clip_self(&self) -> bool {
true
}
fn clip_radius(&self) -> f32 {
self.radius
.or_else(|| self.appearance.and_then(|s| s.radius))
.unwrap_or(3.0)
.max(0.0)
}
fn reuse_state(&self, old: &Self) {
if !Rc::ptr_eq(&self.state, &old.state) {
*self.state.borrow_mut() = old.state.borrow().clone();
}
self.revision.set(old.revision.get_untracked());
}
fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
self.source.track();
let _ = self.revision.get();
Some(Vec::new())
}
fn cursor_icon(&self, _state: VisualState) -> CursorIcon {
if self.disabled {
CursorIcon::Default
} else {
CursorIcon::Text
}
}
fn layout_style(&self) -> LayoutStyle {
let mut s = self.style;
s.margin = self.margin;
s
}
fn intrinsic_size(&self, ctx: &mut LayoutCtx) -> Option<Size> {
let size = self
.font_size
.or_else(|| self.appearance.and_then(|s| s.font_size))
.unwrap_or(14.0);
let weight = self.font_weight();
let line_h = self.line_height();
let text = self.source.get_text();
let lines = split_lines(&text);
let mut w: f32 = 0.0;
for line in &lines {
w = w.max(ctx.text_width_weighted(line, size, weight));
}
if !self.placeholder.is_empty() && text.is_empty() {
for line in split_lines(&self.placeholder) {
w = w.max(ctx.text_width_weighted(line, size, weight));
}
}
let mut rows = lines.len().max(self.min_lines).max(1);
if let Some(max) = self.max_lines {
rows = rows.min(max.max(1));
}
let rows = rows as f32;
Some(Size::new(
w + self.pad_x * 2.0,
rows * line_h + self.pad_y * 2.0,
))
}
fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
if self.disabled {
return;
}
if ctx.phase != EventPhase::Target {
if let Event::Pointer(PointerEvent::Wheel {
delta, modifiers, ..
}) = event
&& matches!(ctx.phase, EventPhase::Bubble)
{
self.on_wheel(*delta, *modifiers, ctx);
}
return;
}
match event {
Event::Pointer(PointerEvent::Down {
button: PointerButton::Left,
pos,
modifiers,
..
}) => {
self.begin_session();
let theme = ctx.environment.get::<Theme>().unwrap_or_else(Theme::dark);
let size = self.resolved_font_size(&theme);
let weight = self.font_weight();
let line_h = self.resolved_line_height(&theme);
let layout = self.event_layout(ctx);
let scroll = self.with_state(|s| s.scroll);
let cx = pos.x - ctx.rect.left() - self.pad_x + scroll.x;
let cy = pos.y - ctx.rect.top() - self.pad_y + scroll.y;
let ri = Self::row_at_y(line_h, layout.rows.len(), cy);
let caret = self.caret_at_visual(ctx, &layout, ri, cx, size, weight);
let extend = modifiers.contains(Modifiers::SHIFT);
let alt = modifiers.contains(Modifiers::ALT);
self.handle_click(caret, extend, alt, now_ms());
self.ensure_caret_visible_ctx(ctx);
self.bump();
ctx.request_focus();
ctx.capture_pointer();
ctx.handled();
}
Event::Pointer(PointerEvent::Move { pos, .. }) => {
if ctx.state.pressed {
let theme = ctx.environment.get::<Theme>().unwrap_or_else(Theme::dark);
let size = self.resolved_font_size(&theme);
let weight = self.font_weight();
let line_h = self.resolved_line_height(&theme);
let layout = self.event_layout(ctx);
let scroll = self.with_state(|s| s.scroll);
let cx = pos.x - ctx.rect.left() - self.pad_x + scroll.x;
let cy = pos.y - ctx.rect.top() - self.pad_y + scroll.y;
let ri = Self::row_at_y(line_h, layout.rows.len(), cy);
let caret = self.caret_at_visual(ctx, &layout, ri, cx, size, weight);
let alt_drag = self.with_state(|s| s.alt_drag);
match alt_drag {
Some(idx) => self.with_state(|s| {
if let Some(c) = s.carets.get_mut(idx) {
c.move_to(caret, true);
}
s.goal_x = None;
}),
None => self.set_caret_extend(caret, true),
}
self.ensure_caret_visible_ctx(ctx);
self.bump();
ctx.handled();
}
}
Event::Pointer(PointerEvent::Wheel {
delta, modifiers, ..
}) => {
self.on_wheel(*delta, *modifiers, ctx);
}
Event::Key(key) => {
if self.on_style_shortcut(key) {
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
} else if !self.on_clipboard_key(key, ctx) {
self.on_key(key, ctx);
} else {
self.ensure_caret_visible_ctx(ctx);
ctx.handled();
}
}
Event::Ime(ime) => {
self.on_ime(ime);
ctx.handled();
}
_ => {}
}
}
fn paint_self(&self, ctx: &mut PaintCtx) {
self.note_focus(ctx.state.focused);
let rect = ctx.rect;
if rect.size.width <= 0.0 || rect.size.height <= 0.0 {
return;
}
let theme = ctx.theme();
let size = self.resolved_font_size(&theme);
let weight = self.font_weight();
let line_h = self.resolved_line_height(&theme);
if line_h <= 0.0 {
return;
}
let text_color = if self.disabled {
theme.disabled
} else {
self.resolved_text_color(&theme)
};
let placeholder_color = if self.disabled {
theme.disabled
} else {
self.resolved_placeholder_color(&theme)
};
let background = self.resolved_background(&theme);
let radius = self.resolved_radius(&theme);
self.with_state(|s| s.viewport = rect.size);
let width = (rect.size.width - self.pad_x * 2.0).max(0.0);
let layout = self.layout_for(ctx, width, size, weight);
let total_chars = layout
.line_starts
.last()
.copied()
.unwrap_or(0)
+ layout.lines.last().map(|l| l.chars().count()).unwrap_or(0);
let rows = layout.rows.len();
let content_w = if self.soft_wrap {
width
} else {
layout
.lines
.iter()
.map(|line| ctx.text_width_weighted(line, size, weight))
.fold(0.0f32, f32::max)
};
let content_h = rows as f32 * line_h;
let (caret, anchor, preedit, preedit_cursor) = self.with_state(|s| {
let main = Self::main_of(s);
(
main.offset,
main.anchor,
s.preedit.clone(),
s.preedit_cursor,
)
});
let caret = caret.min(total_chars);
let selection = anchor.map(|a| (a.min(caret), a.max(caret)));
let composing = !preedit.is_empty();
let comp_start = selection.map(|(a, _)| a).unwrap_or(caret);
let comp_end = selection.map(|(_, b)| b).unwrap_or(caret);
let caret_row = layout.row_of(comp_start);
let caret_span = layout
.rows
.get(caret_row)
.copied()
.unwrap_or(RowSpan {
line: 0,
start: 0,
end: 0,
});
let caret_col = comp_start
.saturating_sub(caret_span.start)
.min(caret_span.end - caret_span.start);
let base_x = row_prefix_x(ctx, &layout, &caret_span, caret_col, size, weight);
let caret_x = if composing {
let inside = match preedit_cursor {
Some((cur, _)) => {
let mut i = cur.min(preedit.len());
while i > 0 && !preedit.is_char_boundary(i) {
i -= 1;
}
ctx.text_width_weighted(&preedit[..i], size, weight)
}
None => ctx.text_width_weighted(&preedit, size, weight),
};
base_x + inside
} else {
base_x
};
self.with_state(|s| {
s.content = Size::new(content_w, content_h);
s.scroll = clamp_scroll(
s.scroll,
content_w,
content_h,
rect.size,
(self.pad_x, self.pad_y),
);
apply_ensure_visible(s, self.pad_x, self.pad_y, caret_x, caret_row, line_h);
if self.soft_wrap {
s.scroll.x = 0.0;
}
s.scroll = clamp_scroll(
s.scroll,
content_w,
content_h,
rect.size,
(self.pad_x, self.pad_y),
);
});
let scroll = self.with_state(|s| s.scroll);
let border = if self.error {
Some((1.5, sequoia_style::FORM_ERROR))
} else {
self.resolved_border(&theme)
};
if background.a > 0.0 {
match border {
Some((w, color)) => {
ctx.bordered_quad(rect, background, radius, w, color);
}
None => {
ctx.quad(rect, background, radius);
}
}
}
let ox = rect.left() + self.pad_x - scroll.x;
let oy = rect.top() + self.pad_y - scroll.y;
let baseline_at =
|row: usize| -> f32 { oy + row as f32 * line_h + line_h * 0.5 + size * 0.35 };
let top_at = |row: usize| -> f32 { oy + row as f32 * line_h };
let first_row = ((scroll.y - self.pad_y) / line_h).floor().max(0.0) as usize;
let last_row = (((scroll.y - self.pad_y + rect.size.height) / line_h)
.ceil()
.max(0.0) as usize)
.min(rows.saturating_sub(1));
if ctx.state.focused && self.highlight_current_line && anchor.is_none() {
let color = self.current_line.unwrap_or(Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 0.05,
});
if color.a > 0.0 && caret_row >= first_row && caret_row <= last_row {
ctx.quad(
Rect::from_xywh(rect.left(), top_at(caret_row), rect.size.width, line_h),
color,
0.0,
);
}
}
if let Some(a) = anchor.filter(|_| !composing) {
let (s0, s1) = (a.min(caret).min(total_chars), a.max(caret).min(total_chars));
if s1 > s0 {
for (ri, row) in layout.rows.iter().enumerate() {
if ri < first_row || ri > last_row {
continue;
}
if s1 <= row.start || s0 > row.end {
continue;
}
let row_chars = row.end - row.start;
let col0 = s0.saturating_sub(row.start).min(row_chars);
let x0 = row_prefix_x(ctx, &layout, row, col0, size, weight);
let x1 = if s1 > row.end {
width.max(x0 + 4.0)
} else {
let col1 = s1.saturating_sub(row.start).min(row_chars);
row_prefix_x(ctx, &layout, row, col1, size, weight)
};
if x1 > x0 {
ctx.quad(
Rect::from_xywh(ox + x0, top_at(ri), x1 - x0, line_h),
theme.accent_pressed,
0.0,
);
}
}
}
}
if layout.text.is_empty() && preedit.is_empty() && !self.placeholder.is_empty() {
for (row, line) in split_lines(&self.placeholder).iter().enumerate() {
if row < first_row || row > last_row {
continue;
}
ctx.draw_text_weighted(
line,
(ox, baseline_at(row)),
size,
weight,
placeholder_color,
);
}
} else {
for ri in first_row..=last_row.min(rows.saturating_sub(1)) {
let Some(row) = layout.rows.get(ri) else {
continue;
};
let line = layout.row_text(row);
if line.is_empty() {
continue;
}
if ri == caret_row && composing && ctx.state.focused {
let line_chars = line.chars().count();
let end_col = comp_end.saturating_sub(row.start).min(line_chars);
let before: String = line.chars().take(caret_col.min(line_chars)).collect();
let after: String = line.chars().skip(end_col).collect();
let bx = ctx.text_width_weighted(&before, size, weight);
ctx.draw_text_weighted(
&before,
(ox, baseline_at(ri)),
size,
weight,
text_color,
);
ctx.draw_text_weighted(
&preedit,
(ox + bx, baseline_at(ri)),
size,
weight,
text_color,
);
let pw = ctx.text_width_weighted(&preedit, size, weight);
ctx.draw_text_weighted(
&after,
(ox + bx + pw, baseline_at(ri)),
size,
weight,
text_color,
);
ctx.quad(
Rect::from_xywh(ox + bx, top_at(ri) + line_h - 2.0, pw.max(1.0), 1.5),
text_color,
0.0,
);
} else {
ctx.draw_text_weighted(
&line,
(ox, baseline_at(ri)),
size,
weight,
text_color,
);
}
}
}
let caret_rect = ctx
.state
.focused
.then(|| {
Rect::from_xywh(
ox + caret_x,
baseline_at(caret_row) - size * 0.8,
CARET_W,
size * 1.05,
)
})
.filter(|_| !self.disabled);
self.with_state(|s| s.caret_rect = caret_rect);
if let Some(r) = caret_rect {
ctx.quad(r, text_color, 0.0);
}
}
}
impl TextArea {
fn on_wheel(&self, delta: Vec2, modifiers: Modifiers, ctx: &mut EventCtx) {
let (mut dx, mut dy) = (delta.x, delta.y);
if modifiers.contains(Modifiers::SHIFT) {
std::mem::swap(&mut dx, &mut dy);
}
let (cur, viewport, content) = self.with_state(|s| (s.scroll, s.viewport, s.content));
let next = clamp_scroll(
Vec2::new(cur.x - dx, cur.y - dy),
content.width,
content.height,
viewport,
self.pad(),
);
if next != cur {
self.with_state(|s| s.scroll = next);
if let Some(f) = &self.on_scroll {
f(next);
}
self.bump();
ctx.handled();
}
}
}
fn sanitize_margin(v: f32) -> f32 {
if v.is_finite() { v.max(0.0) } else { 0.0 }
}
#[cfg(test)]
mod tests {
use super::*;
fn chars_of(s: &str) -> Vec<char> {
s.chars().collect()
}
#[test]
fn line_starts_cover_empty_and_trailing_newline() {
assert_eq!(line_starts(&chars_of("")), vec![0]);
assert_eq!(line_starts(&chars_of("ab")), vec![0]);
assert_eq!(line_starts(&chars_of("a\nb")), vec![0, 2]);
assert_eq!(line_starts(&chars_of("a\n")), vec![0, 2]);
assert_eq!(line_starts(&chars_of("\n")), vec![0, 1]);
}
#[test]
fn caret_roundtrips_through_line_col() {
let text = "ab\ncde\n\nf";
let chars = chars_of(text);
let starts = line_starts(&chars);
assert_eq!(starts.len(), 4);
for caret in 0..=chars.len() {
let (line, col) = line_col_of(&chars, &starts, caret);
assert_eq!(caret_of_line_col(&chars, &starts, line, col), caret);
}
assert_eq!(line_col_of(&chars, &starts, 0), (0, 0));
assert_eq!(line_col_of(&chars, &starts, 2), (0, 2));
assert_eq!(line_col_of(&chars, &starts, 3), (1, 0));
assert_eq!(line_col_of(&chars, &starts, 7), (2, 0));
assert_eq!(line_col_of(&chars, &starts, 8), (3, 0));
}
#[test]
fn sanitize_multiline_normalizes_newlines() {
assert_eq!(sanitize_multiline("a\r\nb\rc"), "a\nb\nc");
assert_eq!(sanitize_multiline("a\x01b\tc"), "ab\tc");
}
#[test]
fn leading_indent_counts_spaces_and_tabs() {
assert_eq!(leading_indent_len(" foo"), 4);
assert_eq!(leading_indent_len("\t\tfoo"), 2);
assert_eq!(leading_indent_len("foo"), 0);
}
}