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}