windows-canvas optimized rendering and text layout - #4756
Conversation
There was a problem hiding this comment.
Pull request overview
This PR extends the windows-reactor + windows-canvas integration to support demand-driven rendering (size-driven and state-driven invalidation) and adds a higher-level TextLayout API for measured/hit-tested text that can be shaped once and reused.
Changes:
- Add demand-driven
canvasand state-drivencanvas_invalidated(viaInvalidator) towindows-reactor’s canvas bridge. - Add
TextLayout+WordWrappingtowindows-canvas, plusDrawingSession::draw_text_layout. - Update docs, tests, and samples to demonstrate demand-driven rendering + text layout + invalidation patterns.
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| docs/crates/windows-reactor.md | Documents new demand-driven rendering APIs and recommended patterns. |
| docs/crates/windows-canvas.md | Adds docs for demand-driven rendering, TextLayout, wrapping, and sample references. |
| crates/tools/bindings/src/canvas.txt | Extends bindgen filter to include DirectWrite layout APIs and constants. |
| crates/tests/libs/canvas/src/lib.rs | Adds Rect::offset tests and new TextLayout behavior tests. |
| crates/samples/canvas/samples/src/lib.rs | Switches sample harness default to demand-driven canvas; adds run_animated. |
| crates/samples/canvas/samples/examples/transform.rs | Uses run_animated since it animates each frame. |
| crates/samples/canvas/samples/examples/text_layout.rs | New example demonstrating TextLayout measurement + demand-driven repaint. |
| crates/samples/canvas/samples/examples/invalidate.rs | New example demonstrating explicit invalidation-driven repaint. |
| crates/samples/canvas/hit_test/src/main.rs | Converts hit-test sample to invalidate-on-pointer events instead of per-frame redraw. |
| crates/samples/canvas/editor/src/main.rs | Converts editor sample to invalidate-on-state-change rendering. |
| crates/libs/reactor/src/lib.rs | Re-exports canvas, canvas_invalidated, and Invalidator under the canvas feature. |
| crates/libs/reactor/src/canvas_bridge.rs | Implements demand-vs-continuous render modes and the Invalidator hook/API. |
| crates/libs/canvas/src/types.rs | Adds Rect::offset. |
| crates/libs/canvas/src/text.rs | Adds WordWrapping, TextLayout, metrics/hit-test APIs. |
| crates/libs/canvas/src/session.rs | Adds DrawingSession::draw_text_layout. |
| crates/libs/canvas/src/bindings.rs | Generated bindings updated for DirectWrite layout + metrics/hit-test structs/methods. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 40 out of 40 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (6)
crates/libs/canvas/src/text.rs:241
hit_test_pointdiscards theHRESULTfromIDWriteTextLayout::HitTestPoint. On failure (e.g., invalid input), the returnedHitTestwill be based on default/undefined metrics. ReturningResult<HitTest>(or validating inputs / asserting success) would prevent silent incorrect results.
unsafe {
_ = self
.raw
.HitTestPoint(point.x, point.y, &mut trailing, &mut inside, &mut m);
}
crates/libs/canvas/src/text.rs:262
caret_boundsdiscards theHRESULTfromIDWriteTextLayout::HitTestTextPosition. This can fail for out-of-range UTF-16 positions; returning aRectanyway can silently produce incorrect caret geometry. Consider returningResult<Rect>(or explicitly clamping/validatingposition).
unsafe {
_ = self
.raw
.HitTestTextPosition(position, trailing, &mut x, &mut y, &mut m);
}
crates/libs/reactor/src/canvas_bridge.rs:401
CanvasImageSource::drawnow takes a callback returningResult<()>, but the doc example above still uses a closure returning()and callsunwrap()inside it. Please update the example to returnOk(())and use?for fallible brush creation so the docs match the API contract.
pub fn draw(
&self,
clear: ColorF,
f: impl FnOnce(&DrawingSession<'_>) -> Result<()>,
) -> Result<bool> {
crates/libs/reactor/src/canvas_bridge.rs:92
- The example for
animated_canvasno longer matches the function signature: the draw closure now returnsResult<()>, so the snippet should returnOk(())(and ideally use?for fallible resource creation).
This issue also appears on line 397 of the same file.
/// animated_canvas(|ctx| {
/// ctx.clear(ColorF::CORNFLOWER_BLUE);
/// ctx.fill_ellipse(&ellipse, &brush);
/// })
/// ```
crates/libs/canvas/src/text.rs:219
TextLayout::metricsignores theHRESULTfromIDWriteTextLayout::GetMetrics. If the call fails, this returns default/garbage metrics with no signal to the caller. Since this is a new public API, consider making it fallible (e.g.metrics(&self) -> Result<TextMetrics>) or at least asserting/handling failure explicitly.
This issue also appears in the following locations of the same file:
- line 237
- line 258
pub fn metrics(&self) -> TextMetrics {
let mut m = DWRITE_TEXT_METRICS::default();
unsafe { _ = self.raw.GetMetrics(&mut m) };
TextMetrics {
crates/samples/canvas/text_layout/src/main.rs:49
- The on-screen readout reports the
TextLayoutmetrics as "px", but theTextMetricsvalues are in device-independent pixels (DIPs). The unit label should match the actual units to avoid confusion.
Adds demand-driven and state-driven controls to
windows-reactor, plus aTextLayoutAPI and invalidate / text layout samples. This makes it far simpler to produce efficient text rendering and demand-driven canvas rendering in general.