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
use std::convert::TryInto;
use euclid::{Point2D, Rect, Size2D};
use thiserror::Error;
use unicode_segmentation::UnicodeSegmentation;
use crate::{unit::Cell, Am, Command, CommandBuf};
/// Draw on a subset of the screen
pub struct Printer<'a> {
/// Area this printer is allowed to print on
area: Rect<u16, Cell>,
cmd_buf: Am<CommandBuf>,
new_cmd_buf: NewCmdBuf<'a>,
}
type NewCmdBuf<'a> = &'a dyn Fn() -> Am<CommandBuf>;
impl<'a> Printer<'a> {
/// Creates a new printer on the given window
pub(crate) fn new<S>(
size: S,
cmd_buf: Am<CommandBuf>,
new_cmd_buf: NewCmdBuf<'a>,
) -> Self
where
S: Into<Size2D<u16, Cell>>,
{
Self {
area: Rect::new(Point2D::zero(), size.into()),
cmd_buf,
new_cmd_buf,
}
}
/// Used on the root view when the screen size changes
pub(crate) fn set_size<S>(&mut self, size: S)
where
S: Into<Size2D<u16, Cell>>,
{
self.area.size = size.into();
}
/// Get the size of this printer
///
/// Note that the printable area will be `[(0, 0), (size.x, size.y))`, i.e.
/// inclusive-exclusive.
pub fn size(&self) -> Size2D<u16, Cell> {
self.area.size
}
/// Prints text inside the printer's area
pub fn print<P>(&self, text: &str, start: P) -> Result<(), OutOfBounds>
where
P: Into<Point2D<u16, Cell>>,
{
// Where we are asked to start printing relative to the printer
let start = start.into();
// Where printing starts relative to the origin of the screen (not the
// printer). Note that crossterm represents the top-left-most cell as
// (0, 0) and not (1, 1).
let start = self.area.origin + start.to_vector();
// Starting point must be inside the printer area
if !&self.area.contains(start) {
return Err(OutOfBounds::Start(self.area, start));
}
// The amount of columns the text requires
let width =
text.graphemes(true).count().try_into().expect("u16 overflow");
// Where the printing will stop relative to the screen
let end = (
start
.x
.saturating_add(width)
// One character only takes a single cell, which means the start
// and end are the same value.
.saturating_sub(1)
// Disallow going before the beginning in case width is zero
.max(start.x),
start.y,
)
.into();
// Ending point must be inside the printer area
if !&self.area.contains(end) {
return Err(OutOfBounds::End(self.area, end));
}
self.cmd_buf.lock().cmds.push(Command::MoveTo(start));
self.cmd_buf.lock().cmds.push(Command::Print(text.to_string()));
Ok(())
}
/// Shows the cursor at the given location
pub fn show_cursor_at<P>(&self, point: P) -> Result<(), OutOfBounds>
where
P: Into<Point2D<u16, Cell>>,
{
let point = point.into();
let point = self.area.origin + point.to_vector();
if !self.area.contains(point) {
return Err(OutOfBounds::Start(self.area, point));
}
let mut cmd_buf = self.cmd_buf.lock();
cmd_buf.cmds.push(Command::MoveTo(point));
cmd_buf.cmds.push(Command::ShowCursor);
cmd_buf.changes_cursor = true;
Ok(())
}
/// Hides the cursor
pub fn hide_cursor<P>(&self) {
self.cmd_buf.lock().cmds.push(Command::HideCursor);
// Don't bother setting changes_cursor to true because we don't want to
// potentially contest another view that wants the cursor shown
// somewhere else
}
/// Returns a printer that's allowed to print on a subset of its parent
pub fn to_sub_area<R>(&self, sub_area: R) -> Result<Self, OutOfBounds>
where
R: Into<Rect<u16, Cell>>,
{
let sub_area = sub_area.into();
let new_area = Rect {
origin: self.area.origin + sub_area.origin.to_vector(),
size: sub_area.size,
};
if !self.area.contains_rect(&new_area) {
return Err(OutOfBounds::Area(self.area, new_area));
}
Ok(Self {
area: new_area,
cmd_buf: (self.new_cmd_buf)(),
new_cmd_buf: self.new_cmd_buf,
})
}
}
/// Error returned from some [`Printer`] methods
///
/// [`Printer`]: Printer
#[derive(Error, Debug)]
pub enum OutOfBounds {
/// The starting point was out of bounds
#[error("starting point {1:?} out of bounds {0:?}")]
Start(Rect<u16, Cell>, Point2D<u16, Cell>),
/// The ending point was out of bounds
#[error("ending point {1:?} out of bounds {0:?}")]
End(Rect<u16, Cell>, Point2D<u16, Cell>),
/// A new rectangle does not fit inside the old rectangle
#[error("inner area {1:?} out of outer rectangle {0:?}")]
Area(Rect<u16, Cell>, Rect<u16, Cell>),
}