1use crate::{
4 anim::FRAME,
5 component::{Component, PaintCtx, Slot, next_slot},
6 context::UiContext,
7 frame::Rect,
8 markup::Dim,
9 props::{Prop, PropValue, Props},
10 shader::{Program, Surface},
11};
12
13pub struct Shader {
39 props: Props,
40 slot: Slot,
41 program: Box<dyn Program>,
42 surface: Surface,
43 cols: u16,
44 rows: u16,
45 live: bool,
46}
47
48impl Shader {
49 pub fn new(program: impl Program + 'static) -> Self {
51 Self {
52 props: Props::new(),
53 slot: next_slot(),
54 program: Box::new(program),
55 surface: Surface::new(),
56 cols: 24,
57 rows: 8,
58 live: true,
59 }
60 }
61
62 pub const fn size(mut self, cols: u16, rows: u16) -> Self {
68 self.cols = cols;
69 self.rows = rows;
70 self
71 }
72
73 fn viewport_cols(&self) -> u16 {
76 match self.props.w() {
77 Some(Dim::Cells(cols)) => cols,
78 _ => self.cols,
79 }
80 }
81
82 fn viewport_rows(&self) -> u16 {
84 self.props.h().unwrap_or(self.rows)
85 }
86
87 pub const fn still(mut self) -> Self {
89 self.live = false;
90 self
91 }
92
93 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
95 self.props.set(prop, value);
96 self
97 }
98}
99
100impl Component for Shader {
101 fn props(&self) -> &Props {
102 &self.props
103 }
104
105 fn props_mut(&mut self) -> &mut Props {
106 &mut self.props
107 }
108
109 fn slot(&self) -> Slot {
110 self.slot
111 }
112
113 fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
114 let cols = self.viewport_cols();
115 (cols, cols)
116 }
117
118 fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
119 self.viewport_rows()
120 }
121
122 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
123 if rect.y >= pc.clip || rect.width == 0 || rect.height == 0 {
124 return;
125 }
126 let base = self.props.style(&pc.ctx.theme);
127 let cols = self.viewport_cols().min(rect.width);
128 let rows = self.viewport_rows().min(rect.height).min(pc.clip - rect.y);
129 let frame = &mut *pc.frame;
130 let mut buffer = [0_u8; 4];
131 self
132 .surface
133 .render(&mut *self.program, pc.now, cols, rows, |x, y, glyph, fg, bg| {
134 let style = match bg {
135 Some(bg) => base.fg(fg).bg(bg),
136 None => base.fg(fg),
137 };
138 frame.put(rect.x + x, rect.y + y, glyph.encode_utf8(&mut buffer), style);
139 });
140 if self.live {
141 pc.wake(self.slot, pc.now + FRAME);
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use std::time::Duration;
149
150 use super::*;
151 use crate::{
152 anim,
153 scene::{Vec3, vec3},
154 test_support::frame_row_text,
155 ui::Ui,
156 };
157
158 fn wall(_: f32, _: f32) -> (Vec3, f32) {
159 (vec3(1.0, 0.0, 0.0), 1.0)
160 }
161
162 fn lit_blocks(ui: &Ui) -> bool {
163 (0..ui.frame().size().height).any(|row| {
164 frame_row_text(ui.frame(), row)
165 .chars()
166 .any(|glyph| glyph == '▀')
167 })
168 }
169
170 #[test]
171 fn live_shader_paints_half_blocks_and_reschedules() {
172 let mut ui = Ui::from_root(Shader::new(wall).size(12, 4), 12, UiContext::default());
173 assert!(lit_blocks(&ui), "the wall lights half-block cells");
174 assert_eq!(ui.next_wake(), Some(anim::FRAME));
175 assert!(ui.tick(anim::FRAME), "the frame deadline repaints");
176 assert_eq!(ui.next_wake(), Some(anim::FRAME + anim::FRAME));
177 }
178
179 #[test]
180 fn still_shader_requests_no_wake() {
181 let ui = Ui::from_root(Shader::new(wall).size(12, 4).still(), 12, UiContext::default());
182 assert!(lit_blocks(&ui));
183 assert_eq!(ui.next_wake(), None);
184 }
185
186 #[test]
187 fn animated_shader_reads_the_paint_clock() {
188 struct Sweep {
190 column: f32,
191 }
192 impl Program for Sweep {
193 fn advance(&mut self, now: Duration, width: f32, _: f32) {
194 self.column = now.as_secs_f32() % width;
195 }
196
197 fn fragment(&self, x: f32, _: f32) -> (Vec3, f32) {
198 let lit = x >= self.column && x < self.column + 1.0;
199 (vec3(1.0, 1.0, 1.0), if lit { 1.0 } else { 0.0 })
200 }
201 }
202
203 let rows = |ui: &Ui| -> Vec<String> {
204 (0..ui.frame().size().height)
205 .map(|row| frame_row_text(ui.frame(), row))
206 .collect()
207 };
208 let mut ui =
209 Ui::from_root(Shader::new(Sweep { column: 0.0 }).size(16, 4), 16, UiContext::default());
210 let start = rows(&ui);
211 ui.tick(Duration::from_millis(2000));
212 assert_ne!(start, rows(&ui), "the clock moves the bar");
213 }
214}