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