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
eddb13f3d7
commit
5267c44adf
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
|
- [x] T48m Docs · E2E: the Docs routes, rail, list, previous and next, and code blocks under Playwright
|
||||||
- [ ] T49 Acceptance: live URL and MVP checklist
|
- [ ] T49 Acceptance: live URL and MVP checklist
|
||||||
- [x] T50a Rendering · Plan: chapter 44 in the PRD, `tasks/ch44.md`, these lines
|
- [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
|
- [ ] 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
|
- [ ] 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
|
- [ ] 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 overlays;
|
||||||
pub mod primitives;
|
pub mod primitives;
|
||||||
pub mod project;
|
pub mod project;
|
||||||
|
pub mod rendering;
|
||||||
pub mod settings;
|
pub mod settings;
|
||||||
pub mod shell;
|
pub mod shell;
|
||||||
pub mod tables;
|
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 | 1 | FAIL | RangeSlider thumbs reported the whole track, not the bounds the other thumb leaves |
|
||||||
| G3 | 2 | PASS | |
|
| G3 | 2 | PASS | |
|
||||||
| T50a | 1 | PASS | codex gpt-6.1-sol max |
|
| T50a | 1 | PASS | codex gpt-6.1-sol max |
|
||||||
|
| T50b | 1 | PASS | |
|
||||||
Reference in new issue
Block a user