Rendering · RenderSurface: host frames at the box's device pixels, redrawn on a new size or revision, freed from the atlas

This commit is contained in:
ZacharyZhang-NY committed 2026-10-03 23:22:49 -04:00
1 parent eddb13f3d7
commit 5267c44adf
6 files changed
+275 -1

No files matched your search

+1 -1
View File
@@ -179,7 +179,7 @@ Per-component lines live in `tasks/`. Tags there:
- [x] T48m Docs · E2E: the Docs routes, rail, list, previous and next, and code blocks under Playwright
- [ ] T49 Acceptance: live URL and MVP checklist
- [x] T50a Rendering · Plan: chapter 44 in the PRD, `tasks/ch44.md`, these lines
- [ ] T50b Rendering · Surface: `rendering::RenderSurface`, tested — `tasks/ch44.md`
- [x] T50b Rendering · Surface: `rendering::RenderSurface`, tested — `tasks/ch44.md`
- [ ] T50c Rendering · Gallery: a Rendering page, its story, `stories.json`; the gallery runs on Linux
- [ ] T50d Rendering · Web: the story runs live on the site from the wasm gallery, checked in a browser
- [ ] T50e Rendering · Platforms: CI builds and tests the crate on Linux, macOS and Windows, arm64 and x86_64
+1
View File
@@ -36,6 +36,7 @@ pub mod onboarding;
pub mod overlays;
pub mod primitives;
pub mod project;
pub mod rendering;
pub mod settings;
pub mod shell;
pub mod tables;
+5
View File
@@ -0,0 +1,5 @@
mod surface;
#[cfg(all(test, feature = "test-support"))]
mod tests;
pub use surface::RenderSurface;
+138
View File
@@ -0,0 +1,138 @@
use std::{rc::Rc, sync::Arc};
use gpui::{
App, Bounds, Corners, DevicePixels, ElementId, IntoElement, ParentElement, Pixels, RenderImage,
RenderOnce, Size, Styled, Window, canvas, prelude::*,
};
use web_time::Instant;
use crate::primitives::{checked_ratio, framed};
type Draw = Rc<dyn Fn(Size<DevicePixels>, &mut App) -> RenderImage>;
/// The frame on screen and what it was drawn for.
struct Drawn {
size: Size<DevicePixels>,
revision: u64,
frame: Arc<RenderImage>,
}
/// Pixels the host draws, one frame at the box's size in device pixels, in the shape `ratio` gives it. It draws again when that size or the host's revision changes, and frees the old frame from the atlas. A frame is BGRA, as gpui's `RenderImage` holds it.
#[derive(IntoElement)]
pub struct RenderSurface {
id: ElementId,
ratio: f32,
revision: u64,
draw: Draw,
}
impl RenderSurface {
/// `ratio` is the box's width over its height; `draw` makes a frame of exactly the size it is given.
pub fn new(
id: impl Into<ElementId>,
ratio: f32,
draw: impl Fn(Size<DevicePixels>, &mut App) -> RenderImage + 'static,
) -> Self {
Self {
id: id.into(),
ratio: checked_ratio(ratio),
revision: 0,
draw: Rc::new(draw),
}
}
/// Bumped by the host whenever the content changes.
pub fn revision(mut self, revision: u64) -> Self {
self.revision = revision;
self
}
}
impl RenderOnce for RenderSurface {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
// Freed from every atlas on leave.
let shown = window.use_keyed_state((self.id.clone(), "frame"), cx, |_, cx| {
cx.on_release(|shown: &mut Option<Drawn>, cx| {
if let Some(drawn) = shown.take() {
cx.drop_image(drawn.frame, None);
}
})
.detach();
None
});
let (id, revision, draw) = (self.id.clone(), self.revision, self.draw);
framed(self.ratio, cx)
.id(self.id)
.debug_selector(|| "render-surface".into())
.child(
canvas(
move |bounds, window, cx| {
let size = bounds.size.to_device_pixels(window.scale_factor());
if size.width.0 == 0 || size.height.0 == 0 {
return None;
}
let stale = shown
.read(cx)
.as_ref()
.is_none_or(|drawn| drawn.size != size || drawn.revision != revision);
if stale {
let frame = drawn(&id, size, revision, &draw, cx);
let old = shown.update(cx, |shown, _| {
shown.replace(Drawn {
size,
revision,
frame,
})
});
if let Some(old) = old {
window
.drop_image(old.frame)
.expect("the atlas frees a frame");
}
}
shown.read(cx).as_ref().map(|drawn| drawn.frame.clone())
},
|bounds: Bounds<Pixels>, frame, window, _| {
if let Some(frame) = frame {
window
.paint_image(bounds, bounds, Corners::default(), frame, 0, false)
.expect("the atlas takes the frame");
}
},
)
.absolute()
.top_0()
.left_0()
.size_full(),
)
}
}
/// The host's frame for `size`, which must match it.
fn drawn(
id: &ElementId,
size: Size<DevicePixels>,
revision: u64,
draw: &Draw,
cx: &mut App,
) -> Arc<RenderImage> {
let start = Instant::now();
let frame = draw(size, cx);
assert_eq!(
frame.frame_count(),
1,
"render surface {id}: a frame is one picture"
);
assert_eq!(
frame.size(0),
size,
"render surface {id}: the host drew the wrong size"
);
log::debug!(
"render surface {id}: drew {}x{} at revision {revision} in {:?}",
size.width.0,
size.height.0,
start.elapsed()
);
Arc::new(frame)
}
+129
View File
@@ -0,0 +1,129 @@
use std::{cell::RefCell, rc::Rc};
use gpui::{
Context, DevicePixels, IntoElement, ParentElement, Render, RenderImage, Size, Styled,
TestAppContext, VisualTestContext, Window, div, px, size,
};
use image::{Frame, RgbaImage};
use super::RenderSurface;
use crate::{layout::tests::narrow_width, theme::Theme};
type Asked = Rc<RefCell<Vec<Size<DevicePixels>>>>;
/// A surface `width` wide at 2:1, its revision set by the test.
struct Host {
width: f32,
revision: u64,
asked: Asked,
off_by: i32,
}
impl Render for Host {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
let (asked, off_by) = (self.asked.clone(), self.off_by);
div().w(px(self.width)).child(
RenderSurface::new("surface", 2.0, move |size, _| {
asked.borrow_mut().push(size);
frame(size.width.0 + off_by, size.height.0)
})
.revision(self.revision),
)
}
}
fn frame(width: i32, height: i32) -> RenderImage {
RenderImage::new(vec![Frame::new(RgbaImage::new(
width as u32,
height as u32,
))])
}
fn hosted(
cx: &mut TestAppContext,
width: f32,
off_by: i32,
) -> (gpui::Entity<Host>, &mut VisualTestContext, Asked) {
cx.update(Theme::init);
let asked = Asked::default();
let seen = asked.clone();
let (host, cx) = cx.add_window_view(move |_, _| Host {
width,
revision: 0,
asked: seen,
off_by,
});
redraw(cx);
(host, cx, asked)
}
fn redraw(cx: &mut VisualTestContext) {
cx.run_until_parked();
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
fn px_size(width: i32, height: i32) -> Size<DevicePixels> {
size(DevicePixels(width), DevicePixels(height))
}
#[gpui::test]
fn draws_once_at_the_box_size_in_device_pixels(cx: &mut TestAppContext) {
let (_, cx, asked) = hosted(cx, 200.0, 0);
assert_eq!(cx.update(|window, _| window.scale_factor()), 2.0);
redraw(cx);
redraw(cx);
assert_eq!(
*asked.borrow(),
[px_size(400, 200)],
"one frame for one size"
);
cx.update(|window, _| window.set_scale_factor(1.0));
redraw(cx);
assert_eq!(
asked.borrow().last(),
Some(&px_size(200, 100)),
"a coarser screen asks for fewer pixels"
);
}
#[gpui::test]
fn a_new_revision_or_width_draws_again(cx: &mut TestAppContext) {
let (host, cx, asked) = hosted(cx, 200.0, 0);
host.update(cx, |host, cx| {
host.revision = 1;
cx.notify();
});
redraw(cx);
host.update(cx, |host, cx| {
host.width = 120.0;
cx.notify();
});
redraw(cx);
assert_eq!(
*asked.borrow(),
[px_size(400, 200), px_size(400, 200), px_size(240, 120)]
);
}
#[gpui::test]
fn an_empty_box_asks_for_nothing(cx: &mut TestAppContext) {
let (_, _, asked) = hosted(cx, 0.0, 0);
assert!(asked.borrow().is_empty(), "no frame for no pixels");
}
#[gpui::test]
#[should_panic(expected = "the host drew the wrong size")]
fn a_frame_of_the_wrong_size_fails(cx: &mut TestAppContext) {
hosted(cx, 200.0, 1);
}
#[gpui::test]
fn the_surface_fills_its_column(cx: &mut TestAppContext) {
cx.update(Theme::init);
let width = narrow_width(cx, "render-surface", |_, _| {
RenderSurface::new("surface", 2.0, |size, _| frame(size.width.0, size.height.0))
.into_any_element()
});
assert_eq!(width, px(240.0));
}
+1
View File
@@ -352,3 +352,4 @@
| G3 | 1 | FAIL | RangeSlider thumbs reported the whole track, not the bounds the other thumb leaves |
| G3 | 2 | PASS | |
| T50a | 1 | PASS | codex gpt-6.1-sol max |
| T50b | 1 | PASS | |