tui_shader/lib.rs
1//! The `tui-shader` crate enables GPU accelerated styling for [`Ratatui`](https://ratatui.rs)
2//! based applications.
3//!
4//! Computing styles at runtime can be expensive when run on the CPU, despite the
5//! small "resolution" of cells in a terminal window. Utilizing the power of the
6//! GPU helps us update the styling in the terminal at considerably higher framerates.
7//!
8//! ## Quickstart
9//!
10//! Add `ratatui` and `tui-shader` as dependencies to your Corgo.toml:
11//!
12//! ```shell
13//! cargo add ratatui tui-shader
14//! ```
15//!
16//! Then create a new application:
17//!
18//! ```rust,no_run
19//! # use tui_shader::{ShaderCanvas, ShaderCanvasState};
20//! let mut terminal = ratatui::init();
21//! let mut state = ShaderCanvasState::default();
22//! while state.get_instant().elapsed().as_secs() < 7 {
23//! terminal.draw(|frame| {
24//! frame.render_stateful_widget(ShaderCanvas::new(),
25//! frame.area(),
26//! &mut state);
27//! }).unwrap();
28//! }
29//! ratatui::restore();
30//! ```
31//!
32//! And run it
33//! ```shell
34//! cargo run
35//! ```
36//!
37//! Well that was lame. Where are all the cool shader-y effects?
38//! We haven't actually provided a shader that the application should use so our [`ShaderCanvasState`]
39//! uses a default shader, which always returns the magenta color. This happends because we created it
40//! using [`ShaderCanvasState::default()`]. Now, let's write a `wgsl` shader and render some fancy stuff
41//! in the terminal.
42//!
43//! ```wgsl
44//! @group(0) @binding(0) var<uniform> time: f32;
45//!
46//! @fragment
47//! fn main(@location(0) uv: vec2<f32>) -> @location(0) vec4<f32> {
48//! let r = sin(1.0 - uv.x + time);
49//! let g = cos(1.0 - uv.y + time);
50//! let b = 0.5;
51//! let d = 1.0 - distance(vec2<f32>(0.5), uv);
52//! let color = vec4<f32>(r, g, b, 1.0) * d;
53//! return color;
54//! }
55//! ```
56//!
57//! Ok, there's a lot to `unwrap` here. At first we define a `uniform` `time` of type `f32`.
58//! The `time` field is filled with data out-of-the-box by our [`ShaderCanvasState`] and can be used in any shader.
59//! The important thing to note is that it isn't the name of the field that's important, it's the `@group` and `@binding`
60//! attributes that determine which data we are reading from it.
61//!
62//! Finally - and this is were the magic happens - we can define our function for
63//! manipulating colors. We must denote our function with `@fragment` because we are writing a fragment shader. As
64//! long as we only define a single `@fragment` function in our file, we can name it whatever we want. Otherwise, we
65//! must create our [`ShaderCanvasState`] using [`ShaderCanvasState::new_with_entry_point`] and pass in the name of
66//! the desired `@fragment` function. A vertex shader cannot be provided as it always uses a single triangle, full-screen
67//! vertex shader.
68//!
69//! We can use the UV coordinates provided by the vertex shader with `@location(0) uv: vec2<f32>`.
70//! Now we have time and UV coordinates to work with to create amazing shaders. This shader just
71//! does some math with these values and returns a new color. Time to get creative!
72//!
73//! Now, all we need to do is create our [`ShaderCanvasState`] using [`ShaderCanvasState::new`] and pass in our shader.
74//!
75//! ```rust,no_run
76//! # use tui_shader::{ShaderCanvas, ShaderCanvasState, WgslShader};
77//! let mut terminal = ratatui::init();
78//! let shader = WgslShader::Path("shader.wgsl");
79//! let mut state = ShaderCanvasState::new(shader).unwrap();
80//! while state.get_instant().elapsed().as_secs() < 5 {
81//! terminal.draw(|frame| {
82//! frame.render_stateful_widget(ShaderCanvas::new(),
83//! frame.area(),
84//! &mut state);
85//! }).unwrap();
86//! }
87//! ratatui::restore();
88//! ```
89//!
90//! Now that's more like it!
91//!
92//! ## Shader Input Parameters
93//!
94//! There a few input parameters that are setup out-of-the-box for use in `tui-shader`:
95//!
96//! | Input | Type | Binding | Explanation |
97//! |----------|-------------|-------------------------|-----------------------------------------------------------------------------------|
98//! | Time | `vec4<f32>` | `@group(0) @binding(0)` | x: time in seconds as `f32`, y: `time.x * 10`, z: `sin(time.x)`, w: `cos(time.x)` |
99//! | Rect | `vec4<u32>` | `@group(0) @binding(1)` | x: x position of rect, y: y position of rect, z: width, w: height |
100//! | UV | `vec2<f32>` | `@location(0)` | x: normalized x coordinate y: norimalized y coordinate |
101//! | Position | `vec4<f32>` | `@builtin(position)` | x: absolute x position y: absolute y position z/w: useless in `tui-shader` |
102
103mod canvas;
104mod context;
105mod state;
106mod style;
107mod util;
108
109pub use crate::canvas::*;
110pub use crate::state::*;
111pub use crate::style::*;
112pub use crate::util::*;
113
114pub use wgpu::include_wgsl;
115
116#[cfg(test)]
117mod tests {
118 use ratatui::{backend::TestBackend, layout::Position};
119
120 use crate::{CharacterRule, ShaderCanvas, ShaderCanvasState, context::ShaderContext};
121
122 #[test]
123 fn default_state() {
124 let mut state = ShaderCanvasState::default();
125 let raw_buffer = state.execute(ShaderContext::default());
126 assert!(raw_buffer.iter().all(|pixel| pixel == &[255, 0, 255, 255]));
127 }
128
129 #[test]
130 fn different_entry_points() {
131 let mut state = ShaderCanvasState::new_with_entry_point(
132 wgpu::include_wgsl!("shaders/test_fragment.wgsl"),
133 "green",
134 )
135 .unwrap();
136 let raw_buffer = state.execute(ShaderContext::default());
137 assert!(raw_buffer.iter().all(|pixel| pixel == &[0, 255, 0, 255]));
138 }
139
140 #[test]
141 fn character_rule_map() {
142 let mut terminal = ratatui::Terminal::new(TestBackend::new(64, 64)).unwrap();
143 let mut state = ShaderCanvasState::default();
144 terminal
145 .draw(|frame| {
146 frame.render_stateful_widget(
147 ShaderCanvas::new().character_rule(CharacterRule::Map(|sample| {
148 if sample.x() == 0 { ' ' } else { '.' }
149 })),
150 frame.area(),
151 &mut state,
152 );
153 let buffer = frame.buffer_mut();
154 for x in 0..buffer.area.width {
155 for y in 0..buffer.area.height {
156 if x == 0 {
157 assert_eq!(buffer.cell_mut(Position::new(x, y)).unwrap().symbol(), " ");
158 } else {
159 assert_eq!(buffer.cell_mut(Position::new(x, y)).unwrap().symbol(), ".");
160 }
161 }
162 }
163 })
164 .unwrap();
165 ratatui::restore();
166 }
167}