fatui/frame/mod.rs
1//! a frame of input and output
2
3mod area;
4mod cgptr;
5
6use std::{
7 fmt::{self, Debug},
8 ops::{Index, IndexMut, Range},
9};
10
11use area::FrameArea;
12use cgptr::CgPtr;
13
14use crate::{
15 Event, InputState, StateEvent,
16 grid::Grid,
17 input::{FrameInput, Nop},
18 output::StyledChar,
19 pos::{Pos, Rect, Size, X, Y},
20};
21
22#[cfg(test)]
23mod test_draw;
24#[cfg(test)]
25mod test_split;
26
27/// a single frame of input/output
28///
29/// one [`Frame`] encompasses:
30/// - a single user input event
31/// - a character grid you can render the ui to
32///
33/// you get it from `Backend::step`, update state and render accordingly,
34/// and then step to the next frame.
35///
36/// the frame you get by default has an [`Event`][crate::Event],
37/// to avoid the overhead of keeping an [`Input`][crate::Input] up to date unnecessarily.
38/// if you want to, though, use [`Self::with`].
39///
40/// # virtual cells
41///
42/// there's one bit of weirdness:
43/// much like x11, where a "window" doesn't necessarily have a full pixel buffer,
44/// frames don't necessarily have *actual character grid cells*
45/// underlying the entire extent of the frame.
46/// instead they know which bits of the grid they do own,
47/// and can draw to those,
48/// and the rest of the ("virtual") cells simply have writes discarded.
49///
50/// you always start with a [`Row`].
51/// for simple implementation, access via indexing always provides a `&mut Styled<char>`,
52/// which is just sometimes an internal discard buffer.
53/// if you'd like to optimize, you can use `Row::extents`,
54/// which
55pub struct Frame<'b, Input> {
56 /// the character grid buffer this is writing to
57 ///
58 /// can't have multiple &mut to the same memory --
59 /// so we have a *mut, and tie the lifetime with a PhantomData
60 /// then slice out specific bits, so our &muts never overlap!
61 ///
62 /// this can be `None` if the frame is fully shadowed,
63 /// to let writes short-circuit.
64 buf: Option<CgPtr<'b>>,
65 /// the input associated with this frame
66 input: Input,
67 /// the portion of the [`Grid<StyledChar>`] this frame owns
68 area: FrameArea,
69 // TODO: extra extents, maybe in FrameArea, for scrollability
70}
71
72impl<Input: Debug> fmt::Debug for Frame<'_, Input> {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 f.debug_struct("Frame<'_>")
75 .field("buf", &..)
76 .field("input", &self.input)
77 .field("area", &self.area)
78 .finish()
79 }
80}
81
82impl Frame<'static, Nop> {
83 /// create a new [`Frame::null`] without associated input
84 ///
85 /// mostly useful for tests.
86 pub fn nop(size: Size) -> Self {
87 Self::null(Nop, size)
88 }
89}
90
91impl<Input> Frame<'static, Input> {
92 /// create a new frame with no backing memory
93 ///
94 /// this effectively makes the entire frame "virtual cells",
95 /// which can be useful to allow components to skip rendering entirely.
96 pub fn null(input: Input, size: Size) -> Self {
97 Self { buf: None, input, area: FrameArea::full(size) }
98 }
99}
100
101impl<'b, Input: FrameInput> Frame<'b, Input> {
102 /// create a new [`Frame`] containing an entire frame buffer
103 pub fn new(input: Input, chars: &'b mut Grid<StyledChar>) -> Self {
104 let size = chars.size();
105 if size.is_empty() {
106 return Frame::null(input, chars.size());
107 }
108 Self { buf: Some(CgPtr::new(chars)), input, area: FrameArea::full(size) }
109 }
110
111 /// split this framebuffer in two vertically adjacent pieces, at the row
112 ///
113 /// **you probably want [`Self::split`]!**
114 /// this is meant to be used to implement splitters,
115 /// not directly by end users.
116 /// (it does have some niche utility though.)
117 ///
118 /// the first is rows `[0, y)` and the second is `[y, height)`,
119 /// but if `y` is out of bounds, this returns `None`.
120 pub fn split_v(self, y: Y) -> Option<(Self, Self)> {
121 if y > self.size().height {
122 return None;
123 }
124 let (ta, ba) = self.area.split_v(y);
125 // TODO: handle virtual areas properly -- maybe `area` can inform that?
126 let (ti, bi) = self.input.split_v(y);
127 Some((
128 Self { buf: self.buf.clone(), input: ti, area: ta },
129 Self { buf: self.buf.clone(), input: bi, area: ba },
130 ))
131 }
132
133 /// split this framebuffer in two horizontally adjacent pieces, at the column.
134 ///
135 /// the first is rows `[0, x)` and the second is `[x, width)`,
136 /// but if `x` is out of bounds, this returns `None`.
137 pub fn split_h(self, x: X) -> Option<(Self, Self)> {
138 if x > self.size().width {
139 return None;
140 }
141 let (la, ra) = self.area.split_h(x);
142 // TODO: handle virtual areas properly -- maybe `area` can inform that?
143 let (li, ri) = self.input.split_h(x);
144 Some((
145 Self { buf: self.buf.clone(), input: li, area: la },
146 Self { buf: self.buf.clone(), input: ri, area: ra },
147 ))
148 }
149
150 /// get the input associated with this frame
151 pub fn input(&self) -> &Input {
152 &self.input
153 }
154 /// get the size of this frame
155 pub fn size(&self) -> Size {
156 self.area.size()
157 }
158 /// get whether this frame contains any cells
159 pub fn is_empty(&self) -> bool {
160 self.size().is_empty()
161 }
162
163 /// expand the frame to occupy more virtual cells
164 ///
165 /// `tl` is how many virtual cells up and to the left should be additionally covered,
166 /// and `br` is how many down and to the right.
167 pub fn vgrow(&mut self, tl: Size, br: Size) {
168 self.area.vgrow(tl, br);
169 }
170
171 /// fill every cell in this frame with a specific character
172 pub fn fill(&mut self, ch: StyledChar) {
173 for ry in self.rows() {
174 let mut row = self.row(ry);
175 row.fill(ch);
176 }
177 }
178
179 /// iterate over all the row positions of this frame
180 ///
181 /// note that you still need to get each row with `row`,
182 /// but you're guaranteed to get a `Some` back.
183 /// (for `&mut` safety reasons there's no way to do an iterator of rows directly.)
184 pub fn rows(&self) -> impl Iterator<Item = Y> + use<Input> {
185 (0..self.size().height.0).map(Y)
186 }
187
188 /// get the row of characters at the given y-coordinate,
189 /// panicking if it's out of bounds.
190 ///
191 /// this allows you to directly futz with the contents of a [`Grid<StyledChar>`],
192 /// but be cautious doing so with a frame you've already rendered to --
193 /// it won't cause ub but it's a recipe for things to look bad!
194 ///
195 /// on the other hand, this is literally exactly how you're meant to render to a blank frame.
196 /// go for it, buddy!
197 ///
198 /// please note that you can't have more than one row "checked out" at a time,
199 /// so you need to make sure not to let the lifetimes overlap
200 /// if you don't have a convenient scope already lying around (e.g. a loop body):
201 /// ```compile_fail(E0499)
202 /// # use fatui::{frame::Frame, input::Nop, pos::{Y, Size}};
203 /// let mut frame = // backend.step(), etc.
204 /// # Frame::nop(Size::ZERO);
205 /// let row1 = frame.row(Y(1));
206 /// // that counts as a mutable reference, so trying to take another fails!
207 /// let row2 = frame.row(Y(2));
208 /// // (and then some attempt to use both at once,
209 /// // because otherwise rustc is smart enough to drop `row1` for you!)
210 /// println!("{row1:?}, {row2:?}");
211 /// ```
212 ///
213 /// if you're using this in a loop body, you likely won't even notice,
214 /// since each row naturally dies at the end of the body before you get the next.
215 /// but if you're not, you might need to do something like:
216 /// ```
217 /// # use fatui::{frame::Frame, input::Nop, pos::{Y, Size}};
218 /// let mut frame = // backend.step(), etc.
219 /// # Frame::nop(Size::rnew(2, 2));
220 /// let row1 = frame.row(Y(1));
221 /// std::mem::drop(row1);
222 /// let row2 = frame.row(Y(2));
223 /// ```
224 pub fn row<'s>(&'s mut self, y: Y) -> Row<'s> {
225 let vwidth = self.size().width;
226 let Some(ref mut cg) = self.buf else {
227 return Row::vir(vwidth);
228 };
229 let Some(ri) = self.area.rowinfo(y) else {
230 return Row::vir(vwidth);
231 };
232 // SAFETY:
233 // - `area`'s bit is only derived by splitting, which is mutually exclusive
234 // - `compile_fail` doctest above proves `row` returns are mutually exclusive
235 // - (`slice` also does bound checks, but these should all be in-bounds too.)
236 let slice = unsafe { cg.slice(ri.slice, ri.real_y) };
237 Row::abs(vwidth, slice, ri.xs)
238 }
239}
240
241impl<'g> Frame<'g, Event> {
242 /// update an [`InputState`] and return this frame with it attached
243 pub fn with<'i>(self, state: &'i mut InputState) -> Frame<'g, StateEvent<'i>> {
244 state.update(&self.input);
245 Frame {
246 buf: self.buf.clone(),
247 input: StateEvent {
248 event: self.input,
249 state: &*state,
250 // TODO: handle virtual sizes correctly
251 rect: Rect::tlsz(Pos::ZERO, self.area.size()),
252 },
253 area: self.area,
254 }
255 }
256}
257
258/// One row of a [`Frame`].
259pub struct Row<'b> {
260 vwidth: X,
261 absolute: Option<(&'b mut [StyledChar], Range<usize>)>,
262 dummy: StyledChar,
263}
264impl fmt::Debug for Row<'_> {
265 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
266 f.debug_struct("Row")
267 .field("vwidth", &self.vwidth.0)
268 .field("absolute", &self.absolute.as_ref().map(|(_, r)| ([..], r)))
269 .finish()
270 }
271}
272
273impl<'b> Row<'b> {
274 fn vir(vwidth: X) -> Self {
275 Self { vwidth, absolute: None, dummy: StyledChar::BLANK }
276 }
277 fn abs(vwidth: X, slice: &'b mut [StyledChar], range: Range<usize>) -> Self {
278 Self { vwidth, absolute: Some((slice, range)), dummy: StyledChar::BLANK }
279 }
280
281 /// fill the row with a specific character
282 pub fn fill(&mut self, ch: StyledChar) {
283 if let Some((s, _)) = &mut self.absolute {
284 s.fill(ch);
285 }
286 }
287
288 /// iterate over all the real characters in this row
289 ///
290 /// **pay close attention to the index!**
291 /// it very well might not start at `0` or end at `.len`!
292 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut StyledChar> {
293 [].into_iter()
294 }
295}
296
297impl<'b> Index<X> for Row<'b> {
298 type Output = StyledChar;
299 fn index(&self, x: X) -> &Self::Output {
300 assert!(x < self.vwidth, "x={x} is out of bounds ({})", self.vwidth);
301 let Some((s, r)) = &self.absolute else {
302 return &self.dummy;
303 };
304 if !r.contains(&x.0) { &self.dummy } else { &s[x.0 - r.start] }
305 }
306}
307impl<'b> IndexMut<X> for Row<'b> {
308 fn index_mut(&mut self, x: X) -> &mut Self::Output {
309 assert!(x < self.vwidth, "x={x} is out of bounds ({})", self.vwidth);
310 let Some((s, r)) = &mut self.absolute else {
311 return &mut self.dummy;
312 };
313 if !r.contains(&x.0) { &mut self.dummy } else { &mut s[x.0 - r.start] }
314 }
315}