ratatui_wgpu/backend/
mod.rs

1pub(crate) mod builder;
2pub(crate) mod wgpu_backend;
3
4use std::num::NonZeroU32;
5
6use ratatui::style::Color;
7use wgpu::{
8    Adapter,
9    BindGroup,
10    CommandEncoder,
11    Device,
12    Extent3d,
13    Queue,
14    RenderPipeline,
15    Surface,
16    SurfaceConfiguration,
17    SurfaceTexture,
18    TextureDescriptor,
19    TextureDimension,
20    TextureFormat,
21    TextureUsages,
22    TextureView,
23    TextureViewDescriptor,
24};
25#[cfg(test)]
26use wgpu::{
27    Buffer,
28    BufferDescriptor,
29    BufferUsages,
30    Texture,
31};
32
33use crate::colors::{
34    named::*,
35    Rgb,
36    ANSI_TO_RGB,
37};
38
39/// A pipeline for post-processing rendered text.
40pub trait PostProcessor {
41    /// Custom user data which will be supplied during creation of the post
42    /// processor. Use this to pass in any external state your processor
43    /// requires.
44    type UserData;
45
46    /// Called during initialization of the backend. This should fully
47    /// initialize the post processor for rendering. Note that you are expected
48    /// to render to the final surface during [`PostProcessor::process`].
49    fn compile(
50        device: &Device,
51        text_view: &TextureView,
52        surface_config: &SurfaceConfiguration,
53        user_data: Self::UserData,
54    ) -> Self;
55
56    /// Called after the drawing dimensions have changed (e.g. the surface was
57    /// resized).
58    fn resize(
59        &mut self,
60        device: &Device,
61        text_view: &TextureView,
62        surface_config: &SurfaceConfiguration,
63    );
64
65    /// Called after text has finished compositing. The provided `text_view` is
66    /// the composited text. The final output of your implementation should
67    /// render to the provided `surface_view`.
68    ///
69    /// <div class="warning">
70    ///
71    /// Retaining a reference to the provided surface view will cause a panic if
72    /// the swapchain is recreated.
73    ///
74    /// </div>
75    fn process(
76        &mut self,
77        encoder: &mut CommandEncoder,
78        queue: &Queue,
79        text_view: &TextureView,
80        surface_config: &SurfaceConfiguration,
81        surface_view: &TextureView,
82    );
83
84    /// Called to see if this post processor wants to update the screen. By
85    /// default, the backend only runs the compositor and post processor when
86    /// the text changes. Returning true from this will override that behavior
87    /// and cause the processor to be invoked after a call to flush, even if no
88    /// text changes occurred.
89    fn needs_update(&self) -> bool {
90        false
91    }
92}
93
94/// The surface dimensions of the backend in pixels.
95pub struct Dimensions {
96    pub width: NonZeroU32,
97    pub height: NonZeroU32,
98}
99
100impl From<(NonZeroU32, NonZeroU32)> for Dimensions {
101    fn from((width, height): (NonZeroU32, NonZeroU32)) -> Self {
102        Self { width, height }
103    }
104}
105
106/// Controls the area the text is rendered to relative to the presentation
107/// surface.
108#[derive(Debug, Default, Clone, Copy)]
109#[non_exhaustive]
110pub enum Viewport {
111    /// Render to the entire surface.
112    #[default]
113    Full,
114    /// Render to a reduced area starting at the top right and rendering up to
115    /// the bottom left - (width, height).
116    Shrink { width: u32, height: u32 },
117}
118
119mod private {
120    use wgpu::Surface;
121
122    use crate::backend::RenderTarget;
123    #[cfg(test)]
124    use crate::backend::{
125        HeadlessSurface,
126        HeadlessTarget,
127    };
128
129    pub trait Sealed {}
130
131    pub struct Token;
132
133    impl Sealed for Surface<'_> {}
134    impl Sealed for RenderTarget {}
135
136    #[cfg(test)]
137    impl Sealed for HeadlessTarget {}
138
139    #[cfg(test)]
140    impl Sealed for HeadlessSurface {}
141}
142
143/// A Texture target that can be rendered to.
144pub trait RenderTexture: private::Sealed + Sized {
145    /// Gets a [`wgpu::TextureView`] that can be used for rendering.
146    fn get_view(&self, _token: private::Token) -> &TextureView;
147    /// Presents the rendered result if applicable.
148    fn present(self, _token: private::Token) {}
149}
150
151impl RenderTexture for RenderTarget {
152    fn get_view(&self, _token: private::Token) -> &TextureView {
153        &self.view
154    }
155
156    fn present(self, _token: private::Token) {
157        self.texture.present();
158    }
159}
160
161#[cfg(test)]
162impl RenderTexture for HeadlessTarget {
163    fn get_view(&self, _token: private::Token) -> &TextureView {
164        &self.view
165    }
166}
167
168/// A surface that can be rendered to.
169pub trait RenderSurface<'s>: private::Sealed {
170    type Target: RenderTexture;
171
172    fn wgpu_surface(&self, _token: private::Token) -> Option<&Surface<'s>>;
173
174    fn get_default_config(
175        &self,
176        adapter: &Adapter,
177        width: u32,
178        height: u32,
179        _token: private::Token,
180    ) -> Option<SurfaceConfiguration>;
181
182    fn configure(&mut self, device: &Device, config: &SurfaceConfiguration, _token: private::Token);
183
184    fn get_current_texture(&self, _token: private::Token) -> Option<Self::Target>;
185}
186
187pub struct RenderTarget {
188    texture: SurfaceTexture,
189    view: TextureView,
190}
191
192impl<'s> RenderSurface<'s> for Surface<'s> {
193    type Target = RenderTarget;
194
195    fn wgpu_surface(&self, _token: private::Token) -> Option<&Surface<'s>> {
196        Some(self)
197    }
198
199    fn get_default_config(
200        &self,
201        adapter: &Adapter,
202        width: u32,
203        height: u32,
204        _token: private::Token,
205    ) -> Option<SurfaceConfiguration> {
206        self.get_default_config(adapter, width, height)
207    }
208
209    fn configure(
210        &mut self,
211        device: &Device,
212        config: &SurfaceConfiguration,
213        _token: private::Token,
214    ) {
215        Surface::configure(self, device, config);
216    }
217
218    fn get_current_texture(&self, _token: private::Token) -> Option<Self::Target> {
219        let output = match self.get_current_texture() {
220            Ok(output) => output,
221            Err(err) => {
222                error!("{err}");
223                return None;
224            }
225        };
226
227        let view = output
228            .texture
229            .create_view(&TextureViewDescriptor::default());
230
231        Some(RenderTarget {
232            texture: output,
233            view,
234        })
235    }
236}
237
238#[cfg(test)]
239pub(crate) struct HeadlessTarget {
240    view: TextureView,
241}
242
243#[cfg(test)]
244pub(crate) struct HeadlessSurface {
245    pub(crate) texture: Option<Texture>,
246    pub(crate) buffer: Option<Buffer>,
247    pub(crate) buffer_width: u32,
248    pub(crate) width: u32,
249    pub(crate) height: u32,
250    pub(crate) format: TextureFormat,
251}
252
253#[cfg(test)]
254impl HeadlessSurface {
255    fn new(format: TextureFormat) -> Self {
256        Self {
257            format,
258            ..Default::default()
259        }
260    }
261}
262
263#[cfg(test)]
264impl Default for HeadlessSurface {
265    fn default() -> Self {
266        Self {
267            texture: Default::default(),
268            buffer: Default::default(),
269            buffer_width: Default::default(),
270            width: Default::default(),
271            height: Default::default(),
272            format: TextureFormat::Rgba8Unorm,
273        }
274    }
275}
276
277#[cfg(test)]
278impl RenderSurface<'static> for HeadlessSurface {
279    type Target = HeadlessTarget;
280
281    fn wgpu_surface(&self, _token: private::Token) -> Option<&Surface<'static>> {
282        None
283    }
284
285    fn get_default_config(
286        &self,
287        _adapter: &Adapter,
288        width: u32,
289        height: u32,
290        _token: private::Token,
291    ) -> Option<SurfaceConfiguration> {
292        Some(SurfaceConfiguration {
293            usage: TextureUsages::RENDER_ATTACHMENT,
294            format: self.format,
295            width,
296            height,
297            present_mode: wgpu::PresentMode::Immediate,
298            desired_maximum_frame_latency: 2,
299            alpha_mode: wgpu::CompositeAlphaMode::Auto,
300            view_formats: vec![],
301        })
302    }
303
304    fn configure(
305        &mut self,
306        device: &Device,
307        config: &SurfaceConfiguration,
308        _token: private::Token,
309    ) {
310        self.texture = Some(device.create_texture(&TextureDescriptor {
311            label: None,
312            size: Extent3d {
313                width: config.width,
314                height: config.height,
315                depth_or_array_layers: 1,
316            },
317            mip_level_count: 1,
318            sample_count: 1,
319            dimension: TextureDimension::D2,
320            format: self.format,
321            usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::COPY_SRC,
322            view_formats: &[],
323        }));
324
325        self.buffer_width = config.width * 4;
326        self.buffer = Some(device.create_buffer(&BufferDescriptor {
327            label: None,
328            size: (self.buffer_width * config.height) as u64,
329            usage: BufferUsages::COPY_DST | BufferUsages::MAP_READ,
330            mapped_at_creation: false,
331        }));
332        self.width = config.width;
333        self.height = config.height;
334    }
335
336    fn get_current_texture(&self, _token: private::Token) -> Option<Self::Target> {
337        self.texture.as_ref().map(|t| HeadlessTarget {
338            view: t.create_view(&TextureViewDescriptor::default()),
339        })
340    }
341}
342
343#[repr(C)]
344#[derive(bytemuck::Pod, bytemuck::Zeroable, Debug, Clone, Copy)]
345struct TextBgVertexMember {
346    vertex: [f32; 2],
347    bg_color: u32,
348}
349
350// Vertex + UVCoord + Color
351#[repr(C)]
352#[derive(bytemuck::Pod, bytemuck::Zeroable, Debug, Clone, Copy)]
353struct TextVertexMember {
354    vertex: [f32; 2],
355    uv: [f32; 2],
356    fg_color: u32,
357    underline_pos: u32,
358    underline_color: u32,
359}
360
361struct TextCacheBgPipeline {
362    pipeline: RenderPipeline,
363    fs_uniforms: BindGroup,
364}
365
366struct TextCacheFgPipeline {
367    pipeline: RenderPipeline,
368    fs_uniforms: BindGroup,
369    atlas_bindings: BindGroup,
370}
371
372struct WgpuState {
373    text_dest_view: TextureView,
374}
375
376fn c2c(color: ratatui::style::Color, reset: Rgb) -> Rgb {
377    match color {
378        Color::Reset => reset,
379        Color::Black => BLACK,
380        Color::Red => RED,
381        Color::Green => GREEN,
382        Color::Yellow => YELLOW,
383        Color::Blue => BLUE,
384        Color::Magenta => MAGENTA,
385        Color::Cyan => CYAN,
386        Color::Gray => GRAY,
387        Color::DarkGray => DARKGRAY,
388        Color::LightRed => LIGHTRED,
389        Color::LightGreen => LIGHTGREEN,
390        Color::LightYellow => LIGHTYELLOW,
391        Color::LightBlue => LIGHTBLUE,
392        Color::LightMagenta => LIGHTMAGENTA,
393        Color::LightCyan => LIGHTCYAN,
394        Color::White => WHITE,
395        Color::Rgb(r, g, b) => [r, g, b],
396        Color::Indexed(idx) => ANSI_TO_RGB[idx as usize],
397    }
398}
399
400fn build_wgpu_state(device: &Device, drawable_width: u32, drawable_height: u32) -> WgpuState {
401    let text_dest = device.create_texture(&TextureDescriptor {
402        label: Some("Text Compositor Out"),
403        size: Extent3d {
404            width: drawable_width.max(1),
405            height: drawable_height.max(1),
406            depth_or_array_layers: 1,
407        },
408        mip_level_count: 1,
409        sample_count: 1,
410        dimension: TextureDimension::D2,
411        format: TextureFormat::Rgba8Unorm,
412        usage: TextureUsages::TEXTURE_BINDING | TextureUsages::RENDER_ATTACHMENT,
413        view_formats: &[],
414    });
415
416    let text_dest_view = text_dest.create_view(&TextureViewDescriptor::default());
417
418    WgpuState { text_dest_view }
419}