1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
use crate::cell::{unicode_column_width, AttributeChange, CellAttributes};
use crate::color::ColorAttribute;
pub use crate::image::{ImageData, TextureCoordinate};
use crate::surface::{CursorShape, Position};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use unicode_segmentation::UnicodeSegmentation;

/// `Change` describes an update operation to be applied to a `Surface`.
/// Changes to the active attributes (color, style), moving the cursor
/// and outputting text are examples of some of the values.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub enum Change {
    /// Change a single attribute
    Attribute(AttributeChange),
    /// Change all possible attributes to the given set of values
    AllAttributes(CellAttributes),
    /// Add printable text.
    /// Control characters are rendered inert by transforming them
    /// to space.  CR and LF characters are interpreted by moving
    /// the cursor position.  CR moves the cursor to the start of
    /// the line and LF moves the cursor down to the next line.
    /// You typically want to use both together when sending in
    /// a line break.
    Text(String),
    /// Clear the screen to the specified color.
    /// Implicitly clears all attributes prior to clearing the screen.
    /// Moves the cursor to the home position (top left).
    ClearScreen(ColorAttribute),
    /// Clear from the current cursor X position to the rightmost
    /// edge of the screen.  The background color is set to the
    /// provided color.  The cursor position remains unchanged.
    ClearToEndOfLine(ColorAttribute),
    /// Clear from the current cursor X position to the rightmost
    /// edge of the screen on the current line.  Clear all of the
    /// lines below the current cursor Y position.  The background
    /// color is set ot the provided color.  The cursor position
    /// remains unchanged.
    ClearToEndOfScreen(ColorAttribute),
    /// Move the cursor to the specified `Position`.
    CursorPosition { x: Position, y: Position },
    /// Change the cursor color.
    CursorColor(ColorAttribute),
    /// Change the cursor shape
    CursorShape(CursorShape),
    /// Place an image at the current cursor position.
    /// The image defines the dimensions in cells.
    /// TODO: check iterm rendering behavior when the image is larger than the width of the screen.
    /// If the image is taller than the remaining space at the bottom
    /// of the screen, the screen will scroll up.
    /// The cursor Y position is unchanged by rendering the Image.
    /// The cursor X position will be incremented by `Image::width` cells.
    Image(Image),
    /// Scroll the `region_size` lines starting at `first_row` upwards
    /// by `scroll_count` lines.  The `scroll_count` lines at the top of
    /// the region are overwritten.  The `scroll_count` lines at the
    /// bottom of the region will become blank.
    ///
    /// After a region is scrolled, the cursor position is undefined,
    /// and the terminal's scroll region is set to the range specified.
    /// To restore scrolling behaviour to the full terminal window, an
    /// additional `Change::ScrollRegionUp { first_row: 0, region_size:
    /// height, scroll_count: 0 }`, where `height` is the height of the
    /// terminal, should be emitted.
    ScrollRegionUp {
        first_row: usize,
        region_size: usize,
        scroll_count: usize,
    },
    /// Scroll the `region_size` lines starting at `first_row` downwards
    /// by `scroll_count` lines.  The `scroll_count` lines at the bottom
    /// the region are overwritten.  The `scroll_count` lines at the top
    /// of the region will become blank.
    ///
    /// After a region is scrolled, the cursor position is undefined,
    /// and the terminal's scroll region is set to the range specified.
    /// To restore scrolling behaviour to the full terminal window, an
    /// additional `Change::ScrollRegionDown { first_row: 0,
    /// region_size: height, scroll_count: 0 }`, where `height` is the
    /// height of the terminal, should be emitted.
    ScrollRegionDown {
        first_row: usize,
        region_size: usize,
        scroll_count: usize,
    },
    /// Change the title of the window in which the surface will be
    /// rendered.
    Title(String),
}

impl Change {
    pub fn is_text(&self) -> bool {
        match self {
            Change::Text(_) => true,
            _ => false,
        }
    }

    pub fn text(&self) -> &str {
        match self {
            Change::Text(text) => text,
            _ => panic!("you must use Change::is_text() to guard calls to Change::text()"),
        }
    }
}

impl<S: Into<String>> From<S> for Change {
    fn from(s: S) -> Self {
        Change::Text(s.into())
    }
}

impl From<AttributeChange> for Change {
    fn from(c: AttributeChange) -> Self {
        Change::Attribute(c)
    }
}

/// Keeps track of a run of changes and allows reasoning about the cursor
/// position and the extent of the screen that the sequence will affect.
/// This is useful for example when implementing something like a LineEditor
/// where you don't want to take control over the entire surface but do want
/// to be able to emit a dynamically sized output relative to the cursor
/// position at the time that the editor is invoked.
pub struct ChangeSequence {
    changes: Vec<Change>,
    screen_rows: usize,
    screen_cols: usize,
    pub(crate) cursor_x: usize,
    pub(crate) cursor_y: isize,
    render_y_max: isize,
    render_y_min: isize,
}

