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:
1 parent
61d2378ae2
commit
a538092e99
6 files changed
+275
-1
No files matched your search
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
mod surface;
|
||||
#[cfg(all(test, feature = "test-support"))]
|
||||
mod tests;
|
||||
|
||||
pub use surface::RenderSurface;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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 | |
|
||||
Reference in new issue
Block a user