-use std::ops::Range;
+use enum_iterator::Sequence;
+use std::{borrow::Cow, fmt::Debug, ops::Range};
-use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
+use unicode_width::UnicodeWidthChar;
/// A line of text, encoded in UTF-8, with support functions that properly
/// handle double-width characters and backspaces.
}
pub fn put(&mut self, x0: usize, s: &str) {
- let w = s.width();
+ let w = Widths::new(s).sum::<usize>();
+ dbg!(w);
let x1 = x0 + w;
if w == 0 {
// Nothing to do.
self.string.push_str(s);
self.width = x1;
} else {
+ dbg!(x0);
+ dbg!(x1);
let span = self.find_span(x0, x1);
+ dbg!(&span);
if span.columns.start < x0 || span.columns.end > x1 {
let tail = self.string.split_off(span.offsets.end);
self.string.truncate(span.offsets.start);
fn find_span(&self, x0: usize, x1: usize) -> Position {
let p0 = self.find_pos(x0);
let p1 = self.find_pos(x1);
- if p0.columns.start >= x1 {
+ if true || p0.columns.start >= x1 {
Position {
columns: p0.columns.start..p1.columns.start,
offsets: p0.offsets.start..p1.offsets.start,
}
}
+ // Returns the [Position] that contains column `target_x`.
fn find_pos(&self, target_x: usize) -> Position {
let mut x = 0;
let mut ofs = 0;
offsets: ofs..ofs,
}
}
+
+ fn str(&self) -> &str {
+ &self.string
+ }
}
/// Position of one or more characters within a [TextLine].
+#[derive(Debug)]
struct Position {
/// 0-based display columns.
columns: Range<usize>,
offsets: Range<usize>,
}
+/// Iterates through the column widths in a string.
struct Widths<'a> {
s: &'a str,
base: &'a str,
Self { s, base: s }
}
+ /// Returns the amount of the string remaining to be visited.
fn as_str(&self) -> &str {
self.s
}
+ // Returns the offset into the original string of the characters remaining
+ // to be visited.
fn offset(&self) -> usize {
self.base.len() - self.s.len()
}
fn next(&mut self) -> Option<Self::Item> {
let mut iter = self.s.char_indices();
let (_, mut c) = iter.next()?;
- if iter.as_str().starts_with('\x08') {
+ while iter.as_str().starts_with('\x08') {
iter.next();
c = match iter.next() {
Some((_, c)) => c,
Some(w)
}
}
+
+#[derive(Copy, Clone, PartialEq, Eq, Sequence)]
+struct Emphasis {
+ pub bold: bool,
+ pub underline: bool,
+}
+
+impl Emphasis {
+ const fn plain() -> Self {
+ Self {
+ bold: false,
+ underline: false,
+ }
+ }
+ fn is_plain(&self) -> bool {
+ *self == Self::plain()
+ }
+ fn apply<'a>(&self, s: &'a str) -> Cow<'a, str> {
+ if self.is_plain() {
+ Cow::from(s)
+ } else {
+ let mut output = String::with_capacity(
+ s.len() * (1 + self.bold as usize * 2 + self.underline as usize * 2),
+ );
+ for c in s.chars() {
+ if self.bold {
+ output.push(c);
+ output.push('\x08');
+ }
+ if self.underline {
+ output.push('_');
+ output.push('\x08');
+ }
+ output.push(c);
+ }
+ Cow::from(output)
+ }
+ }
+}
+
+impl Debug for Emphasis {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ Self {
+ bold: false,
+ underline: false,
+ } => "plain",
+ Self {
+ bold: true,
+ underline: false,
+ } => "bold",
+ Self {
+ bold: false,
+ underline: true,
+ } => "underline",
+ Self {
+ bold: true,
+ underline: true,
+ } => "bold+underline",
+ }
+ )
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::{Emphasis, TextLine};
+ use enum_iterator::all;
+
+ #[test]
+ fn overwrite_rest_of_line() {
+ for lowercase in all::<Emphasis>() {
+ for uppercase in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ line.put(0, &lowercase.apply("abc"));
+ line.put(1, &uppercase.apply("BCD"));
+ assert_eq!(
+ line.str(),
+ &format!("{}{}", lowercase.apply("a"), uppercase.apply("BCD")),
+ "uppercase={uppercase:?} lowercase={lowercase:?}"
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn overwrite_partial_line() {
+ for lowercase in all::<Emphasis>() {
+ for uppercase in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `AbCDEf`.
+ line.put(0, &lowercase.apply("abcdef"));
+ line.put(0, &uppercase.apply("A"));
+ line.put(2, &uppercase.apply("CDE"));
+ assert_eq!(
+ line.str().replace('\x08', "#"),
+ format!(
+ "{}{}{}{}",
+ uppercase.apply("A"),
+ lowercase.apply("b"),
+ uppercase.apply("CDE"),
+ lowercase.apply("f")
+ )
+ .replace('\x08', "#"),
+ "uppercase={uppercase:?} lowercase={lowercase:?}"
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn overwrite_rest_with_double_width() {
+ for lowercase in all::<Emphasis>() {
+ for hiragana in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `kaきくけ"`.
+ line.put(0, &lowercase.apply("kakiku"));
+ line.put(2, &hiragana.apply("きくけ"));
+ assert_eq!(
+ line.str(),
+ &format!("{}{}", lowercase.apply("ka"), hiragana.apply("きくけ")),
+ "lowercase={lowercase:?} hiragana={hiragana:?}"
+ );
+ }
+ }
+ }
+
+ #[test]
+ fn overwrite_partial_with_double_width() {
+ for lowercase in all::<Emphasis>() {
+ for hiragana in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `かkiくけko".
+ line.put(0, &lowercase.apply("kakikukeko"));
+ line.put(0, &hiragana.apply("か"));
+ line.put(4, &hiragana.apply("くけ"));
+ assert_eq!(
+ line.str(),
+ &format!(
+ "{}{}{}{}",
+ hiragana.apply("か"),
+ lowercase.apply("ki"),
+ hiragana.apply("くけ"),
+ lowercase.apply("ko")
+ ),
+ "lowercase={lowercase:?} hiragana={hiragana:?}"
+ );
+ }
+ }
+ }
+
+ /// Overwrite rest of line, aligned double-width over double-width
+ #[test]
+ fn aligned_double_width_rest_of_line() {
+ for bottom in all::<Emphasis>() {
+ for top in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `あきくけ`.
+ line.put(0, &bottom.apply("あいう"));
+ line.put(2, &top.apply("きくけ"));
+ assert_eq!(
+ line.str(),
+ &format!("{}{}", bottom.apply("あ"), top.apply("きくけ")),
+ "bottom={bottom:?} top={top:?}"
+ );
+ }
+ }
+ }
+
+ /// Overwrite rest of line, misaligned double-width over double-width
+ #[test]
+ fn misaligned_double_width_rest_of_line() {
+ for bottom in all::<Emphasis>() {
+ for top in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `あきくけ`.
+ line.put(0, &bottom.apply("あいう"));
+ line.put(3, &top.apply("きくけ"));
+ assert_eq!(
+ line.str(),
+ &format!("{}?{}", bottom.apply("あ"), top.apply("きくけ")),
+ "bottom={bottom:?} top={top:?}"
+ );
+ }
+ }
+ }
+
+ /// Overwrite partial line, aligned double-width over double-width
+ #[test]
+ fn aligned_double_width_partial() {
+ for bottom in all::<Emphasis>() {
+ for top in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `かいくけお`.
+ line.put(0, &bottom.apply("あいうえお"));
+ line.put(0, &top.apply("か"));
+ line.put(4, &top.apply("くけ"));
+ assert_eq!(
+ line.str(),
+ &format!(
+ "{}{}{}{}",
+ top.apply("か"),
+ bottom.apply("い"),
+ top.apply("くけ"),
+ bottom.apply("お")
+ ),
+ "bottom={bottom:?} top={top:?}"
+ );
+ }
+ }
+ }
+
+ /// Overwrite partial line, misaligned double-width over double-width
+ #[test]
+ fn misaligned_double_width_partial() {
+ for bottom in all::<Emphasis>() {
+ for top in all::<Emphasis>() {
+ let mut line = TextLine::new();
+ // Produces `?か??くけ?さ`.
+ line.put(0, &bottom.apply("あいうえお"));
+ line.put(1, &top.apply("か"));
+ //line.put(5, &top.apply("くけ"));
+ assert_eq!(
+ line.str(),
+ &format!(
+ "?{}??{}{}",
+ top.apply("か"),
+ top.apply("くけ"),
+ bottom.apply("さ")
+ ),
+ "bottom={bottom:?} top={top:?}"
+ );
+ }
+ }
+ }
+}