use crate::color::Color;
use crate::widgets::text_area::TextHAlign;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct InlineStyle {
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
pub font_family: Option<String>,
pub font_size: Option<f64>,
pub text_color: Option<Color>,
pub highlight: Option<Color>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TextRun {
pub text: String,
pub style: InlineStyle,
}
impl TextRun {
pub fn new(text: impl Into<String>, style: InlineStyle) -> Self {
Self {
text: text.into(),
style,
}
}
pub fn plain(text: impl Into<String>) -> Self {
Self::new(text, InlineStyle::default())
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ListKind {
#[default]
None,
Bullet,
Ordered,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Block {
pub runs: Vec<TextRun>,
pub align: TextHAlign,
pub list: ListKind,
pub indent: u8,
}
impl Default for Block {
fn default() -> Self {
Self::new()
}
}
impl Block {
pub fn new() -> Self {
Self {
runs: Vec::new(),
align: TextHAlign::Left,
list: ListKind::None,
indent: 0,
}
}
pub fn from_run(run: TextRun) -> Self {
Self {
runs: vec![run],
..Self::new()
}
}
pub fn plain(text: impl Into<String>) -> Self {
let text = text.into();
if text.is_empty() {
Self::new()
} else {
Self::from_run(TextRun::plain(text))
}
}
pub fn text_len(&self) -> usize {
self.runs.iter().map(|r| r.text.len()).sum()
}
pub fn text(&self) -> String {
self.runs.iter().map(|r| r.text.as_str()).collect()
}
pub fn ensure_boundary(&mut self, byte: usize) -> usize {
let mut acc = 0usize;
let mut i = 0usize;
while i < self.runs.len() {
if byte == acc {
return i;
}
let len = self.runs[i].text.len();
if byte < acc + len {
let split_at = byte - acc;
debug_assert!(
self.runs[i].text.is_char_boundary(split_at),
"DocPos byte offset must land on a UTF-8 char boundary"
);
let tail = self.runs[i].text.split_off(split_at);
let style = self.runs[i].style.clone();
self.runs.insert(i + 1, TextRun { text: tail, style });
return i + 1;
}
acc += len;
i += 1;
}
self.runs.len()
}
pub fn normalize(&mut self) {
self.runs.retain(|r| !r.text.is_empty());
let mut i = 0;
while i + 1 < self.runs.len() {
if self.runs[i].style == self.runs[i + 1].style {
let next = self.runs.remove(i + 1);
self.runs[i].text.push_str(&next.text);
} else {
i += 1;
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RichDoc {
pub blocks: Vec<Block>,
}
impl Default for RichDoc {
fn default() -> Self {
Self::new()
}
}
impl RichDoc {
pub fn new() -> Self {
Self {
blocks: vec![Block::new()],
}
}
pub fn from_blocks(blocks: Vec<Block>) -> Self {
if blocks.is_empty() {
Self::new()
} else {
Self { blocks }
}
}
pub fn end_pos(&self) -> DocPos {
let block = self.blocks.len().saturating_sub(1);
DocPos {
block,
byte: self.blocks.get(block).map(Block::text_len).unwrap_or(0),
}
}
pub fn plain_text(&self) -> String {
self.blocks
.iter()
.map(Block::text)
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct DocPos {
pub block: usize,
pub byte: usize,
}
impl DocPos {
pub fn new(block: usize, byte: usize) -> Self {
Self { block, byte }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DocRange {
pub start: DocPos,
pub end: DocPos,
}
impl DocRange {
pub fn new(start: DocPos, end: DocPos) -> Self {
Self { start, end }
}
pub fn collapsed(pos: DocPos) -> Self {
Self {
start: pos,
end: pos,
}
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
pub fn ordered(&self) -> (DocPos, DocPos) {
if self.start <= self.end {
(self.start, self.end)
} else {
(self.end, self.start)
}
}
pub fn min(&self) -> DocPos {
self.ordered().0
}
pub fn max(&self) -> DocPos {
self.ordered().1
}
}
pub fn insert_text(doc: &mut RichDoc, pos: DocPos, text: &str, style: InlineStyle) {
if text.is_empty() {
return;
}
let Some(block) = doc.blocks.get_mut(pos.block) else {
return;
};
let idx = block.ensure_boundary(pos.byte);
block.runs.insert(
idx,
TextRun {
text: text.to_string(),
style,
},
);
block.normalize();
}
pub fn remove_range(doc: &mut RichDoc, range: DocRange) -> DocPos {
let (a, b) = range.ordered();
if a == b {
return a;
}
let b = DocPos {
block: b.block.min(doc.blocks.len().saturating_sub(1)),
byte: b.byte,
};
if a.block == b.block {
if let Some(block) = doc.blocks.get_mut(a.block) {
let start = block.ensure_boundary(a.byte);
let end = block.ensure_boundary(b.byte);
block.runs.drain(start..end);
block.normalize();
}
return a;
}
if let Some(block) = doc.blocks.get_mut(a.block) {
let start = block.ensure_boundary(a.byte);
block.runs.truncate(start);
}
let tail_runs = if let Some(block) = doc.blocks.get_mut(b.block) {
let end = block.ensure_boundary(b.byte);
block.runs.drain(0..end);
std::mem::take(&mut block.runs)
} else {
Vec::new()
};
if let Some(block) = doc.blocks.get_mut(a.block) {
block.runs.extend(tail_runs);
block.normalize();
}
doc.blocks.drain(a.block + 1..=b.block);
a
}
pub fn split_block(doc: &mut RichDoc, pos: DocPos) -> DocPos {
let Some(block) = doc.blocks.get_mut(pos.block) else {
return pos;
};
let idx = block.ensure_boundary(pos.byte);
let tail_runs = block.runs.split_off(idx);
let (align, list, indent) = (block.align, block.list, block.indent);
block.normalize();
let mut new_block = Block {
runs: tail_runs,
align,
list,
indent,
};
new_block.normalize();
doc.blocks.insert(pos.block + 1, new_block);
DocPos {
block: pos.block + 1,
byte: 0,
}
}
pub fn merge_block_with_prev(doc: &mut RichDoc, idx: usize) -> DocPos {
if idx == 0 || idx >= doc.blocks.len() {
return DocPos { block: idx, byte: 0 };
}
let join_byte = doc.blocks[idx - 1].text_len();
let block = doc.blocks.remove(idx);
doc.blocks[idx - 1].runs.extend(block.runs);
doc.blocks[idx - 1].normalize();
DocPos {
block: idx - 1,
byte: join_byte,
}
}
pub fn extract_range(doc: &RichDoc, range: DocRange) -> Vec<Block> {
let (a, b) = range.ordered();
if a == b {
return Vec::new();
}
let b_block = b.block.min(doc.blocks.len().saturating_sub(1));
let mut out = Vec::new();
for bi in a.block..=b_block {
let Some(src) = doc.blocks.get(bi) else {
continue;
};
let mut block = src.clone();
let hi = if bi == b_block { b.byte } else { block.text_len() };
let lo = if bi == a.block { a.byte } else { 0 };
let end = block.ensure_boundary(hi);
block.runs.truncate(end);
let start = block.ensure_boundary(lo);
block.runs.drain(0..start);
block.normalize();
out.push(block);
}
out
}
pub fn splice_fragment(doc: &mut RichDoc, pos: DocPos, fragment: &[Block]) -> DocPos {
if fragment.is_empty() {
return pos;
}
let Some(target) = doc.blocks.get_mut(pos.block) else {
return pos;
};
if fragment.len() == 1 {
let default = Block::new();
let adopt_attrs = target.runs.is_empty()
&& target.align == default.align
&& target.list == default.list
&& target.indent == default.indent;
let mut idx = target.ensure_boundary(pos.byte);
let mut added = 0usize;
for run in &fragment[0].runs {
target.runs.insert(idx, run.clone());
idx += 1;
added += run.text.len();
}
target.normalize();
if adopt_attrs {
target.align = fragment[0].align;
target.list = fragment[0].list;
target.indent = fragment[0].indent;
}
return DocPos::new(pos.block, pos.byte + added);
}
let split_idx = target.ensure_boundary(pos.byte);
let tail_runs = target.runs.split_off(split_idx);
let (t_align, t_list, t_indent) = (target.align, target.list, target.indent);
target.runs.extend(fragment[0].runs.iter().cloned());
target.normalize();
let last = &fragment[fragment.len() - 1];
let end_byte = last.text_len();
let mut new_blocks: Vec<Block> = fragment[1..fragment.len() - 1].to_vec();
let mut last_block = Block {
runs: last.runs.clone(),
align: t_align,
list: t_list,
indent: t_indent,
};
last_block.runs.extend(tail_runs);
last_block.normalize();
new_blocks.push(last_block);
let count = new_blocks.len(); for (k, block) in new_blocks.into_iter().enumerate() {
doc.blocks.insert(pos.block + 1 + k, block);
}
DocPos::new(pos.block + count, end_byte)
}
#[cfg(test)]
#[path = "model_tests.rs"]
mod model_tests;