Repository: osa1/tiny (language: rust)

Issue:
Long, spaceless messages get cut off
Long messages that don't have enough spaces to line wrap get truncated.

I believe this is due to the `draw()` in `line.rs` and how it handles when there is no whitespace to split on.
```
                    // Not possible to split. Need to make sure we don't render out
                    // of bounds.
                    if col - pos_x < width {
                        if line >= first_line {
                            tb.change_cell(col, pos_y + line, char, sty.fg, sty.bg);
                        }
                        col += 1;
                    }
```

This isn't a huge problem. Personally, I would just resize the window and hope that the message/link can all fit, but it's probably a pain for people using a tiling window manager...

Wide view:
![image](https://user-images.githubusercontent.com/5448324/83335246-26622280-a2b4-11ea-9a34-e0f347a91436.png)
Compact view:
![image](https://user-images.githubusercontent.com/5448324/83335256-337f1180-a2b4-11ea-87d7-71c298c95373.png)

Tested on master


Discussion / hints:
Thanks for reporting. I thought I handled this, but apparently not. I think this is pretty bad as it can be misleading. For example, if someone pasted a long link and the window width is small, the link will cut off and when you copy you'll only copy the prefix.

We should split the line at the last visible character when this happens.

Fix patch:
diff --git a/libtiny_tui/src/input_area/input_line.rs b/libtiny_tui/src/input_area/input_line.rs
index afa4e157..78f515a9 100644
--- a/libtiny_tui/src/input_area/input_line.rs
+++ b/libtiny_tui/src/input_area/input_line.rs
@@ -1,153 +1,7 @@
-use crate::{config::Colors, termbox, utils};
+use crate::{config::Colors, line_split::LineDataCache, termbox, utils};
 use std::{cmp::min, ops::RangeBounds, vec::Drain};
 use termbox_simple::Termbox;
 
-/// Cache that stores the state of InputLine's height calculation.
-/// `line_count` is used as the dirty bit to invalidate the cache.
-#[derive(Clone, Debug)]
-struct LineDataCache {
-    /// Indices to split on when we draw, not always whitespaces
-    split_indices: Vec<i32>,
-    /// The total number of lines (height) that will be rendered
-    line_count: Option<i32>,
-    /// The current width of InputArea. Used in determining if we need to invalidate due to resize.
-    width: i32,
-    /// The current width of the InputLine (may be shorter due to nickname)
-    line_width: i32,
-    /// Current nickname length. Used in determining if we need to invalidate due to resize.
-    nick_length: usize,
-    /// The index into InputLine::buffer of the last whitespace that we saw in calculate_height()
-    last_whitespace_idx: Option<i32>,
-    /// True if the last character was a whitespace character
-    prev_char_is_whitespace: bool,
-    /// The length of the current line that is being added to.
-    /// Used to determine when to wrap to the next line in calculate_height()
-    current_line_length: i32,
-}
-
-impl LineDataCache {
-    fn new() -> LineDataCache {
-        LineDataCache {
-            split_indices: Vec::new(),
-            line_count: None,
-            width: 0,
-            line_width: 0,
-            nick_length: 0,
-            last_whitespace_idx: None,
-            prev_char_is_whitespace: false,
-            current_line_length: 0,
-        }
-    }
-
-    /// Performs a check to see if the width or nickname length changed
-    /// which would require an invalidation of the cache and recalculation of
-    /// the InputLine height.
-    fn needs_resize(&self, width: i32, nick_length: usize) -> bool {
-        self.width != width || self.nick_length != nick_length
-    }
-
-    /// Sets `line_count` to `None`, which invalidates the cache.
-    fn set_dirty(&mut self) {
-        self.line_count = None;
-    }
-
-    /// Checks if the cache is invalidated by seeing if
-    /// `line_count` is `None`.
-    fn is_dirty(&self) -> bool {
-        self.line_count.is_none()
-    }
-
-    /// Resets the cache to a default state that requires
-    /// a height calculation.
-    fn reset(&mut self, width: i32, nick_length: usize) {
-        self.split_indices.clear();
-        self.line_count = None;
-        self.width = width;
-        self.nick_length = nick_length;
-        self.line_width = width - nick_length as i32;
-        self.last_whitespace_idx = None;
-        self.prev_char_is_whitespace = false;
-        self.current_line_length = 0;
-    }
-
-    fn get_line_count(&self) -> Option<usize> {
-        self.line_count.map(|c| c as usize)
-    }
-
-    /// Function that calculates the height of the `InputLine`.
-    /// and sets `split_indices` for drawing.
-    /// An `offset` allows for resuming the calculation - see InputLine::insert().
-    /// `offset` must be less than or equal to the current buffer size.
-    ///
-    /// Scans through the buffer in one pass to determine how many lines
-    /// will be needed to render the text with word wrapping.
-    /// If an offset is provided, it will continue the calculation
-    /// from the saved state and save the new line count in `line_count`.
-    fn calculate_height(&mut self, buffer: &[char], offset: usize) {
-        debug_assert!(offset <= buffer.len());
-        let mut temp_count = 1;
-        if let Some(line_count) = self.line_count {
-            temp_count = line_count;
-            // If we made space for the cursor, subtract it.
-            if self.current_line_length == self.line_width {
-                temp_count -= 1;
-            }
-        }
-        for (c, current_idx) in buffer.iter().skip(offset).zip(offset..) {
-            let current_idx = current_idx as i32;
-            self.current_line_length += 1;
-
-            if c.is_whitespace() {
-                // Splitting
-                if self.current_line_length > self.line_width {
-                    // we're on a whitespace so just go to next line
-                    temp_count += 1;
-                    // this character will be the first one on the next line
-                    self.current_line_length = 1;
-                    // nick is shown on the first line, set width to full width in the consecutive
-                    // lines
-                    self.line_width = self.width;
-                    // store index for drawing
-                    self.split_indices.push(current_idx);
-                }
-                // store whitespace for splitting
-                self.last_whitespace_idx = Some(current_idx);
-                self.prev_char_is_whitespace = true;
-            } else {
-                // Splitting
-                if self.current_line_length > self.line_width {
-                    // if the previous character was a whitespace, then we have a clean split
-                    if !self.prev_char_is_whitespace && self.last_whitespace_idx.is_some() {
-                        // move back to the last whitespace and get the length of the input that
-                        // will be on the next line
-                        self.current_line_length = current_idx - self.last_whitespace_idx.unwrap();
-                        // store index for drawing
-                        self.split_indices
-                            .push(self.last_whitespace_idx.unwrap() + 1);
-                    } else {
-                        // unclean split on non-whitespace
-                        self.current_line_length = 1;
-                        // store index for drawing
-                        self.split_indices.push(current_idx);
-                    }
-                    // invalidate whitespace since we split here
-                    self.last_whitespace_idx = None;
-                    // moved to next line
-                    temp_count += 1;
-                    // set width to full width
-                    self.line_width = self.width;
-                }
-                self.prev_char_is_whitespace = false;
-            }
-        }
-
-        // Last line length is `line_width`, make room for cursor
-        if self.current_line_length == self.line_width {
-            temp_count += 1;
-        }
-        self.line_count = Some(temp_count);
-    }
-}
 #[derive(Clone, Debug)]
 pub(crate) struct InputLine {
     /// Input buffer
@@ -163,7 +17,7 @@ impl InputLine {
     pub(crate) fn new() -> InputLine {
         InputLine {
             buffer: Vec::with_capacity(512),
-            line_data: LineDataCache::new(),
+            line_data: LineDataCache::new(true),
         }
     }
 
@@ -171,7 +25,7 @@ impl InputLine {
     pub(crate) fn from_buffer(buffer: Vec<char>) -> InputLine {
         InputLine {
             buffer,
-            line_data: LineDataCache::new(),
+            line_data: LineDataCache::new(true),
         }
     }
 
@@ -216,7 +70,8 @@ impl InputLine {
     pub(crate) fn insert(&mut self, idx: usize, element: char) {
         self.buffer.insert(idx, element);
         if idx == self.buffer.len() - 1 {
-            self.line_data.calculate_height(&self.buffer, idx);
+            self.line_data
+                .calculate_height(&mut self.buffer.iter().copied(), idx);
         } else {
             self.line_data.set_dirty();
         }
@@ -231,7 +86,8 @@ impl InputLine {
     pub(crate) fn calculate_height(&mut self, width: i32, nick_length: usize) -> usize {
         if self.line_data.is_dirty() || self.line_data.needs_resize(width, nick_length) {
             self.line_data.reset(width, nick_length);
-            self.line_data.calculate_height(&self.buffer, 0);
+            self.line_data
+                .calculate_height(&mut self.buffer.iter().copied(), 0);
         }
         self.line_data.get_line_count().unwrap()
     }
@@ -289,7 +145,7 @@ fn draw_line_wrapped(
             cursor_xychar = (pos_x, pos_y, c);
         }
     };
-    let mut split_indices_iter = line.line_data.split_indices.iter().copied().peekable();
+    let mut split_indices_iter = line.line_data.get_splits().iter().copied().peekable();
     for (char_idx, c) in line.buffer.iter().enumerate() {
         let mut style = colors.user_msg;
         // for autocompletion highlighting
diff --git a/libtiny_tui/src/lib.rs b/libtiny_tui/src/lib.rs
index 010dda5c..13b2d39b 100644
--- a/libtiny_tui/src/lib.rs
+++ b/libtiny_tui/src/lib.rs
@@ -7,6 +7,7 @@ mod config;
 mod editor;
 mod exit_dialogue;
 mod input_area;
+mod line_split;
 mod messaging;
 #[doc(hidden)]
 // FIXME: This is "pub" to be able to use in an example
diff --git a/libtiny_tui/src/line_split.rs b/libtiny_tui/src/line_split.rs
new file mode 100644
index 00000000..c712a9bd
--- /dev/null
+++ b/libtiny_tui/src/line_split.rs
@@ -0,0 +1,152 @@
+/// Cache that stores the state of a line's height calculation.
+/// `line_count` is used as the dirty bit to invalidate the cache.
+#[derive(Clone, Debug)]
+pub struct LineDataCache {
+    /// Indices to split on when we draw, not always whitespaces
+    split_indices: Vec<i32>,
+    /// The total number of lines (height) that will be rendered
+    line_count: Option<i32>,
+    /// The current width of InputArea. Used in determining if we need to invalidate due to resize.
+    width: i32,
+    /// The current width of the InputLine (may be shorter due to nickname)
+    line_width: i32,
+    /// Current nickname length. Used in determining if we need to invalidate due to resize.
+    nick_length: usize,
+    /// The index into InputLine::buffer of the last whitespace that we saw in calculate_height()
+    last_whitespace_idx: Option<i32>,
+    /// True if the last character was a whitespace character
+    prev_char_is_whitespace: bool,
+    /// The length of the current line that is being added to.
+    /// Used to determine when to wrap to the next line in calculate_height()
+    current_line_length: i32,
+    /// If the line of text has a cursor
+    has_cursor: bool,
+}
+
+impl LineDataCache {
+    pub fn new(has_cursor: bool) -> LineDataCache {
+        LineDataCache {
+            split_indices: Vec::new(),
+            line_count: None,
+            width: 0,
+            line_width: 0,
+            nick_length: 0,
+            last_whitespace_idx: None,
+            prev_char_is_whitespace: false,
+            current_line_length: 0,
+            has_cursor,
+        }
+    }
+
+    /// Performs a check to see if the width or nickname length changed
+    /// which would require an invalidation of the cache and recalculation of
+    /// the line height.
+    pub fn needs_resize(&self, width: i32, nick_length: usize) -> bool {
+        self.width != width || self.nick_length != nick_length
+    }
+
+    /// Sets `line_count` to `None`, which invalidates the cache.
+    pub fn set_dirty(&mut self) {
+        self.line_count = None;
+    }
+
+    /// Checks if the cache is invalidated by seeing if
+    /// `line_count` is `None`.
+    pub fn is_dirty(&self) -> bool {
+        self.line_count.is_none()
+    }
+
+    /// Resets the cache to a default state that requires
+    /// a height calculation.
+    pub fn reset(&mut self, width: i32, nick_length: usize) {
+        self.split_indices.clear();
+        self.line_count = None;
+        self.width = width;
+        self.nick_length = nick_length;
+        self.line_width = width - nick_length as i32;
+        self.last_whitespace_idx = None;
+        self.prev_char_is_whitespace = false;
+        self.current_line_length = 0;
+    }
+
+    pub fn get_line_count(&self) -> Option<usize> {
+        self.line_count.map(|c| c as usize)
+    }
+
+    pub fn get_splits(&self) -> &[i32] {
+        &self.split_indices
+    }
+
+    /// Function that calculates the height of the line.
+    /// and sets `split_indices` for drawing.
+    /// An `offset` allows for resuming the calculation - see InputLine::insert().
+    /// `offset` must be less than or equal to the current buffer size.
+    ///
+    /// Scans through the buffer in one pass to determine how many lines
+    /// will be needed to render the text with word wrapping.
+    /// If an offset is provided, it will continue the calculation
+    /// from the saved state and save the new line count in `line_count`.
+    pub fn calculate_height<I: Iterator<Item = char>>(&mut self, buffer: I, offset: usize) {
+        let mut temp_count = 1;
+        if let Some(line_count) = self.line_count {
+            temp_count = line_count;
+            // If we made space for the cursor, subtract it.
+            if self.has_cursor && self.current_line_length == self.line_width {
+                temp_count -= 1;
+            }
+        }
+        for (c, current_idx) in buffer.skip(offset).zip(offset..) {
+            let current_idx = current_idx as i32;
+            self.current_line_length += 1;
+
+            if c.is_whitespace() {
+                // Splitting
+                if self.current_line_length > self.line_width {
+                    // we're on a whitespace so just go to next line
+                    temp_count += 1;
+                    // this character will be the first one on the next line
+                    self.current_line_length = 1;
+                    // nick is shown on the first line, set width to full width in the consecutive
+                    // lines
+                    self.line_width = self.width;
+                    // store index for drawing
+                    self.split_indices.push(current_idx);
+                }
+                // store whitespace for splitting
+                self.last_whitespace_idx = Some(current_idx);
+                self.prev_char_is_whitespace = true;
+            } else {
+                // Splitting
+                if self.current_line_length > self.line_width {
+                    // if the previous character was a whitespace, then we have a clean split
+                    if !self.prev_char_is_whitespace && self.last_whitespace_idx.is_some() {
+                        // move back to the last whitespace and get the length of the input that
+                        // will be on the next line
+                        self.current_line_length = current_idx - self.last_whitespace_idx.unwrap();
+                        // store index for drawing
+                        self.split_indices
+                            .push(self.last_whitespace_idx.unwrap() + 1);
+                    } else {
+                        // unclean split on non-whitespace
+                        self.current_line_length = 1;
+                        // store index for drawing
+                        self.split_indices.push(current_idx);
+                    }
+                    // invalidate whitespace since we split here
+                    self.last_whitespace_idx = None;
+                    // moved to next line
+                    temp_count += 1;
+                    // set width to full width
+                    self.line_width = self.width;
+                }
+                self.prev_char_is_whitespace = false;
+            }
+        }
+
+        // Last line length is `line_width`, make room for cursor
+        if self.has_cursor && self.current_line_length == self.line_width {
+            temp_count += 1;
+        }
+        self.line_count = Some(temp_count);
+    }
+}
diff --git a/libtiny_tui/src/msg_area/line.rs b/libtiny_tui/src/msg_area/line.rs
index 87a0f27f..2d70605b 100644
--- a/libtiny_tui/src/msg_area/line.rs
+++ b/libtiny_tui/src/msg_area/line.rs
@@ -1,34 +1,28 @@
+use crate::{
+    config, config::Colors, line_split::LineDataCache, utils::translate_irc_control_chars,
+};
 use std::mem;
 use termbox_simple::{self, Termbox};
 
-use crate::{config, config::Colors, utils::translate_irc_control_chars};
-
 /// A single line added to the widget. May be rendered as multiple lines on the
 /// screen.
 #[derive(Debug)]
 pub(crate) struct Line {
     /// Line segments.
-    segs: Vec<Seg>,
+    string_segments: Vec<StyledString>,
 
     /// The segment we're currently extending.
-    current_seg: Seg,
+    current_seg: StyledString,
 
     /// Number of characters in the line (includes all segments).
     len_chars: i32,
 
-    /// Char indices (NOT byte indices!) of split positions of the line - when
-    /// the line doesn't fit into the screen we split it into multiple lines
-    /// using these positions as split points.
-    ///
-    /// It's important that these are really indices ignoring invisible chars,
-    /// as we use difference between two indices in this vector as length of
-    /// substrings.
-    splits: Vec<i32>,
+    line_data: LineDataCache,
 }
 
 #[derive(Debug)]
-struct Seg {
-    text: String,
+struct StyledString {
+    string: String,
     style: SegStyle,
 }
 
@@ -59,7 +53,7 @@ pub(crate) enum SchemeStyle {
     Timestamp,
 }
 
-impl Seg {
+impl StyledString {
     pub(crate) fn style(&self, colors: &Colors) -> config::Style {
         match self.style {
             SegStyle::Fixed(style) => style,
@@ -91,29 +85,29 @@ const TERMBOX_COLOR_PREFIX: char = '\x00';
 impl Line {
     pub(crate) fn new() -> Line {
         Line {
-            segs: vec![],
-            current_seg: Seg {
-                text: String::new(),
+            string_segments: vec![],
+            current_seg: StyledString {
+                string: String::new(),
                 style: SegStyle::SchemeStyle(SchemeStyle::UserMsg),
             },
             len_chars: 0,
-            splits: Vec::new(),
+            line_data: LineDataCache::new(false),
         }
     }
 
     pub(crate) fn set_style(&mut self, style: SegStyle) {
         // Just update the last segment if it's empty
-        if self.current_seg.text.is_empty() {
+        if self.current_seg.string.is_empty() {
             self.current_seg.style = style;
         } else if self.current_seg.style != style {
             let seg = mem::replace(
                 &mut self.current_seg,
-                Seg {
-                    text: String::new(),
+                StyledString {
+                    string: String::new(),
                     style,
                 },
             );
-            self.segs.push(seg);
+            self.string_segments.push(seg);
         }
     }
 
@@ -129,7 +123,7 @@ impl Line {
             );
         }
         let str = translate_irc_control_chars(str, push_color);
-        self.current_seg.text.reserve(str.len());
+        self.current_seg.string.reserve(str.len());
 
         let mut iter = str.chars();
         while let Some(char) = iter.next() {
@@ -142,10 +136,7 @@ impl Line {
                 let style = config::Style { fg, bg };
                 self.set_style(SegStyle::Fixed(style));
             } else if char > '\x08' {
-                self.current_seg.text.push(char);
-                if char.is_whitespace() {
-                    self.splits.push(self.len_chars);
-                }
+                self.current_seg.string.push(char);
                 self.len_chars += 1;
             }
         }
@@ -153,43 +144,25 @@ impl Line {
 
     pub(crate) fn add_char(&mut self, char: char) {
         assert_ne!(char, TERMBOX_COLOR_PREFIX);
-        if char.is_whitespace() {
-            self.splits.push(self.len_chars);
-        }
-        self.current_seg.text.push(char);
+
+        self.current_seg.string.push(char);
         self.len_chars += 1;
     }
 
     /// How many lines does this take when rendered? O(n) where n = number of
     /// split positions in the line (i.e. whitespaces).
-    pub(crate) fn rendered_height(&self, width: i32) -> i32 {
-        let mut lines: i32 = 1;
-        let mut line_start: i32 = 0;
-
-        for split_idx in 0..self.splits.len() {
-            let char_idx = self.splits[split_idx];
-            // debug!("rendered_height: char_idx: {}", char_idx);
-            let col = char_idx - line_start;
-
-            // How many more chars can we render in this line?
-            let slots_in_line: i32 = width - (col + 1);
-
-            // How many chars do we need to render until the next split point?
-            let chars_until_next_split: i32 =
-                // -1 because we don't need to render the last space.
-                *self.splits.get(split_idx + 1).unwrap_or(&self.len_chars) - 1 - char_idx;
-
-            // debug!("rendered_height: slots_in_line: {}, chars_until_next_split: {}",
-            //        slots_in_line, chars_until_next_split);
-
-            if (chars_until_next_split as i32) > slots_in_line {
-                // debug!("splitting at {}", char_idx);
-                lines += 1;
-                line_start = char_idx + 1;
-            }
+    pub(crate) fn rendered_height(&mut self, width: i32) -> i32 {
+        if self.line_data.is_dirty() || self.line_data.needs_resize(width, 0) {
+            self.line_data.reset(width, 0);
+            // Get all segs together
+            let mut full_line = self
+                .string_segments
+                .iter()
+                .flat_map(|s| s.string.chars())
+                .chain(self.current_seg.string.chars());
+            self.line_data.calculate_height(&mut full_line, 0);
         }
-
-        lines
+        self.line_data.get_line_count().unwrap() as i32
     }
 
     pub(crate) fn draw(
@@ -200,65 +173,44 @@ impl Line {
         pos_y: i32,
         first_line: i32,
         height: i32,
-        width: i32,
     ) {
         let mut col = pos_x;
-        let mut line = 0;
-
-        let mut next_split_idx: usize = 0;
-
-        let mut char_idx: i32 = 0;
-
-        let last_seg: [&Seg; 1] = [&self.current_seg];
-        for seg in self.segs.iter().chain(last_seg.iter().copied()) {
+        let mut line_num = 0;
+        let mut char_idx = 0;
+        let mut split_indices_iter = self.line_data.get_splits().iter().copied().peekable();
+
+        for seg in self
+            .string_segments
+            .iter()
+            .chain(std::iter::once(&self.current_seg))
+        {
             let sty = seg.style(colors);
-
-            for char in seg.text.chars() {
-                if char.is_whitespace() {
-                    // We may want to move to the next line
-                    next_split_idx += 1;
-                    let next_split = self.splits.get(next_split_idx).unwrap_or(&self.len_chars);
-
-                    // How many more chars can we render in this line?
-                    let slots_in_line = width - (col - pos_x);
-
-                    // How many chars do we need to render if until the next
-                    // split point?
-                    assert!(*next_split > char_idx);
-                    let chars_until_next_split: i32 = *next_split - char_idx;
-
-                    // debug!("chars_until_next_split: {},
-                    //        slots_in_line: {}",
-                    //        chars_until_next_split,
-                    //        slots_in_line);
-
-                    if (chars_until_next_split as i32) <= slots_in_line {
-                        // keep rendering chars
-                        if line >= first_line {
-                            tb.change_cell(col, pos_y + line, char, sty.fg, sty.bg);
-                        }
-                        col += 1;
-                    } else {
-                        // Need to split here. Ignore whitespace char.
-                        line += 1;
-                        if line >= height {
+            for c in seg.string.chars() {
+                // If split_indices_iter yields we already know the indices for the start of each line. If it
+                // does not then we just continue outputting on this line.
+                if let Some(next_line_start) = split_indices_iter.peek() {
+                    if char_idx == *next_line_start as usize {
+                        // Move to next line
+                        line_num += 1;
+                        if line_num >= height {
                             break;
                         }
-                        col = pos_x;
-                    }
-                } else {
-                    debug_assert!(!char.is_ascii_control());
-
-                    // Not possible to split. Need to make sure we don't render out
-                    // of bounds.
-                    if col - pos_x < width {
-                        if line >= first_line {
-                            tb.change_cell(col, pos_y + line, char, sty.fg, sty.bg);
+                        // Reset column
+                        col = 0;
+                        // Move to the next line start index
+                        split_indices_iter.next();
+                        // Don't draw whitespaces
+                        if c.is_whitespace() {
+                            char_idx += 1;
+                            continue;
                         }
-                        col += 1;
                     }
                 }
-
+                // Write out the character
+                if line_num >= first_line {
+                    tb.change_cell(col, pos_y + line_num, c, sty.fg, sty.bg);
+                }
+                col += 1;
                 char_idx += 1;
             }
         }
@@ -307,7 +259,7 @@ mod tests {
     fn height_test_1() {
         let mut line = Line::new();
         line.add_text("a b c d e");
-        assert_eq!(line.rendered_height(1), 5);
+        assert_eq!(line.rendered_height(1), 9);
         assert_eq!(line.rendered_height(2), 5);
         assert_eq!(line.rendered_height(3), 3);
         assert_eq!(line.rendered_height(4), 3);
@@ -322,7 +274,7 @@ mod tests {
     fn height_test_2() {
         let mut line = Line::new();
         line.add_text("ab c d e");
-        assert_eq!(line.rendered_height(1), 4);
+        assert_eq!(line.rendered_height(1), 8);
         assert_eq!(line.rendered_height(2), 4);
         assert_eq!(line.rendered_height(3), 3);
         assert_eq!(line.rendered_height(4), 2);
@@ -336,8 +288,8 @@ mod tests {
     fn height_test_3() {
         let mut line = Line::new();
         line.add_text("ab cd e");
-        assert_eq!(line.rendered_height(1), 3);
-        assert_eq!(line.rendered_height(2), 3);
+        assert_eq!(line.rendered_height(1), 7);
+        assert_eq!(line.rendered_height(2), 4);
         assert_eq!(line.rendered_height(3), 3);
         assert_eq!(line.rendered_height(4), 2);
         assert_eq!(line.rendered_height(5), 2);
@@ -349,8 +301,8 @@ mod tests {
     fn height_test_4() {
         let mut line = Line::new();
         line.add_text("ab cde");
-        assert_eq!(line.rendered_height(1), 2);
-        assert_eq!(line.rendered_height(2), 2);
+        assert_eq!(line.rendered_height(1), 6);
+        assert_eq!(line.rendered_height(2), 4);
         assert_eq!(line.rendered_height(3), 2);
         assert_eq!(line.rendered_height(4), 2);
         assert_eq!(line.rendered_height(5), 2);
@@ -359,15 +311,6 @@ mod tests {
 
     #[test]
     fn height_test_5() {
-        let mut line = Line::new();
-        line.add_text("abcde");
-        for i in 0..6 {
-            assert_eq!(line.rendered_height(i), 1);
-        }
-    }
-
-    #[test]
-    fn height_test_6() {
         let text: String = {
             let mut text = String::new();
             let mut single_line = String::new();
@@ -388,7 +331,7 @@ mod tests {
         line.add_text(&text);
         // lipsum.txt has 1160 words in it. each line should contain at most one
         // word so we should have 1160 lines.
-        assert_eq!(line.rendered_height(1), 1160);
+        assert_eq!(line.rendered_height(80), 102);
     }
 
     #[bench]
diff --git a/libtiny_tui/src/msg_area/mod.rs b/libtiny_tui/src/msg_area/mod.rs
index e3a41c40..187a14e5 100644
--- a/libtiny_tui/src/msg_area/mod.rs
+++ b/libtiny_tui/src/msg_area/mod.rs
@@ -50,7 +50,7 @@ impl MsgArea {
         self.lines_height = None;
     }
 
-    pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+    pub(crate) fn draw(&mut self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
         // Where to render current line
         let mut row = pos_y + self.height - 1;
 
@@ -60,7 +60,7 @@ impl MsgArea {
         // Draw lines in reverse order
         let mut line_idx = (self.lines.len() as i32) - 1;
         while line_idx >= 0 && row >= pos_y {
-            let line = &self.lines[line_idx as usize];
+            let line = &mut self.lines[line_idx as usize];
             let line_height = line.rendered_height(self.width);
             debug_assert!(line_height > 0);
 
@@ -80,7 +80,7 @@ impl MsgArea {
             // How many lines to skip in the `Line` before rendering
             let render_from = max(0, pos_y - line_row);
 
-            line.draw(tb, colors, pos_x, line_row, render_from, height, self.width);
+            line.draw(tb, colors, pos_x, line_row, render_from, height);
             row = line_row - 1;
             line_idx -= 1;
             skip = 0;
@@ -101,7 +101,7 @@ impl MsgArea {
             Some(height) => height,
             None => {
                 let mut total_height = 0;
-                for line in &self.lines {
+                for line in &mut self.lines {
                     total_height += line.rendered_height(self.width);
                 }
                 self.lines_height = Some(total_height);


Test patch: