1use std::sync::Arc;
10
11use gpui::prelude::*;
12use gpui::{
13 canvas, fill, px, quad, size, App, BorderStyle, Bounds, ContentMask, Corners, Edges, Hsla,
14 ImageFormat, IntoElement, Pixels, RenderImage, Window,
15};
16
17use crate::devtools::Probed;
18use crate::theme::theme;
19
20#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
22pub enum GpuFit {
23 #[default]
25 Contain,
26 Cover,
28 Stretch,
30}
31
32#[derive(Clone, Copy, Debug, Default, PartialEq)]
34pub struct GpuRect {
35 pub x: f32,
36 pub y: f32,
37 pub width: f32,
38 pub height: f32,
39}
40
41impl GpuRect {
42 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
43 Self {
44 x,
45 y,
46 width,
47 height,
48 }
49 }
50}
51
52#[derive(Clone, Debug)]
57pub struct GpuTexture {
58 image: Arc<gpui::Image>,
59}
60
61impl GpuTexture {
62 pub fn from_encoded(format: ImageFormat, bytes: impl Into<Vec<u8>>) -> Self {
63 Self {
64 image: Arc::new(gpui::Image::from_bytes(format, bytes.into())),
65 }
66 }
67
68 pub fn png(bytes: impl Into<Vec<u8>>) -> Self {
69 Self::from_encoded(ImageFormat::Png, bytes)
70 }
71
72 pub fn jpeg(bytes: impl Into<Vec<u8>>) -> Self {
73 Self::from_encoded(ImageFormat::Jpeg, bytes)
74 }
75
76 pub fn webp(bytes: impl Into<Vec<u8>>) -> Self {
77 Self::from_encoded(ImageFormat::Webp, bytes)
78 }
79}
80
81#[derive(Clone, Debug)]
82struct GpuQuad {
83 bounds: GpuRect,
84 fill: Hsla,
85 radius: f32,
86 border_width: f32,
87 border: Hsla,
88}
89
90#[derive(Clone, Debug)]
91enum GpuCommand {
92 Quad(GpuQuad),
93 Texture {
94 texture: GpuTexture,
95 bounds: GpuRect,
96 source: Option<GpuRect>,
97 },
98}
99
100#[derive(Clone, Debug)]
104pub struct GpuScene {
105 width: f32,
106 height: f32,
107 commands: Vec<GpuCommand>,
108}
109
110impl GpuScene {
111 pub fn new(width: f32, height: f32) -> Self {
112 Self {
113 width: finite_positive(width),
114 height: finite_positive(height),
115 commands: Vec::new(),
116 }
117 }
118
119 pub fn size(&self) -> (f32, f32) {
120 (self.width, self.height)
121 }
122
123 pub fn len(&self) -> usize {
124 self.commands.len()
125 }
126
127 pub fn is_empty(&self) -> bool {
128 self.commands.is_empty()
129 }
130
131 pub fn rect(mut self, bounds: GpuRect, color: Hsla) -> Self {
133 self.push_rect(bounds, color);
134 self
135 }
136
137 pub fn rounded_rect(mut self, bounds: GpuRect, color: Hsla, radius: f32) -> Self {
139 self.commands.push(GpuCommand::Quad(GpuQuad {
140 bounds: sane_rect(bounds),
141 fill: color,
142 radius: finite_nonnegative(radius),
143 border_width: 0.0,
144 border: gpui::transparent_black(),
145 }));
146 self
147 }
148
149 pub fn bordered_rect(
151 mut self,
152 bounds: GpuRect,
153 fill: Hsla,
154 border: Hsla,
155 border_width: f32,
156 radius: f32,
157 ) -> Self {
158 self.commands.push(GpuCommand::Quad(GpuQuad {
159 bounds: sane_rect(bounds),
160 fill,
161 radius: finite_nonnegative(radius),
162 border_width: finite_nonnegative(border_width),
163 border,
164 }));
165 self
166 }
167
168 pub fn push_rect(&mut self, bounds: GpuRect, color: Hsla) {
169 self.commands.push(GpuCommand::Quad(GpuQuad {
170 bounds: sane_rect(bounds),
171 fill: color,
172 radius: 0.0,
173 border_width: 0.0,
174 border: gpui::transparent_black(),
175 }));
176 }
177
178 pub fn texture(mut self, texture: GpuTexture, bounds: GpuRect) -> Self {
180 self.push_texture(texture, bounds);
181 self
182 }
183
184 pub fn sprite(mut self, texture: GpuTexture, source: GpuRect, bounds: GpuRect) -> Self {
190 self.push_sprite(texture, source, bounds);
191 self
192 }
193
194 pub fn background(self, texture: GpuTexture) -> Self {
196 let bounds = GpuRect::new(0.0, 0.0, self.width, self.height);
197 self.texture(texture, bounds)
198 }
199
200 pub fn push_texture(&mut self, texture: GpuTexture, bounds: GpuRect) {
201 self.commands.push(GpuCommand::Texture {
202 texture,
203 bounds: sane_rect(bounds),
204 source: None,
205 });
206 }
207
208 pub fn push_sprite(&mut self, texture: GpuTexture, source: GpuRect, bounds: GpuRect) {
209 self.commands.push(GpuCommand::Texture {
210 texture,
211 bounds: sane_rect(bounds),
212 source: Some(sane_source_rect(source)),
213 });
214 }
215}
216
217#[derive(IntoElement)]
222pub struct GpuView {
223 scene: GpuScene,
224 fit: GpuFit,
225 width: Option<f32>,
226 height: Option<f32>,
227 background: Option<Hsla>,
228 pixel_snap: bool,
229}
230
231impl GpuView {
232 pub fn new(scene: GpuScene) -> Self {
233 Self {
234 scene,
235 fit: GpuFit::Contain,
236 width: None,
237 height: Some(240.0),
238 background: None,
239 pixel_snap: false,
240 }
241 }
242
243 pub fn fit(mut self, fit: GpuFit) -> Self {
244 self.fit = fit;
245 self
246 }
247
248 pub fn width(mut self, width: f32) -> Self {
249 self.width = Some(finite_positive(width));
250 self
251 }
252
253 pub fn height(mut self, height: f32) -> Self {
254 self.height = Some(finite_positive(height));
255 self
256 }
257
258 pub fn full_height(mut self) -> Self {
260 self.height = None;
261 self
262 }
263
264 pub fn background(mut self, color: Hsla) -> Self {
266 self.background = Some(color);
267 self
268 }
269
270 pub fn pixelated(mut self) -> Self {
273 self.pixel_snap = true;
274 self
275 }
276}
277
278impl RenderOnce for GpuView {
279 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
280 let background = self
281 .background
282 .unwrap_or_else(|| theme(cx).surface().hsla());
283 let scene = Arc::new(self.scene);
284 let prepaint_scene = scene.clone();
285 let fit = self.fit;
286 let pixel_snap = self.pixel_snap;
287
288 let mut surface = canvas(
289 move |_, window, cx| {
290 prepaint_scene
291 .commands
292 .iter()
293 .filter_map(|command| match command {
294 GpuCommand::Texture { texture, .. } => {
295 Some(texture.image.clone().use_render_image(window, cx))
296 }
297 GpuCommand::Quad(_) => None,
298 })
299 .collect::<Vec<Option<Arc<RenderImage>>>>()
300 },
301 move |bounds, images, window, _cx| {
302 window.paint_quad(fill(bounds, background));
303 let mut images = images.iter();
304 let transform = SceneTransform::new(
305 scene.width,
306 scene.height,
307 f32::from(bounds.size.width),
308 f32::from(bounds.size.height),
309 fit,
310 );
311 window.with_content_mask(Some(ContentMask { bounds }), |window| {
312 for command in &scene.commands {
313 match command {
314 GpuCommand::Quad(command) => {
315 let command_bounds = transform.bounds(bounds, command.bounds, pixel_snap);
316 let radius = transform.radius(command.radius, pixel_snap);
317 let border_width = transform.radius(command.border_width, pixel_snap);
318 window.paint_quad(quad(
319 command_bounds,
320 Corners::all(px(radius)),
321 command.fill,
322 Edges::all(px(border_width)),
323 command.border,
324 BorderStyle::Solid,
325 ));
326 }
327 GpuCommand::Texture {
328 bounds: destination,
329 source,
330 ..
331 } => {
332 let Some(image) = images.next().and_then(Clone::clone) else {
333 continue;
334 };
335 let image_bounds = transform.bounds(bounds, *destination, pixel_snap);
336 if let Some(source) = source {
337 let full_width = f32::from(image_bounds.size.width) / source.width;
338 let full_height = f32::from(image_bounds.size.height) / source.height;
339 let atlas_bounds = Bounds::new(
340 image_bounds.origin
341 - gpui::point(px(source.x * full_width), px(source.y * full_height)),
342 size(px(full_width), px(full_height)),
343 );
344 window.with_content_mask(
345 Some(ContentMask {
346 bounds: image_bounds,
347 }),
348 |window| {
349 let _ = window.paint_image(atlas_bounds, Corners::default(), image, 0, false);
350 },
351 );
352 } else {
353 let _ = window.paint_image(image_bounds, Corners::default(), image, 0, false);
354 }
355 }
356 }
357 }
358 });
359 },
360 )
361 .overflow_hidden();
362
363 surface = match self.height {
364 Some(height) => surface.h(px(height)),
365 None => surface.h_full(),
366 };
367
368 match self.width {
369 Some(width) => surface.w(px(width)).probe("GpuView"),
370 None => surface.w_full().probe("GpuView"),
371 }
372 }
373}
374
375#[derive(Clone, Copy, Debug, PartialEq)]
376struct SceneTransform {
377 scale_x: f32,
378 scale_y: f32,
379 offset_x: f32,
380 offset_y: f32,
381}
382
383impl SceneTransform {
384 fn new(scene_w: f32, scene_h: f32, view_w: f32, view_h: f32, fit: GpuFit) -> Self {
385 let sx = finite_positive(view_w) / finite_positive(scene_w);
386 let sy = finite_positive(view_h) / finite_positive(scene_h);
387 let (scale_x, scale_y) = match fit {
388 GpuFit::Contain => {
389 let scale = sx.min(sy);
390 (scale, scale)
391 }
392 GpuFit::Cover => {
393 let scale = sx.max(sy);
394 (scale, scale)
395 }
396 GpuFit::Stretch => (sx, sy),
397 };
398 Self {
399 scale_x,
400 scale_y,
401 offset_x: (view_w - scene_w * scale_x) * 0.5,
402 offset_y: (view_h - scene_h * scale_y) * 0.5,
403 }
404 }
405
406 fn bounds(self, viewport: Bounds<Pixels>, source: GpuRect, pixel_snap: bool) -> Bounds<Pixels> {
407 let x = self.offset_x + source.x * self.scale_x;
408 let y = self.offset_y + source.y * self.scale_y;
409 let width = source.width * self.scale_x;
410 let height = source.height * self.scale_y;
411 let snap = |value: f32| if pixel_snap { value.round() } else { value };
412 Bounds::new(
413 viewport.origin + gpui::point(px(snap(x)), px(snap(y))),
414 size(px(snap(width)), px(snap(height))),
415 )
416 }
417
418 fn radius(self, value: f32, pixel_snap: bool) -> f32 {
419 let scaled = value * self.scale_x.min(self.scale_y);
420 if pixel_snap {
421 scaled.round()
422 } else {
423 scaled
424 }
425 }
426}
427
428fn finite_positive(value: f32) -> f32 {
429 if value.is_finite() && value > 0.0 {
430 value
431 } else {
432 1.0
433 }
434}
435
436fn finite_nonnegative(value: f32) -> f32 {
437 if value.is_finite() {
438 value.max(0.0)
439 } else {
440 0.0
441 }
442}
443
444fn sane_rect(rect: GpuRect) -> GpuRect {
445 GpuRect {
446 x: if rect.x.is_finite() { rect.x } else { 0.0 },
447 y: if rect.y.is_finite() { rect.y } else { 0.0 },
448 width: finite_nonnegative(rect.width),
449 height: finite_nonnegative(rect.height),
450 }
451}
452
453fn sane_source_rect(rect: GpuRect) -> GpuRect {
454 let x = if rect.x.is_finite() {
455 rect.x.clamp(0.0, 1.0 - f32::EPSILON)
456 } else {
457 0.0
458 };
459 let y = if rect.y.is_finite() {
460 rect.y.clamp(0.0, 1.0 - f32::EPSILON)
461 } else {
462 0.0
463 };
464 let width = if rect.width.is_finite() {
465 rect.width.clamp(f32::EPSILON, 1.0 - x)
466 } else {
467 1.0 - x
468 };
469 let height = if rect.height.is_finite() {
470 rect.height.clamp(f32::EPSILON, 1.0 - y)
471 } else {
472 1.0 - y
473 };
474 GpuRect::new(x, y, width, height)
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn contain_centers_the_letterboxed_scene() {
483 let transform = SceneTransform::new(200.0, 100.0, 100.0, 100.0, GpuFit::Contain);
484 assert_eq!(transform.scale_x, 0.5);
485 assert_eq!(transform.scale_y, 0.5);
486 assert_eq!(transform.offset_x, 0.0);
487 assert_eq!(transform.offset_y, 25.0);
488 }
489
490 #[test]
491 fn cover_centers_the_clipped_scene() {
492 let transform = SceneTransform::new(200.0, 100.0, 100.0, 100.0, GpuFit::Cover);
493 assert_eq!(transform.scale_x, 1.0);
494 assert_eq!(transform.scale_y, 1.0);
495 assert_eq!(transform.offset_x, -50.0);
496 assert_eq!(transform.offset_y, 0.0);
497 }
498
499 #[test]
500 fn stretch_maps_each_axis_independently() {
501 let transform = SceneTransform::new(200.0, 100.0, 100.0, 100.0, GpuFit::Stretch);
502 assert_eq!(transform.scale_x, 0.5);
503 assert_eq!(transform.scale_y, 1.0);
504 assert_eq!(transform.offset_x, 0.0);
505 assert_eq!(transform.offset_y, 0.0);
506 }
507
508 #[test]
509 fn scene_sanitizes_non_finite_geometry() {
510 let scene = GpuScene::new(f32::NAN, 0.0).rect(
511 GpuRect::new(f32::INFINITY, 2.0, -3.0, f32::NAN),
512 gpui::black(),
513 );
514 assert_eq!(scene.size(), (1.0, 1.0));
515 let GpuCommand::Quad(command) = &scene.commands[0] else {
516 panic!("expected quad");
517 };
518 assert_eq!(command.bounds, GpuRect::new(0.0, 2.0, 0.0, 0.0));
519 }
520
521 #[test]
522 fn sprite_sources_are_clamped_to_the_texture() {
523 assert_eq!(
524 sane_source_rect(GpuRect::new(0.75, -1.0, 0.5, f32::NAN)),
525 GpuRect::new(0.75, 0.0, 0.25, 1.0)
526 );
527 }
528}