impl ChangeSequence {
    pub fn new(rows: usize, cols: usize) -> Self {
        Self {
            changes: vec![],
            screen_rows: rows,
            screen_cols: cols,
            cursor_x: 0,
            cursor_y: 0,
            render_y_max: 0,
            render_y_min: 0,
        }
    }

    pub fn consume(self) -> Vec<Change> {
        self.changes
    }

    /// Returns the cursor position, (x, y).
    pub fn current_cursor_position(&self) -> (usize, isize) {
        (self.cursor_x, self.cursor_y)
    }

    pub fn move_to(&mut self, (cursor_x, cursor_y): (usize, isize)) {
        self.add(Change::CursorPosition {
            x: Position::Relative(cursor_x as isize - self.cursor_x as isize),
            y: Position::Relative(cursor_y - self.cursor_y),
        });
    }

    /// Returns the total number of rows affected
    pub fn render_height(&self) -> usize {
        (self.render_y_max - self.render_y_min).max(0).abs() as usize
    }

    fn update_render_height(&mut self) {
        self.render_y_max = self.render_y_max.max(self.cursor_y);
        self.render_y_min = self.render_y_min.min(self.cursor_y);
    }

    pub fn add_changes(&mut self, changes: Vec<Change>) {
        for change in changes {
            self.add(change);
        }
    }

    pub fn add<C: Into<Change>>(&mut self, change: C) {
        let change = change.into();
        match &change {
            Change::AllAttributes(_)
            | Change::Attribute(_)
            | Change::CursorColor(_)
            | Change::CursorShape(_)
            | Change::ClearToEndOfLine(_)
            | Change::Title(_)
            | Change::ClearToEndOfScreen(_) => {}
            Change::Text(t) => {
                for g in t.as_str().graphemes(true) {
                    if self.cursor_x == self.screen_cols {
                        self.cursor_y += 1;
                        self.cursor_x = 0;
                    }
                    if g == "\n" {
                        self.cursor_y += 1;
                    } else if g == "\r" {
                        self.cursor_x = 0;
                    } else if g == "\r\n" {
                        self.cursor_y += 1;
                        self.cursor_x = 0;
                    } else {
                        let len = unicode_column_width(g);
                        self.cursor_x += len;
                    }
                }
                self.update_render_height();
            }
            Change::Image(im) => {
                self.cursor_x += im.width;
                self.render_y_max = self.render_y_max.max(self.cursor_y + im.height as isize);
            }
            Change::ClearScreen(_) => {
                self.cursor_x = 0;
                self.cursor_y = 0;
            }
            Change::CursorPosition { x, y } => {
                self.cursor_x = match x {
                    Position::Relative(x) => {
                        ((self.cursor_x as isize + x) % self.screen_cols as isize) as usize
                    }
                    Position::Absolute(x) => x % self.screen_cols,
                    Position::EndRelative(x) => (self.screen_cols - x) % self.screen_cols,
                };

                self.cursor_y = match y {
                    Position::Relative(y) => {
                        (self.cursor_y as isize + y) % self.screen_rows as isize
                    }
                    Position::Absolute(y) => (y % self.screen_rows) as isize,
                    Position::EndRelative(y) => {
                        ((self.screen_rows - y) % self.screen_rows) as isize
                    }
                };
                self.update_render_height();
            }
            Change::ScrollRegionUp { .. } | Change::ScrollRegionDown { .. } => {
                // The resultant cursor position is undefined by
                // the renderer!
                // We just pick something.
                self.cursor_x = 0;
                self.cursor_y = 0;
            }
        }

        self.changes.push(change);
    }
}

/// The `Image` `Change` needs to support adding an image that spans multiple
/// rows and columns, as well as model the content for just one of those cells.
/// For instance, if some of the cells inside an image are replaced by textual
/// content, and the screen is scrolled, computing the diff change stream needs
/// to be able to express that a single cell holds a slice from a larger image.
/// The `Image` struct expresses its dimensions in cells and references a region
/// in the shared source image data using texture coordinates.
/// A 4x3 cell image would set `width=3`, `height=3`, `top_left=(0,0)`, `bottom_right=(1,1)`.
/// The top left cell from that image, if it were to be included in a diff,
/// would be recorded as `width=1`, `height=1`, `top_left=(0,0)`, `bottom_right=(1/4,1/3)`.
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct Image {
    /// measured in cells
    pub width: usize,
    /// measure in cells
    pub height: usize,
    /// Texture coordinate for the top left of this image block.
    /// (0,0) is the top left of the ImageData. (1, 1) is
    /// the bottom right.
    pub top_left: TextureCoordinate,
    /// Texture coordinates for the bottom right of this image block.
    pub bottom_right: TextureCoordinate,
    /// the image data
    pub image: Arc<ImageData>,
}