Keep the GLES path when the scale arrives first, stop re-rendering what does not change, and let the compositor skip what the surface covers
Measured on a Librem 5 (Cortex-A53, GC7000Lite, 720×1440 at scale 2) with a frame trace on both sides of the socket: the shell's main surface repainted in 110 to 136 ms, quick settings drew at 20 fps with the GPU saturated, and every panel open uploaded its wallpaper and icons again. Most of it was the toolkit doing work the frame did not need. Backend choice. When the compositor announces the output scale before the first `configure`, `scale_factor_changed` allocated an SHM pool for the surface and `on_configure` took that pool as the sign that the software path had been chosen, locking the surface to tiny-skia for its whole life. Every ltk main surface on a scale-2 phone ran in software while its overlays, which inherit the scale from the main and never receive that event, ran on the GPU. The scale handler now only reallocates a pool that already exists; before the first configure neither path is chosen. Wallpapers. `WallpaperBundle::for_size` returned the crop at the source resolution, a 1103×2160 slice of a 4K image for a 720×1440 panel, 10 MB uploaded per texture and a hundred milliseconds each time. The crop is now downscaled to the surface (bilinear, once per size, cached), also for landscape surfaces smaller than the source. Viewport. `Element::Viewport` created a fresh sub-canvas every frame, on GLES a framebuffer, a 2048² glyph atlas and empty image and glyph caches, so a panel clipped to a height that animates re-uploaded its wallpaper and re-rasterised every glyph on every frame. Without a bottom fade the clip is now a scissor on the parent canvas (a mask on the software backend): no offscreen pass, no blit, and the child's textures stay in the parent's caches. `local_viewport` is honoured through a temporary override of the layout viewport. Viewports with a fade keep their sub-canvas, which, like the scroll sub-canvases, is now kept across frames and resized in place instead of reallocated when its size changes. Caches. The image texture cache moves from the canvas to the GL context: every canvas and sub-canvas shares one content-keyed LRU (32 MiB), so an overlay or subsurface that is destroyed and recreated finds its images resident. A GLES canvas dropped with an overlay is pooled in `AppData::canvas_pool` and handed to the next overlay, with its compiled programs and glyph atlas, which took 130 to 290 ms to build on that GPU. Subsurface slots keep their scroll and viewport sub-canvases between rasters. Surfaces. A `container.surface()` with shadows costs a Gaussian drop-shadow pass, a fill and one pass per inset shadow, all full-quad fragment shaders; a dozen glass cards were 40 ms of GPU per frame for chrome that never changes. `Canvas::fill_surface` on GLES now renders the chrome once per size, corners and style into a texture held by the context cache (`gles_render/surface_cache.rs`, 16 MiB LRU) and blits it, drawing directly only for surfaces with `PlusLighter` or `Overlay` insets, which depend on the backdrop. Opaque regions. ltk never called `wl_surface.set_opaque_region`, so the compositor blended every ltk surface and repainted everything underneath a full-screen homescreen. A surface whose `background_color` is opaque now declares itself opaque, and the new `App::overlay_opaque_region( id )` lets an overlay declare the physical rects it paints opaque, such as a panel column over an opaque wallpaper. `sync_opaque_region` sends the rects only when they change. Frame pacing. `eglSwapBuffers` blocked 13 to 37 ms per frame waiting for Mesa's own throttle callback; ltk paces itself on `wl_surface.frame`, so `make_current` sets the swap interval to 0. A subsurface move is now applied only when its parent has no frame callback outstanding, and the parent commit that applies it requests the next callback: a drag commits once per vblank with the latest finger position instead of once per input sample, without the app throttling samples itself. While the app reports `subsurface_motion_only`, the main's callback, and the callback of an overlay that hosts a subsurface, no longer force a full repaint; they mark the surface and the reconcile keeps the cadence with a bare commit when nothing moved. Swipe releases arm a settle the same way instead of dirtying every cache. Two hooks, `on_overlay_frame_committed` and `on_subsurface_committed`, let an app hand a panel over between its host and a sliding subsurface on real commits rather than loop ticks. Lines. A diagonal `draw_line` on GLES stamped one `fill_rect` per pixel of length, 190 draw calls for a spinner and over 2000 for a pattern-lock stroke; it is now a single rotated quad through the rect shader, with round caps and antialiased edges. A pixel test covers the scissor viewport, `for_size` gets a downscale test, and `docs/architecture.md`, `docs/widgets.md` and `docs/backends.md` describe the new hook, the viewport paths, the shared caches and the line rasteriser.
This commit is contained in:
@@ -83,7 +83,8 @@ In practice, that model is easiest to adopt in three steps:
|
|||||||
|
|
||||||
- `shell_mode()` → `ShellMode::Layer( Layer::Background | Bottom | Top | Overlay )`.
|
- `shell_mode()` → `ShellMode::Layer( Layer::Background | Bottom | Top | Overlay )`.
|
||||||
- `layer_anchor()`, `layer_size()`, `exclusive_zone()`, `keyboard_exclusive()` — the layer-shell knobs.
|
- `layer_anchor()`, `layer_size()`, `exclusive_zone()`, `keyboard_exclusive()` — the layer-shell knobs.
|
||||||
- `background_color()` → `Color::rgba( 0, 0, 0, 0 )` for transparent surfaces (panels, OSDs).
|
- `background_color()` → `Color::rgba( 0, 0, 0, 0 )` for transparent surfaces (panels, OSDs). An opaque colour also declares the whole surface opaque to the compositor, which then neither blends it nor repaints what lies underneath — return one whenever the view covers the surface (a wallpaper-backed homescreen).
|
||||||
|
- `overlay_opaque_region( id )` → the physical rects of an overlay that are fully opaque this frame (a panel column painted over an opaque wallpaper), for the same compositor savings on a surface whose background is transparent. `None` (default) declares nothing.
|
||||||
|
|
||||||
**Implement when external state matters**
|
**Implement when external state matters**
|
||||||
|
|
||||||
@@ -282,7 +283,9 @@ The cheap things and the expensive things, in rough order:
|
|||||||
- *Avoid in `view()`*: cloning large `Vec<u8>` image buffers. `img_widget` takes an `Arc<Vec<u8>>`; build the `Arc` once at load time and clone *the Arc*, not the bytes.
|
- *Avoid in `view()`*: cloning large `Vec<u8>` image buffers. `img_widget` takes an `Arc<Vec<u8>>`; build the `Arc` once at load time and clone *the Arc*, not the bytes.
|
||||||
- *Avoid `is_animating() = true` when nothing is moving.* It pegs the loop at the full animation rate and burns battery on the mobile target.
|
- *Avoid `is_animating() = true` when nothing is moving.* It pegs the loop at the full animation rate and burns battery on the mobile target.
|
||||||
- *Lower `poll_interval()` is not free.* Crustace polls every 30 s because the clock only shows HH:MM. If your UI shows seconds, `Some(Duration::from_secs(1))` is fine; if it shows nothing time-sensitive, leave it `None`.
|
- *Lower `poll_interval()` is not free.* Crustace polls every 30 s because the clock only shows HH:MM. If your UI shows seconds, `Some(Duration::from_secs(1))` is fine; if it shows nothing time-sensitive, leave it `None`.
|
||||||
- *Scroll viewports own a sub-canvas.* They're slightly more expensive to draw than a plain column. Use them when you need clipping or actual scrolling, not as a wrapper.
|
- *Scroll viewports own a sub-canvas.* They're slightly more expensive to draw than a plain column. Use them when you need clipping or actual scrolling, not as a wrapper. A plain `viewport` without `fade_bottom` is cheaper: it clips with a scissor on the parent canvas and adds no offscreen pass, so it is the right wrapper for a panel whose visible height animates.
|
||||||
|
- *Themed surfaces with shadows are pre-rendered on GLES.* A `container.surface(...)` whose slot carries outer or inset shadows (all with `Normal` blend) is drawn once per (size, corners, style) into a texture held by the context's cache and blitted afterwards, so a panel of a dozen glass cards costs a dozen textured quads per frame instead of six shader passes per card. Sliders and surfaces with `PlusLighter` / `Overlay` insets keep the direct path.
|
||||||
|
- *Image textures are cached per GL context, not per canvas.* On the GLES backend every canvas and sub-canvas of a process shares one content-keyed texture cache (32 MiB, LRU), so an overlay or subsurface that is destroyed and recreated finds its icons and wallpaper resident; destroyed overlay canvases are pooled and reused too, shaders and glyph atlas included. Keep image buffers stable (`Arc` clones) so the fingerprint hits.
|
||||||
- *GPU vs software*: the GLES path is selected automatically when EGL is available, with no API-level difference for the application. The two backends are not yet pixel-identical — gradients and the shadow / backdrop pipeline degrade under software; the authoritative list of gaps is the README's *Backend Differences* section.
|
- *GPU vs software*: the GLES path is selected automatically when EGL is available, with no API-level difference for the application. The two backends are not yet pixel-identical — gradients and the shadow / backdrop pipeline degrade under software; the authoritative list of gaps is the README's *Backend Differences* section.
|
||||||
|
|
||||||
When a redraw feels sluggish: add a one-line print at the top of `view()` and confirm it's not being called more often than expected. The single most common mistake is leaving `is_animating()` returning `true` after the animation finished.
|
When a redraw feels sluggish: add a one-line print at the top of `view()` and confirm it's not being called more often than expected. The single most common mistake is leaving `is_animating()` returning `true` after the animation finished.
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ The two backends are kept at visual parity for the common primitives — solid f
|
|||||||
|
|
||||||
| Capability | Software | GLES | Notes |
|
| Capability | Software | GLES | Notes |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| Solid fills, strokes, lines (`fill_rect`, `stroke_rect`, `draw_line`) | Yes | Yes | Visual parity. |
|
| Solid fills, strokes, lines (`fill_rect`, `stroke_rect`, `draw_line`) | Yes | Yes | Visual parity. A diagonal `draw_line` is one rotated quad on GLES (antialiased, round caps); software strokes a path. |
|
||||||
| Text (`draw_text`, `draw_text_with_font`) | Yes | Yes | Both snap pen positions to integer pixels; glyph caches differ (per-canvas hashmap with eviction vs. shared atlas texture) but output is at parity. |
|
| Text (`draw_text`, `draw_text_with_font`) | Yes | Yes | Both snap pen positions to integer pixels; glyph caches differ (per-canvas hashmap with eviction vs. shared atlas texture) but output is at parity. |
|
||||||
| Images (`draw_image_data`) | Yes | Yes | Both snap destinations to integer pixels. GLES caches uploaded textures content-keyed, bounded to 32 MiB with LRU eviction; software draws directly from the buffer. |
|
| Images (`draw_image_data`) | Yes | Yes | Both snap destinations to integer pixels. GLES caches uploaded textures content-keyed, bounded to 32 MiB with LRU eviction; software draws directly from the buffer. |
|
||||||
| External GL texture (`draw_external_texture`, `External::texture`) | No-op | Yes | Software has no GL state to sample from. Use `External::cpu` for a backend-independent immediate-mode draw hook. |
|
| External GL texture (`draw_external_texture`, `External::texture`) | No-op | Yes | Software has no GL state to sample from. Use `External::cpu` for a backend-independent immediate-mode draw hook. |
|
||||||
|
|||||||
+3
-1
@@ -644,7 +644,9 @@ viewport( panel_view )
|
|||||||
# }
|
# }
|
||||||
```
|
```
|
||||||
|
|
||||||
By default the clip's sub-canvas inherits the root layout viewport, so viewport-relative (`vw` / `vh` / `vmin`) and fluid `Length`s inside resolve exactly as they would outside the clip — the right behaviour for scroll-like clipping. `local_viewport()` opts out: the child resolves those `Length`s against the viewport's own rect instead. Use it for a fixed-size floating mini-UI — a phone-shaped panel pinned to a corner of a desktop-wide surface — whose content is calibrated against the panel rect and must not scale with the surface hosting it.
|
By default the clip inherits the root layout viewport, so viewport-relative (`vw` / `vh` / `vmin`) and fluid `Length`s inside resolve exactly as they would outside the clip — the right behaviour for scroll-like clipping. `local_viewport()` opts out: the child resolves those `Length`s against the viewport's own rect instead. Use it for a fixed-size floating mini-UI — a phone-shaped panel pinned to a corner of a desktop-wide surface — whose content is calibrated against the panel rect and must not scale with the surface hosting it.
|
||||||
|
|
||||||
|
Without `fade_bottom` the clip is a plain scissor (a mask on the software backend) on the parent canvas: no offscreen pass, no blit, and the child's images and glyphs stay in the parent's caches. A viewport whose height changes every frame — a panel sliding in — therefore costs no more than drawing its child. With `fade_bottom` the child renders into a sub-canvas that is kept across frames and resized in place when the viewport's size changes.
|
||||||
|
|
||||||
**See also**: the slide-in panel recipe in
|
**See also**: the slide-in panel recipe in
|
||||||
[`docs/cookbook.md`](./cookbook.md#slide-in-panel).
|
[`docs/cookbook.md`](./cookbook.md#slide-in-panel).
|
||||||
|
|||||||
+14
@@ -568,6 +568,15 @@ pub trait App: 'static
|
|||||||
/// the main frame that replaces it has been sent.
|
/// the main frame that replaces it has been sent.
|
||||||
fn on_main_frame_committed( &mut self ) {}
|
fn on_main_frame_committed( &mut self ) {}
|
||||||
|
|
||||||
|
/// Called every time a buffer for the overlay `id` has been rendered and
|
||||||
|
/// committed.
|
||||||
|
fn on_overlay_frame_committed( &mut self, _id: OverlayId ) {}
|
||||||
|
|
||||||
|
/// Called every time the content of the subsurface `id` has been
|
||||||
|
/// rasterised and committed (first raster, resize or a new
|
||||||
|
/// `content_version`), before the parent commit that maps it.
|
||||||
|
fn on_subsurface_committed( &mut self, _id: SubsurfaceId ) {}
|
||||||
|
|
||||||
/// Called when the surface is asked to close (compositor request, titlebar button,
|
/// Called when the surface is asked to close (compositor request, titlebar button,
|
||||||
/// layer-shell closed event). Return `true` to allow the application to exit,
|
/// layer-shell closed event). Return `true` to allow the application to exit,
|
||||||
/// or `false` to cancel.
|
/// or `false` to cancel.
|
||||||
@@ -953,6 +962,11 @@ pub trait App: 'static
|
|||||||
/// Return `None` (default) to receive input everywhere.
|
/// Return `None` (default) to receive input everywhere.
|
||||||
fn input_region( &self ) -> Option<Vec<crate::types::Rect>> { None }
|
fn input_region( &self ) -> Option<Vec<crate::types::Rect>> { None }
|
||||||
|
|
||||||
|
/// Rects (physical layout pixels) of overlay `id` that are fully opaque
|
||||||
|
/// this frame, so the compositor can skip blending them and repainting
|
||||||
|
/// what lies underneath. `None` (default) declares nothing opaque.
|
||||||
|
fn overlay_opaque_region( &self, _id: OverlayId ) -> Option<Vec<crate::types::Rect>> { None }
|
||||||
|
|
||||||
/// Return `Some(( title, app_id ))` to force an XDG toplevel window instead of
|
/// Return `Some(( title, app_id ))` to force an XDG toplevel window instead of
|
||||||
/// layer-shell overlay. The compositor will display the title in the title bar
|
/// layer-shell overlay. The compositor will display the title in the title bar
|
||||||
/// and use the app_id for taskbar/icon matching. Return `None` (default) to
|
/// and use the app_id for taskbar/icon matching. Return `None` (default) to
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ pub struct UiSurface<Msg: Clone>
|
|||||||
selection_anchor: HashMap<usize, usize>,
|
selection_anchor: HashMap<usize, usize>,
|
||||||
scroll_offsets: HashMap<usize, ( f32, f32 )>,
|
scroll_offsets: HashMap<usize, ( f32, f32 )>,
|
||||||
scroll_canvases: HashMap<usize, Canvas>,
|
scroll_canvases: HashMap<usize, Canvas>,
|
||||||
|
viewport_canvases: HashMap<usize, Canvas>,
|
||||||
scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
|
scroll_navigable_items: HashMap<usize, Vec<( usize, f32, f32 )>>,
|
||||||
content_dirty: bool,
|
content_dirty: bool,
|
||||||
}
|
}
|
||||||
@@ -228,6 +229,7 @@ impl<Msg: Clone> UiSurface<Msg>
|
|||||||
selection_anchor: HashMap::new(),
|
selection_anchor: HashMap::new(),
|
||||||
scroll_offsets: HashMap::new(),
|
scroll_offsets: HashMap::new(),
|
||||||
scroll_canvases: HashMap::new(),
|
scroll_canvases: HashMap::new(),
|
||||||
|
viewport_canvases: HashMap::new(),
|
||||||
scroll_navigable_items: HashMap::new(),
|
scroll_navigable_items: HashMap::new(),
|
||||||
content_dirty: true,
|
content_dirty: true,
|
||||||
}
|
}
|
||||||
@@ -401,6 +403,8 @@ impl<Msg: Clone> UiSurface<Msg>
|
|||||||
scroll_offsets: std::mem::take( &mut self.scroll_offsets ),
|
scroll_offsets: std::mem::take( &mut self.scroll_offsets ),
|
||||||
scroll_rects: Vec::new(),
|
scroll_rects: Vec::new(),
|
||||||
scroll_canvases: std::mem::take( &mut self.scroll_canvases ),
|
scroll_canvases: std::mem::take( &mut self.scroll_canvases ),
|
||||||
|
viewport_canvases: std::mem::take( &mut self.viewport_canvases ),
|
||||||
|
viewport_seq: 0,
|
||||||
scroll_navigable_items: std::mem::take( &mut self.scroll_navigable_items ),
|
scroll_navigable_items: std::mem::take( &mut self.scroll_navigable_items ),
|
||||||
scroll_clipped: Vec::new(),
|
scroll_clipped: Vec::new(),
|
||||||
previous_widget_rects: self.widget_rects.clone(),
|
previous_widget_rects: self.widget_rects.clone(),
|
||||||
@@ -450,6 +454,7 @@ impl<Msg: Clone> UiSurface<Msg>
|
|||||||
// getter together with a `scroll_by( flat_idx, dy )` mutator and start
|
// getter together with a `scroll_by( flat_idx, dy )` mutator and start
|
||||||
// keeping the field again.
|
// keeping the field again.
|
||||||
self.scroll_canvases = ctx.scroll_canvases;
|
self.scroll_canvases = ctx.scroll_canvases;
|
||||||
|
self.viewport_canvases = ctx.viewport_canvases;
|
||||||
self.scroll_navigable_items = ctx.scroll_navigable_items;
|
self.scroll_navigable_items = ctx.scroll_navigable_items;
|
||||||
self.content_dirty = false;
|
self.content_dirty = false;
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,47 @@ use smithay_client_toolkit::reexports::client::protocol::wl_surface::WlSurface;
|
|||||||
use crate::render::Canvas;
|
use crate::render::Canvas;
|
||||||
use crate::types::{ Color, Rect };
|
use crate::types::{ Color, Rect };
|
||||||
|
|
||||||
|
/// Declare the opaque part of the surface: the whole of it when the
|
||||||
|
/// background is opaque, else the app's `region` (physical rects), so the
|
||||||
|
/// compositor can skip blending it and repainting what lies underneath.
|
||||||
|
/// `applied` remembers the rects last sent.
|
||||||
|
pub( crate ) fn sync_opaque_region(
|
||||||
|
wl_surface: &WlSurface,
|
||||||
|
compositor: &CompositorState,
|
||||||
|
applied: &mut Option<Vec<Rect>>,
|
||||||
|
bg: Color,
|
||||||
|
region: Option<&[Rect]>,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
scale: u32,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
let s = scale.max( 1 ) as f32;
|
||||||
|
let rects: Vec<Rect> = if bg.a >= 1.0
|
||||||
|
{
|
||||||
|
vec![ Rect { x: 0.0, y: 0.0, width: width as f32 * s, height: height as f32 * s } ]
|
||||||
|
} else {
|
||||||
|
region.map( |r| r.to_vec() ).unwrap_or_default()
|
||||||
|
};
|
||||||
|
if applied.as_ref() == Some( &rects ) { return; }
|
||||||
|
if rects.is_empty()
|
||||||
|
{
|
||||||
|
wl_surface.set_opaque_region( None );
|
||||||
|
} else if let Ok( wl_region ) = Region::new( compositor ) {
|
||||||
|
for r in &rects
|
||||||
|
{
|
||||||
|
wl_region.add(
|
||||||
|
( r.x / s ).round() as i32,
|
||||||
|
( r.y / s ).round() as i32,
|
||||||
|
( r.width / s ).round() as i32,
|
||||||
|
( r.height / s ).round() as i32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
wl_surface.set_opaque_region( Some( wl_region.wl_region() ) );
|
||||||
|
}
|
||||||
|
*applied = Some( rects );
|
||||||
|
}
|
||||||
|
|
||||||
/// Paint the client-side title bar (close button, divider, title text).
|
/// Paint the client-side title bar (close button, divider, title text).
|
||||||
///
|
///
|
||||||
/// Returns the close button rect in physical pixels so the caller can
|
/// Returns the close button rect in physical pixels so the caller can
|
||||||
|
|||||||
+5
-1
@@ -31,7 +31,7 @@ use crate::types::{ Color, Rect };
|
|||||||
use crate::widget::Element;
|
use crate::widget::Element;
|
||||||
|
|
||||||
use super::{ build_draw_ctx, commit_draw_ctx, layout_and_draw };
|
use super::{ build_draw_ctx, commit_draw_ctx, layout_and_draw };
|
||||||
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar };
|
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar, sync_opaque_region };
|
||||||
|
|
||||||
/// GPU full-redraw path. Mirrors [`super::software::draw_surface_full`]
|
/// GPU full-redraw path. Mirrors [`super::software::draw_surface_full`]
|
||||||
/// but writes into the EGL surface's persistent FBO instead of an SHM
|
/// but writes into the EGL surface's persistent FBO instead of an SHM
|
||||||
@@ -51,6 +51,7 @@ pub( crate ) fn draw_surface_full_gpu<Msg: Clone>(
|
|||||||
view: &Element<Msg>,
|
view: &Element<Msg>,
|
||||||
bg: Color,
|
bg: Color,
|
||||||
input_region: Option<&[Rect]>,
|
input_region: Option<&[Rect]>,
|
||||||
|
opaque_region: Option<&[Rect]>,
|
||||||
debug_layout: bool,
|
debug_layout: bool,
|
||||||
pw: u32,
|
pw: u32,
|
||||||
ph: u32,
|
ph: u32,
|
||||||
@@ -114,6 +115,7 @@ pub( crate ) fn draw_surface_full_gpu<Msg: Clone>(
|
|||||||
ss.content_dirty = false;
|
ss.content_dirty = false;
|
||||||
|
|
||||||
let wl_surface = ss.surface.wl_surface();
|
let wl_surface = ss.surface.wl_surface();
|
||||||
|
sync_opaque_region( wl_surface, compositor, &mut ss.opaque_full, bg, opaque_region, ss.width, ss.height, scale );
|
||||||
apply_input_region( wl_surface, compositor, input_region, scale );
|
apply_input_region( wl_surface, compositor, input_region, scale );
|
||||||
// Frame callback request must precede the implicit commit done by
|
// Frame callback request must precede the implicit commit done by
|
||||||
// `swap_buffers_with_damage`, so the compositor can attach it to this
|
// `swap_buffers_with_damage`, so the compositor can attach it to this
|
||||||
@@ -138,6 +140,7 @@ pub( crate ) fn draw_surface_partial_gpu<Msg: Clone>(
|
|||||||
view: &Element<Msg>,
|
view: &Element<Msg>,
|
||||||
bg: Color,
|
bg: Color,
|
||||||
input_region: Option<&[Rect]>,
|
input_region: Option<&[Rect]>,
|
||||||
|
opaque_region: Option<&[Rect]>,
|
||||||
dirty_rects: Vec<Rect>,
|
dirty_rects: Vec<Rect>,
|
||||||
pw: u32,
|
pw: u32,
|
||||||
ph: u32,
|
ph: u32,
|
||||||
@@ -187,6 +190,7 @@ pub( crate ) fn draw_surface_partial_gpu<Msg: Clone>(
|
|||||||
ss.content_dirty = false;
|
ss.content_dirty = false;
|
||||||
|
|
||||||
let wl_surface = ss.surface.wl_surface();
|
let wl_surface = ss.surface.wl_surface();
|
||||||
|
sync_opaque_region( wl_surface, compositor, &mut ss.opaque_full, bg, opaque_region, ss.width, ss.height, scale );
|
||||||
apply_input_region( wl_surface, compositor, input_region, scale );
|
apply_input_region( wl_surface, compositor, input_region, scale );
|
||||||
request_frame( wl_surface );
|
request_frame( wl_surface );
|
||||||
// Convert top-left dirty rects to EGL bottom-left coords for the
|
// Convert top-left dirty rects to EGL bottom-left coords for the
|
||||||
|
|||||||
+66
-2
@@ -259,7 +259,7 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
|
|||||||
let sw = (rect.width.ceil() as u32).max( 1 );
|
let sw = (rect.width.ceil() as u32).max( 1 );
|
||||||
let sh = (rect.height.ceil() as u32).max( 1 );
|
let sh = (rect.height.ceil() as u32).max( 1 );
|
||||||
let mut sub = ctx.scroll_canvases.remove( &my_idx )
|
let mut sub = ctx.scroll_canvases.remove( &my_idx )
|
||||||
.filter( |c| c.size() == ( sw, sh ) )
|
.map( |mut c| { if c.size() != ( sw, sh ) { c.resize( sw, sh ); } c } )
|
||||||
.unwrap_or_else( || canvas.sub_canvas( sw, sh ) );
|
.unwrap_or_else( || canvas.sub_canvas( sw, sh ) );
|
||||||
sub.clear();
|
sub.clear();
|
||||||
|
|
||||||
@@ -351,13 +351,76 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
|
|||||||
|
|
||||||
next_idx
|
next_idx
|
||||||
}
|
}
|
||||||
|
// Without a fade the clip is a plain scissor on the same canvas: no
|
||||||
|
// offscreen pass, no blit, and the child's textures stay in the
|
||||||
|
// parent's caches.
|
||||||
|
Element::Viewport( v ) if v.fade_bottom <= 0.0 =>
|
||||||
|
{
|
||||||
|
let child_h = v.child.preferred_size( rect.width, canvas ).1;
|
||||||
|
let effective_h = child_h.max( rect.height );
|
||||||
|
let saved_clip = canvas.clip_bounds();
|
||||||
|
let clip = if saved_clip.is_empty()
|
||||||
|
{
|
||||||
|
rect
|
||||||
|
} else {
|
||||||
|
let ( mut x0, mut y0, mut x1, mut y1 ) = ( f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY );
|
||||||
|
for r in &saved_clip
|
||||||
|
{
|
||||||
|
x0 = x0.min( r.x );
|
||||||
|
y0 = y0.min( r.y );
|
||||||
|
x1 = x1.max( r.x + r.width );
|
||||||
|
y1 = y1.max( r.y + r.height );
|
||||||
|
}
|
||||||
|
clamp_rect_to( rect, Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 } )
|
||||||
|
};
|
||||||
|
canvas.set_clip_rects( &[ clip ] );
|
||||||
|
let prev_viewport = if v.local_viewport
|
||||||
|
{
|
||||||
|
canvas.layout_viewport_override( Some( ( rect.width, rect.height ) ) )
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let child_rect = Rect { x: rect.x, y: rect.y, width: rect.width, height: effective_h };
|
||||||
|
let rects_before = ctx.widget_rects.len();
|
||||||
|
let scroll_rects_before = ctx.scroll_rects.len();
|
||||||
|
let next_idx = layout_and_draw::<Msg>( v.child.as_ref(), canvas, child_rect, ctx, flat_idx );
|
||||||
|
|
||||||
|
if v.local_viewport { canvas.layout_viewport_override( prev_viewport ); }
|
||||||
|
canvas.set_clip_rects( &saved_clip );
|
||||||
|
|
||||||
|
let new_rects: Vec<LaidOutWidget<Msg>> = ctx.widget_rects.drain( rects_before.. ).collect();
|
||||||
|
for mut w in new_rects
|
||||||
|
{
|
||||||
|
w.paint_rect = clamp_rect_to( w.paint_rect, rect );
|
||||||
|
if w.rect.y + w.rect.height > rect.y && w.rect.y < rect.y + rect.height
|
||||||
|
{
|
||||||
|
ctx.widget_rects.push( w );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let new_scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )> =
|
||||||
|
ctx.scroll_rects.drain( scroll_rects_before.. ).collect();
|
||||||
|
for ( r, idx, ax ) in new_scroll_rects
|
||||||
|
{
|
||||||
|
let clamped = clamp_rect_to( r, rect );
|
||||||
|
if clamped.width > 0.0 && clamped.height > 0.0
|
||||||
|
{
|
||||||
|
ctx.scroll_rects.push( ( clamped, idx, ax ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next_idx
|
||||||
|
}
|
||||||
Element::Viewport( v ) =>
|
Element::Viewport( v ) =>
|
||||||
{
|
{
|
||||||
let child_h = v.child.preferred_size( rect.width, canvas ).1;
|
let child_h = v.child.preferred_size( rect.width, canvas ).1;
|
||||||
let effective_h = child_h.max( rect.height );
|
let effective_h = child_h.max( rect.height );
|
||||||
let vw = ( rect.width.ceil() as u32 ).max( 1 );
|
let vw = ( rect.width.ceil() as u32 ).max( 1 );
|
||||||
let vh = ( rect.height.ceil() as u32 ).max( 1 );
|
let vh = ( rect.height.ceil() as u32 ).max( 1 );
|
||||||
let mut sub = canvas.sub_canvas( vw, vh );
|
let seq = ctx.viewport_seq;
|
||||||
|
ctx.viewport_seq += 1;
|
||||||
|
let mut sub = ctx.viewport_canvases.remove( &seq )
|
||||||
|
.map( |mut c| { if c.size() != ( vw, vh ) { c.resize( vw, vh ); } c } )
|
||||||
|
.unwrap_or_else( || canvas.sub_canvas( vw, vh ) );
|
||||||
if v.local_viewport { sub.set_local_layout_viewport(); }
|
if v.local_viewport { sub.set_local_layout_viewport(); }
|
||||||
sub.clear();
|
sub.clear();
|
||||||
|
|
||||||
@@ -397,6 +460,7 @@ pub( crate ) fn layout_and_draw<Msg: Clone>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvas.blit_fade_bottom( &sub, rect.x as i32, rect.y as i32, v.fade_bottom );
|
canvas.blit_fade_bottom( &sub, rect.x as i32, rect.y as i32, v.fade_bottom );
|
||||||
|
ctx.viewport_canvases.insert( seq, sub );
|
||||||
next_idx
|
next_idx
|
||||||
}
|
}
|
||||||
Element::RichText( r ) =>
|
Element::RichText( r ) =>
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ pub( crate ) struct DrawCtx<Msg: Clone>
|
|||||||
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
|
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
|
||||||
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
|
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
|
||||||
pub scroll_canvases: HashMap<usize, Canvas>,
|
pub scroll_canvases: HashMap<usize, Canvas>,
|
||||||
|
/// Sub-canvases of `Viewport` elements, keyed by their order of
|
||||||
|
/// appearance in the walk (a viewport owns no flat index of its own).
|
||||||
|
pub viewport_canvases: HashMap<usize, Canvas>,
|
||||||
|
pub viewport_seq: usize,
|
||||||
/// Per-scroll navigation map: list of `(flat_idx, content_y, height)`
|
/// Per-scroll navigation map: list of `(flat_idx, content_y, height)`
|
||||||
/// for every interactive item the scroll's child laid out, in
|
/// for every interactive item the scroll's child laid out, in
|
||||||
/// document order, **including items currently scrolled off-screen**.
|
/// document order, **including items currently scrolled off-screen**.
|
||||||
@@ -120,6 +124,8 @@ pub( crate ) fn build_draw_ctx<Msg: Clone>(
|
|||||||
scroll_offsets: std::mem::take( &mut frame.scroll_offsets ),
|
scroll_offsets: std::mem::take( &mut frame.scroll_offsets ),
|
||||||
scroll_rects: Vec::new(),
|
scroll_rects: Vec::new(),
|
||||||
scroll_canvases: std::mem::take( &mut frame.scroll_canvases ),
|
scroll_canvases: std::mem::take( &mut frame.scroll_canvases ),
|
||||||
|
viewport_canvases: std::mem::take( &mut frame.viewport_canvases ),
|
||||||
|
viewport_seq: 0,
|
||||||
scroll_navigable_items: std::mem::take( &mut frame.scroll_navigable_items ),
|
scroll_navigable_items: std::mem::take( &mut frame.scroll_navigable_items ),
|
||||||
scroll_clipped: Vec::new(),
|
scroll_clipped: Vec::new(),
|
||||||
previous_widget_rects: frame.widget_rects.clone(),
|
previous_widget_rects: frame.widget_rects.clone(),
|
||||||
@@ -148,6 +154,7 @@ pub( crate ) fn commit_draw_ctx<Msg: Clone>(
|
|||||||
frame.widget_rects = ctx.widget_rects;
|
frame.widget_rects = ctx.widget_rects;
|
||||||
frame.scroll_rects = ctx.scroll_rects;
|
frame.scroll_rects = ctx.scroll_rects;
|
||||||
frame.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases );
|
frame.scroll_canvases = std::mem::take( &mut ctx.scroll_canvases );
|
||||||
|
frame.viewport_canvases = std::mem::take( &mut ctx.viewport_canvases );
|
||||||
frame.cursor_state = ctx.cursor_state;
|
frame.cursor_state = ctx.cursor_state;
|
||||||
frame.selection_anchor = ctx.selection_anchor;
|
frame.selection_anchor = ctx.selection_anchor;
|
||||||
frame.scroll_offsets = ctx.scroll_offsets;
|
frame.scroll_offsets = ctx.scroll_offsets;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ use crate::types::{ Color, Rect };
|
|||||||
use crate::widget::Element;
|
use crate::widget::Element;
|
||||||
|
|
||||||
use super::{ build_draw_ctx, commit_draw_ctx, compute_damage, layout_and_draw };
|
use super::{ build_draw_ctx, commit_draw_ctx, compute_damage, layout_and_draw };
|
||||||
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar };
|
use super::chrome::{ apply_input_region, draw_fallback_banner, draw_titlebar, sync_opaque_region };
|
||||||
|
|
||||||
/// Full redraw path: clear the canvas, run layout+draw for every widget, then
|
/// Full redraw path: clear the canvas, run layout+draw for every widget, then
|
||||||
/// emit either tight per-widget damage or a single full-surface damage rect
|
/// emit either tight per-widget damage or a single full-surface damage rect
|
||||||
@@ -41,6 +41,7 @@ pub( crate ) fn draw_surface_full<Msg: Clone>(
|
|||||||
view: &Element<Msg>,
|
view: &Element<Msg>,
|
||||||
bg: Color,
|
bg: Color,
|
||||||
input_region: Option<&[Rect]>,
|
input_region: Option<&[Rect]>,
|
||||||
|
opaque_region: Option<&[Rect]>,
|
||||||
debug_layout: bool,
|
debug_layout: bool,
|
||||||
shm_format: wl_shm::Format,
|
shm_format: wl_shm::Format,
|
||||||
swap_rb: bool,
|
swap_rb: bool,
|
||||||
@@ -128,6 +129,7 @@ pub( crate ) fn draw_surface_full<Msg: Clone>(
|
|||||||
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
||||||
|
|
||||||
let wl_surface = ss.surface.wl_surface();
|
let wl_surface = ss.surface.wl_surface();
|
||||||
|
sync_opaque_region( wl_surface, compositor, &mut ss.opaque_full, bg, opaque_region, ss.width, ss.height, scale );
|
||||||
buffer.attach_to( wl_surface ).expect( "attach" );
|
buffer.attach_to( wl_surface ).expect( "attach" );
|
||||||
|
|
||||||
if damage_rects.is_empty()
|
if damage_rects.is_empty()
|
||||||
@@ -159,6 +161,7 @@ pub( crate ) fn draw_surface_partial<Msg: Clone>(
|
|||||||
view: &Element<Msg>,
|
view: &Element<Msg>,
|
||||||
bg: Color,
|
bg: Color,
|
||||||
input_region: Option<&[Rect]>,
|
input_region: Option<&[Rect]>,
|
||||||
|
opaque_region: Option<&[Rect]>,
|
||||||
shm_format: wl_shm::Format,
|
shm_format: wl_shm::Format,
|
||||||
swap_rb: bool,
|
swap_rb: bool,
|
||||||
dirty_rects: Vec<Rect>,
|
dirty_rects: Vec<Rect>,
|
||||||
@@ -221,6 +224,7 @@ pub( crate ) fn draw_surface_partial<Msg: Clone>(
|
|||||||
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
||||||
|
|
||||||
let wl_surface = ss.surface.wl_surface();
|
let wl_surface = ss.surface.wl_surface();
|
||||||
|
sync_opaque_region( wl_surface, compositor, &mut ss.opaque_full, bg, opaque_region, ss.width, ss.height, scale );
|
||||||
buffer.attach_to( wl_surface ).expect( "attach" );
|
buffer.attach_to( wl_surface ).expect( "attach" );
|
||||||
|
|
||||||
for r in &dirty_rects
|
for r in &dirty_rects
|
||||||
|
|||||||
@@ -290,6 +290,9 @@ impl EglContext
|
|||||||
Some( surface.surface ),
|
Some( surface.surface ),
|
||||||
Some( self.context ),
|
Some( self.context ),
|
||||||
).map_err( |e| format!( "eglMakeCurrent: {e:?}" ) )?;
|
).map_err( |e| format!( "eglMakeCurrent: {e:?}" ) )?;
|
||||||
|
// ltk paces itself on wl_surface.frame; Mesa's own throttle would
|
||||||
|
// block eglSwapBuffers until the previous frame's callback.
|
||||||
|
let _ = self.egl.swap_interval( self.display, 0 );
|
||||||
|
|
||||||
if self.gl.get().is_none()
|
if self.gl.get().is_none()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -243,6 +243,8 @@ pub struct AppData<A: App>
|
|||||||
/// lazily-emitted sliding panel — does not recompile the ~14 shader
|
/// lazily-emitted sliding panel — does not recompile the ~14 shader
|
||||||
/// programs (hundreds of ms on a mobile GPU) every time it reappears.
|
/// programs (hundreds of ms on a mobile GPU) every time it reappears.
|
||||||
pub subsurface_gles_canvas: Option<crate::render::Canvas>,
|
pub subsurface_gles_canvas: Option<crate::render::Canvas>,
|
||||||
|
/// GLES canvases of destroyed overlays, reused by the next overlay.
|
||||||
|
pub canvas_pool: Vec<crate::render::Canvas>,
|
||||||
/// Surface currently receiving pointer events (updated on each pointer
|
/// Surface currently receiving pointer events (updated on each pointer
|
||||||
/// event via its `surface` field).
|
/// event via its `surface` field).
|
||||||
pub pointer_focus: SurfaceFocus,
|
pub pointer_focus: SurfaceFocus,
|
||||||
|
|||||||
+10
-4
@@ -63,6 +63,7 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
|
|||||||
main_view,
|
main_view,
|
||||||
main_bg,
|
main_bg,
|
||||||
main_region.as_deref(),
|
main_region.as_deref(),
|
||||||
|
None,
|
||||||
debug_layout,
|
debug_layout,
|
||||||
format,
|
format,
|
||||||
swap_rb,
|
swap_rb,
|
||||||
@@ -95,6 +96,7 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
|
|||||||
&view,
|
&view,
|
||||||
main_bg,
|
main_bg,
|
||||||
None,
|
None,
|
||||||
|
None,
|
||||||
debug_layout,
|
debug_layout,
|
||||||
format,
|
format,
|
||||||
swap_rb,
|
swap_rb,
|
||||||
@@ -117,6 +119,7 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
|
|||||||
}
|
}
|
||||||
let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, focus ); };
|
let req_frame = | wl: &WlSurface | { let _ = wl.frame( qh, focus ); };
|
||||||
let bg = Color::rgba( 0.0, 0.0, 0.0, 0.0 );
|
let bg = Color::rgba( 0.0, 0.0, 0.0, 0.0 );
|
||||||
|
let opaque = data.app.overlay_opaque_region( spec.id );
|
||||||
draw_surface::<A::Message>(
|
draw_surface::<A::Message>(
|
||||||
ss,
|
ss,
|
||||||
&data.compositor_state,
|
&data.compositor_state,
|
||||||
@@ -124,6 +127,7 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
|
|||||||
&spec.view,
|
&spec.view,
|
||||||
bg,
|
bg,
|
||||||
spec.input_region.as_deref(),
|
spec.input_region.as_deref(),
|
||||||
|
opaque.as_deref(),
|
||||||
debug_layout,
|
debug_layout,
|
||||||
format,
|
format,
|
||||||
swap_rb,
|
swap_rb,
|
||||||
@@ -131,6 +135,7 @@ pub( crate ) fn draw_frame<A: App>( data: &mut AppData<A> ) -> bool
|
|||||||
);
|
);
|
||||||
ss.needs_redraw = false;
|
ss.needs_redraw = false;
|
||||||
ss.last_draw = std::time::Instant::now();
|
ss.last_draw = std::time::Instant::now();
|
||||||
|
data.app.on_overlay_frame_committed( spec.id );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +154,7 @@ fn draw_surface<Msg: Clone>(
|
|||||||
view: &Element<Msg>,
|
view: &Element<Msg>,
|
||||||
bg: Color,
|
bg: Color,
|
||||||
input_region: Option<&[Rect]>,
|
input_region: Option<&[Rect]>,
|
||||||
|
opaque_region: Option<&[Rect]>,
|
||||||
debug_layout: bool,
|
debug_layout: bool,
|
||||||
shm_format: wl_shm::Format,
|
shm_format: wl_shm::Format,
|
||||||
swap_rb: bool,
|
swap_rb: bool,
|
||||||
@@ -184,12 +190,12 @@ fn draw_surface<Msg: Clone>(
|
|||||||
if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() )
|
if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() )
|
||||||
{
|
{
|
||||||
draw_surface_partial_gpu(
|
draw_surface_partial_gpu(
|
||||||
ss, compositor, ctx, view, bg, input_region,
|
ss, compositor, ctx, view, bg, input_region, opaque_region,
|
||||||
dirty_rects, pw, ph, scale, request_frame,
|
dirty_rects, pw, ph, scale, request_frame,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
draw_surface_partial(
|
draw_surface_partial(
|
||||||
ss, compositor, view, bg, input_region,
|
ss, compositor, view, bg, input_region, opaque_region,
|
||||||
shm_format, swap_rb, dirty_rects, pw, ph, scale, request_frame,
|
shm_format, swap_rb, dirty_rects, pw, ph, scale, request_frame,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -200,12 +206,12 @@ fn draw_surface<Msg: Clone>(
|
|||||||
if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() )
|
if let ( Some( ctx ), true ) = ( egl_ctx, ss.egl_surface.is_some() )
|
||||||
{
|
{
|
||||||
draw_surface_full_gpu(
|
draw_surface_full_gpu(
|
||||||
ss, compositor, ctx, view, bg, input_region, debug_layout,
|
ss, compositor, ctx, view, bg, input_region, opaque_region, debug_layout,
|
||||||
pw, ph, scale, request_frame,
|
pw, ph, scale, request_frame,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
draw_surface_full(
|
draw_surface_full(
|
||||||
ss, compositor, view, bg, input_region, debug_layout,
|
ss, compositor, view, bg, input_region, opaque_region, debug_layout,
|
||||||
shm_format, swap_rb, pw, ph, scale, request_frame,
|
shm_format, swap_rb, pw, ph, scale, request_frame,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-11
@@ -70,7 +70,9 @@ fn apply_surface_scale<Msg: Clone>(
|
|||||||
es.resize( pw as i32, ph as i32 );
|
es.resize( pw as i32, ph as i32 );
|
||||||
// Canvas FBO reallocation is deferred to the draw path: it needs
|
// Canvas FBO reallocation is deferred to the draw path: it needs
|
||||||
// `eglMakeCurrent` first.
|
// `eglMakeCurrent` first.
|
||||||
} else {
|
} else if ss.pool.is_some() {
|
||||||
|
// Before the first configure neither path is chosen yet; creating a
|
||||||
|
// pool here would lock the surface to SHM.
|
||||||
ss.pool = Some(
|
ss.pool = Some(
|
||||||
smithay_client_toolkit::shm::slot::SlotPool::new(
|
smithay_client_toolkit::shm::slot::SlotPool::new(
|
||||||
( pw * ph * 4 ) as usize, shm,
|
( pw * ph * 4 ) as usize, shm,
|
||||||
@@ -811,16 +813,10 @@ impl<A: App> Dispatch<WlCallback, super::SurfaceFocus> for AppData<A>
|
|||||||
{
|
{
|
||||||
if state.app.subsurface_motion_only()
|
if state.app.subsurface_motion_only()
|
||||||
{
|
{
|
||||||
// The main buffer is unchanged; only the subsurface
|
// The main buffer is unchanged; the subsurface
|
||||||
// moves (repositioned by the per-frame reconcile).
|
// reconcile applies the latest move on this tick and
|
||||||
// Keep the vsync cadence with a bare frame callback +
|
// re-arms the callback.
|
||||||
// commit — no buffer attach, no re-raster.
|
state.main.frame_tick_due = true;
|
||||||
if let Some( wl ) = state.main.surface.try_wl_surface().cloned()
|
|
||||||
{
|
|
||||||
let _ = wl.frame( &state.qh, super::SurfaceFocus::Main );
|
|
||||||
wl.commit();
|
|
||||||
state.main.frame_pending = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -850,10 +846,18 @@ impl<A: App> Dispatch<WlCallback, super::SurfaceFocus> for AppData<A>
|
|||||||
}
|
}
|
||||||
super::SurfaceFocus::Overlay( id ) =>
|
super::SurfaceFocus::Overlay( id ) =>
|
||||||
{
|
{
|
||||||
|
let hosts_subsurface = state.subsurfaces.values()
|
||||||
|
.any( |s| s.parent() == crate::app::SubsurfaceParent::Overlay( id ) );
|
||||||
if let Some( ss ) = state.overlays.get_mut( &id )
|
if let Some( ss ) = state.overlays.get_mut( &id )
|
||||||
{
|
{
|
||||||
ss.frame_pending = false;
|
ss.frame_pending = false;
|
||||||
if is_animating
|
if is_animating
|
||||||
|
{
|
||||||
|
if hosts_subsurface && state.app.subsurface_motion_only()
|
||||||
|
{
|
||||||
|
ss.frame_tick_due = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
state.overlays_dirty = true;
|
state.overlays_dirty = true;
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
@@ -863,6 +867,7 @@ impl<A: App> Dispatch<WlCallback, super::SurfaceFocus> for AppData<A>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<A: App> Dispatch<ZwpTextInputV3, ()> for AppData<A>
|
impl<A: App> Dispatch<ZwpTextInputV3, ()> for AppData<A>
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -51,8 +51,18 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
|||||||
.collect();
|
.collect();
|
||||||
for id in removed
|
for id in removed
|
||||||
{
|
{
|
||||||
if let Some( ss ) = data.overlays.remove( &id )
|
if let Some( mut ss ) = data.overlays.remove( &id )
|
||||||
{
|
{
|
||||||
|
// A GLES canvas carries compiled programs, the glyph atlas and
|
||||||
|
// the image cache: keep it for the next overlay instead of
|
||||||
|
// paying all of that again on every open.
|
||||||
|
if let Some( canvas ) = ss.canvas.take()
|
||||||
|
{
|
||||||
|
if matches!( canvas, crate::render::Canvas::Gles( _ ) )
|
||||||
|
{
|
||||||
|
data.canvas_pool.push( canvas );
|
||||||
|
}
|
||||||
|
}
|
||||||
if ss.gesture.long_press_fired
|
if ss.gesture.long_press_fired
|
||||||
{
|
{
|
||||||
data.main.gesture.long_press_fired = true;
|
data.main.gesture.long_press_fired = true;
|
||||||
@@ -144,6 +154,8 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
|||||||
let main_widget_rects = &data.main.frame.widget_rects;
|
let main_widget_rects = &data.main.frame.widget_rects;
|
||||||
let app = &data.app;
|
let app = &data.app;
|
||||||
let overlays_m = &mut data.overlays;
|
let overlays_m = &mut data.overlays;
|
||||||
|
let canvas_pool = &mut data.canvas_pool;
|
||||||
|
let has_egl = data.egl_context.is_some();
|
||||||
for spec in &specs
|
for spec in &specs
|
||||||
{
|
{
|
||||||
let resolved_size = resolve_size( &spec.size );
|
let resolved_size = resolve_size( &spec.size );
|
||||||
@@ -364,6 +376,7 @@ pub( super ) fn reconcile_overlays<A: App>( data: &mut AppData<A> )
|
|||||||
ss.last_requested_size = resolved_size;
|
ss.last_requested_size = resolved_size;
|
||||||
ss.last_requested_zone = to_logical_zone( spec.exclusive_zone );
|
ss.last_requested_zone = to_logical_zone( spec.exclusive_zone );
|
||||||
ss.layer_anchor = Some( spec.anchor );
|
ss.layer_anchor = Some( spec.anchor );
|
||||||
|
if has_egl { ss.canvas = canvas_pool.pop(); }
|
||||||
overlays_m.insert( spec.id, ss );
|
overlays_m.insert( spec.id, ss );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -354,6 +354,7 @@ pub( crate ) fn try_run<A: App>( mut app: A ) -> Result<(), RunError>
|
|||||||
main_output: None,
|
main_output: None,
|
||||||
subsurfaces: std::collections::HashMap::new(),
|
subsurfaces: std::collections::HashMap::new(),
|
||||||
subsurface_gles_canvas: None,
|
subsurface_gles_canvas: None,
|
||||||
|
canvas_pool: Vec::new(),
|
||||||
pointer_focus: SurfaceFocus::Main,
|
pointer_focus: SurfaceFocus::Main,
|
||||||
keyboard_focus: SurfaceFocus::Main,
|
keyboard_focus: SurfaceFocus::Main,
|
||||||
touch_focus: std::collections::HashMap::new(),
|
touch_focus: std::collections::HashMap::new(),
|
||||||
|
|||||||
@@ -58,12 +58,26 @@ pub( crate ) struct SubsurfaceSlot
|
|||||||
last_version: u64,
|
last_version: u64,
|
||||||
rastered: bool,
|
rastered: bool,
|
||||||
parent: SubsurfaceParent,
|
parent: SubsurfaceParent,
|
||||||
|
scroll_canvases: HashMap<usize, Canvas>,
|
||||||
|
viewport_canvases: HashMap<usize, Canvas>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubsurfaceSlot
|
impl SubsurfaceSlot
|
||||||
{
|
{
|
||||||
fn destroy( mut self )
|
pub( crate ) fn parent( &self ) -> SubsurfaceParent
|
||||||
{
|
{
|
||||||
|
self.parent
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy( mut self, egl: Option<&Arc<EglContext>> )
|
||||||
|
{
|
||||||
|
// GLES sub-canvases free their objects on drop and need the context current.
|
||||||
|
if let ( Some( ctx ), Some( es ) ) = ( egl, self.egl_surface.as_ref() )
|
||||||
|
{
|
||||||
|
let _ = ctx.make_current( es );
|
||||||
|
}
|
||||||
|
self.scroll_canvases.clear();
|
||||||
|
self.viewport_canvases.clear();
|
||||||
// Drop the EGL surface before the wl_surface its wl_egl_window wraps.
|
// Drop the EGL surface before the wl_surface its wl_egl_window wraps.
|
||||||
self.egl_surface = None;
|
self.egl_surface = None;
|
||||||
self.subsurface.destroy();
|
self.subsurface.destroy();
|
||||||
@@ -71,6 +85,56 @@ impl SubsurfaceSlot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn focus_of( parent: SubsurfaceParent ) -> super::SurfaceFocus
|
||||||
|
{
|
||||||
|
match parent
|
||||||
|
{
|
||||||
|
SubsurfaceParent::Main => super::SurfaceFocus::Main,
|
||||||
|
SubsurfaceParent::Overlay( id ) => super::SurfaceFocus::Overlay( id ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parent_frame_pending<A: App>( data: &AppData<A>, parent: SubsurfaceParent ) -> bool
|
||||||
|
{
|
||||||
|
match parent
|
||||||
|
{
|
||||||
|
SubsurfaceParent::Main => data.main.frame_pending,
|
||||||
|
SubsurfaceParent::Overlay( id ) => data.overlays.get( &id ).is_some_and( |ss| ss.frame_pending ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Request a frame callback on `parent` ahead of the commit that applies a
|
||||||
|
/// subsurface move, unless one is already outstanding.
|
||||||
|
fn arm_parent_frame<A: App>( data: &mut AppData<A>, parent: SubsurfaceParent, wl: &WlSurface )
|
||||||
|
{
|
||||||
|
let ss = match parent
|
||||||
|
{
|
||||||
|
SubsurfaceParent::Main => &mut data.main,
|
||||||
|
SubsurfaceParent::Overlay( id ) => match data.overlays.get_mut( &id ) { Some( s ) => s, None => return },
|
||||||
|
};
|
||||||
|
ss.frame_tick_due = false;
|
||||||
|
if ss.frame_pending { return; }
|
||||||
|
let _ = wl.frame( &data.qh, focus_of( parent ) );
|
||||||
|
ss.frame_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep the frame cadence on a parent whose callback fired while the app
|
||||||
|
/// animates by subsurface motion only, when this pass moved nothing on it.
|
||||||
|
fn keep_parent_cadence<A: App>( data: &mut AppData<A>, parent: SubsurfaceParent, animating: bool )
|
||||||
|
{
|
||||||
|
let ss = match parent
|
||||||
|
{
|
||||||
|
SubsurfaceParent::Main => &mut data.main,
|
||||||
|
SubsurfaceParent::Overlay( id ) => match data.overlays.get_mut( &id ) { Some( s ) => s, None => return },
|
||||||
|
};
|
||||||
|
if !std::mem::take( &mut ss.frame_tick_due ) { return; }
|
||||||
|
if ss.frame_pending || !animating { return; }
|
||||||
|
let Some( wl ) = ss.surface.try_wl_surface().cloned() else { return };
|
||||||
|
let _ = wl.frame( &data.qh, focus_of( parent ) );
|
||||||
|
wl.commit();
|
||||||
|
ss.frame_pending = true;
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a subsurface's parent surface to `( wl_surface, phys_w, phys_h,
|
/// Resolve a subsurface's parent surface to `( wl_surface, phys_w, phys_h,
|
||||||
/// scale )`. Returns `None` when an [`SubsurfaceParent::Overlay`] target is
|
/// scale )`. Returns `None` when an [`SubsurfaceParent::Overlay`] target is
|
||||||
/// absent, not yet configured, or zero-sized — the caller skips the spec for
|
/// absent, not yet configured, or zero-sized — the caller skips the spec for
|
||||||
@@ -116,6 +180,7 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
|||||||
// Parents touched this frame; each committed once at the end so the
|
// Parents touched this frame; each committed once at the end so the
|
||||||
// placement / mapping actually lands.
|
// placement / mapping actually lands.
|
||||||
let mut dirty_parents: Vec<( SubsurfaceParent, WlSurface )> = Vec::new();
|
let mut dirty_parents: Vec<( SubsurfaceParent, WlSurface )> = Vec::new();
|
||||||
|
let mut committed: Vec<crate::app::SubsurfaceId> = Vec::new();
|
||||||
|
|
||||||
// Drop subsurfaces whose id disappeared from the spec list.
|
// Drop subsurfaces whose id disappeared from the spec list.
|
||||||
let present: std::collections::HashSet<crate::app::SubsurfaceId> =
|
let present: std::collections::HashSet<crate::app::SubsurfaceId> =
|
||||||
@@ -129,7 +194,7 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
|||||||
if let Some( slot ) = data.subsurfaces.remove( &id )
|
if let Some( slot ) = data.subsurfaces.remove( &id )
|
||||||
{
|
{
|
||||||
let parent = slot.parent;
|
let parent = slot.parent;
|
||||||
slot.destroy();
|
slot.destroy( data.egl_context.as_ref() );
|
||||||
if let Some( ( wl, _, _, _ ) ) = resolve_parent( data, parent )
|
if let Some( ( wl, _, _, _ ) ) = resolve_parent( data, parent )
|
||||||
{
|
{
|
||||||
mark_parent_dirty( &mut dirty_parents, parent, &wl );
|
mark_parent_dirty( &mut dirty_parents, parent, &wl );
|
||||||
@@ -164,9 +229,12 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
|||||||
last_version: u64::MAX,
|
last_version: u64::MAX,
|
||||||
rastered: false,
|
rastered: false,
|
||||||
parent: spec.parent,
|
parent: spec.parent,
|
||||||
|
scroll_canvases: HashMap::new(),
|
||||||
|
viewport_canvases: HashMap::new(),
|
||||||
} );
|
} );
|
||||||
mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
|
mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
|
||||||
}
|
}
|
||||||
|
let parent_pending = parent_frame_pending( data, spec.parent );
|
||||||
|
|
||||||
// Tracked on the slot (not the canvas) because the GLES canvas lives
|
// Tracked on the slot (not the canvas) because the GLES canvas lives
|
||||||
// in `data.subsurface_gles_canvas`, shared across all subsurfaces.
|
// in `data.subsurface_gles_canvas`, shared across all subsurfaces.
|
||||||
@@ -192,11 +260,15 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
|||||||
slot.rastered = true;
|
slot.rastered = true;
|
||||||
slot.last_version = spec.content_version;
|
slot.last_version = spec.content_version;
|
||||||
slot.last_size = ( pw, ph );
|
slot.last_size = ( pw, ph );
|
||||||
|
committed.push( spec.id );
|
||||||
mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
|
mark_parent_dirty( &mut dirty_parents, spec.parent, &parent_wl );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A move waits for the parent's outstanding frame callback, so a drag
|
||||||
|
// commits once per vblank with the latest position instead of once
|
||||||
|
// per input sample.
|
||||||
let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
|
let slot = data.subsurfaces.get_mut( &spec.id ).unwrap();
|
||||||
if slot.last_pos != ( spec.x, spec.y )
|
if slot.last_pos != ( spec.x, spec.y ) && !parent_pending
|
||||||
{
|
{
|
||||||
let ( lx, ly ) = to_logical( spec.x, spec.y, scale );
|
let ( lx, ly ) = to_logical( spec.x, spec.y, scale );
|
||||||
slot.subsurface.set_position( lx, ly );
|
slot.subsurface.set_position( lx, ly );
|
||||||
@@ -211,10 +283,24 @@ pub( crate ) fn reconcile_subsurfaces<A: App>( data: &mut AppData<A> )
|
|||||||
|
|
||||||
// Applies pending subsurface placement / mapping without re-attaching the
|
// Applies pending subsurface placement / mapping without re-attaching the
|
||||||
// parent buffer — the cheap per-frame move.
|
// parent buffer — the cheap per-frame move.
|
||||||
for ( _, wl ) in dirty_parents
|
for ( parent, wl ) in dirty_parents
|
||||||
{
|
{
|
||||||
|
arm_parent_frame( data, parent, &wl );
|
||||||
wl.commit();
|
wl.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let animating = data.app.is_animating();
|
||||||
|
keep_parent_cadence( data, SubsurfaceParent::Main, animating );
|
||||||
|
let overlay_ids: Vec<crate::app::OverlayId> = data.overlays.keys().copied().collect();
|
||||||
|
for id in overlay_ids
|
||||||
|
{
|
||||||
|
keep_parent_cadence( data, SubsurfaceParent::Overlay( id ), animating );
|
||||||
|
}
|
||||||
|
|
||||||
|
for id in committed
|
||||||
|
{
|
||||||
|
data.app.on_subsurface_committed( id );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Input-transparent subsurfaces never carry focus / hover / scroll state,
|
/// Input-transparent subsurfaces never carry focus / hover / scroll state,
|
||||||
@@ -233,6 +319,8 @@ fn empty_draw_ctx<Msg: Clone>() -> DrawCtx<Msg>
|
|||||||
scroll_offsets: HashMap::new(),
|
scroll_offsets: HashMap::new(),
|
||||||
scroll_rects: Vec::new(),
|
scroll_rects: Vec::new(),
|
||||||
scroll_canvases: HashMap::new(),
|
scroll_canvases: HashMap::new(),
|
||||||
|
viewport_canvases: HashMap::new(),
|
||||||
|
viewport_seq: 0,
|
||||||
scroll_navigable_items: HashMap::new(),
|
scroll_navigable_items: HashMap::new(),
|
||||||
scroll_clipped: Vec::new(),
|
scroll_clipped: Vec::new(),
|
||||||
previous_widget_rects: Vec::new(),
|
previous_widget_rects: Vec::new(),
|
||||||
@@ -241,6 +329,14 @@ fn empty_draw_ctx<Msg: Clone>() -> DrawCtx<Msg>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn slot_draw_ctx<Msg: Clone>( scroll: &mut HashMap<usize, Canvas>, viewport: &mut HashMap<usize, Canvas> ) -> DrawCtx<Msg>
|
||||||
|
{
|
||||||
|
let mut ctx = empty_draw_ctx::<Msg>();
|
||||||
|
ctx.scroll_canvases = std::mem::take( scroll );
|
||||||
|
ctx.viewport_canvases = std::mem::take( viewport );
|
||||||
|
ctx
|
||||||
|
}
|
||||||
|
|
||||||
/// Raster a subsurface's content. Uses the GLES path when an [`EglContext`]
|
/// Raster a subsurface's content. Uses the GLES path when an [`EglContext`]
|
||||||
/// is available — only there does the `surface-panel` backdrop blur render;
|
/// is available — only there does the `surface-panel` backdrop blur render;
|
||||||
/// the software fallback paints the same widgets without the blur.
|
/// the software fallback paints the same widgets without the blur.
|
||||||
@@ -320,9 +416,11 @@ fn render_slot_gpu<Msg: Clone>(
|
|||||||
canvas.clear();
|
canvas.clear();
|
||||||
|
|
||||||
let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
|
let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
|
||||||
let mut ctx = empty_draw_ctx::<Msg>();
|
let mut ctx = slot_draw_ctx::<Msg>( &mut slot.scroll_canvases, &mut slot.viewport_canvases );
|
||||||
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
|
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
|
||||||
canvas.present();
|
canvas.present();
|
||||||
|
slot.scroll_canvases = ctx.scroll_canvases;
|
||||||
|
slot.viewport_canvases = ctx.viewport_canvases;
|
||||||
|
|
||||||
let wl = &slot.surface;
|
let wl = &slot.surface;
|
||||||
wl.set_buffer_scale( scale as i32 );
|
wl.set_buffer_scale( scale as i32 );
|
||||||
@@ -382,8 +480,10 @@ fn render_slot_software<Msg: Clone>(
|
|||||||
canvas.clear();
|
canvas.clear();
|
||||||
|
|
||||||
let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
|
let screen_rect = Rect { x: 0.0, y: 0.0, width: pw as f32, height: ph as f32 };
|
||||||
let mut ctx = empty_draw_ctx::<Msg>();
|
let mut ctx = slot_draw_ctx::<Msg>( &mut slot.scroll_canvases, &mut slot.viewport_canvases );
|
||||||
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
|
layout_and_draw::<Msg>( view, canvas, screen_rect, &mut ctx, 0 );
|
||||||
|
slot.scroll_canvases = ctx.scroll_canvases;
|
||||||
|
slot.viewport_canvases = ctx.viewport_canvases;
|
||||||
|
|
||||||
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
canvas.write_to_wayland_buf( canvas_buf, swap_rb );
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,7 @@ pub( crate ) struct FrameState<Msg: Clone>
|
|||||||
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
|
pub scroll_offsets: HashMap<usize, ( f32, f32 )>,
|
||||||
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
|
pub scroll_rects: Vec<( Rect, usize, crate::widget::scroll::ScrollAxis )>,
|
||||||
pub scroll_canvases: HashMap<usize, Canvas>,
|
pub scroll_canvases: HashMap<usize, Canvas>,
|
||||||
|
pub viewport_canvases: HashMap<usize, Canvas>,
|
||||||
/// Per-scroll list of `(flat_idx, content_y, height)` entries for every
|
/// Per-scroll list of `(flat_idx, content_y, height)` entries for every
|
||||||
/// interactive item the scroll's child laid out, in document order. Includes
|
/// interactive item the scroll's child laid out, in document order. Includes
|
||||||
/// items currently scrolled off-screen so keyboard arrow handlers can step
|
/// items currently scrolled off-screen so keyboard arrow handlers can step
|
||||||
@@ -199,6 +200,7 @@ impl<Msg: Clone> FrameState<Msg>
|
|||||||
scroll_offsets: HashMap::new(),
|
scroll_offsets: HashMap::new(),
|
||||||
scroll_rects: Vec::new(),
|
scroll_rects: Vec::new(),
|
||||||
scroll_canvases: HashMap::new(),
|
scroll_canvases: HashMap::new(),
|
||||||
|
viewport_canvases: HashMap::new(),
|
||||||
scroll_navigable_items: HashMap::new(),
|
scroll_navigable_items: HashMap::new(),
|
||||||
scroll_clipped: Vec::new(),
|
scroll_clipped: Vec::new(),
|
||||||
prev_focused: None,
|
prev_focused: None,
|
||||||
@@ -298,6 +300,12 @@ pub( crate ) struct SurfaceState<Msg: Clone>
|
|||||||
/// so the per-surface pacing follows the compositor (and the display
|
/// so the per-surface pacing follows the compositor (and the display
|
||||||
/// refresh / VRR / "screen is off") instead of a fixed-period timer.
|
/// refresh / VRR / "screen is off") instead of a fixed-period timer.
|
||||||
pub frame_pending: bool,
|
pub frame_pending: bool,
|
||||||
|
/// Set by the frame callback while the app animates by moving
|
||||||
|
/// subsurfaces only; the subsurface reconcile keeps the cadence
|
||||||
|
/// with a bare frame request + commit when nothing moved.
|
||||||
|
pub frame_tick_due: bool,
|
||||||
|
/// Opaque rects (physical) last applied to the surface.
|
||||||
|
pub opaque_full: Option<Vec<Rect>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Msg: Clone> SurfaceState<Msg>
|
impl<Msg: Clone> SurfaceState<Msg>
|
||||||
@@ -335,6 +343,8 @@ impl<Msg: Clone> SurfaceState<Msg>
|
|||||||
last_popup_anchor: None,
|
last_popup_anchor: None,
|
||||||
popup_reposition_token: 0,
|
popup_reposition_token: 0,
|
||||||
frame_pending: false,
|
frame_pending: false,
|
||||||
|
frame_tick_due: false,
|
||||||
|
opaque_full: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ impl GlesCanvas
|
|||||||
/// Composite the clip layer back onto the canvas FBO, multiplied by the
|
/// Composite the clip layer back onto the canvas FBO, multiplied by the
|
||||||
/// anti-aliased coverage mask, then end the path clip. No-op when no path
|
/// anti-aliased coverage mask, then end the path clip. No-op when no path
|
||||||
/// clip is open.
|
/// clip is open.
|
||||||
fn flush_clip_layer( &mut self )
|
pub( super ) fn flush_clip_layer( &mut self )
|
||||||
{
|
{
|
||||||
if !self.clip_layer_active { return; }
|
if !self.clip_layer_active { return; }
|
||||||
self.clip_layer_active = false;
|
self.clip_layer_active = false;
|
||||||
|
|||||||
+123
-32
@@ -20,9 +20,13 @@
|
|||||||
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
|
//! memory (RGBA8: `w × h × 4` per texture) with least-recently-drawn
|
||||||
//! eviction, so a stream of distinct buffers (a photo carousel, video
|
//! eviction, so a stream of distinct buffers (a photo carousel, video
|
||||||
//! thumbnails) recycles textures instead of growing GPU memory for the
|
//! thumbnails) recycles textures instead of growing GPU memory for the
|
||||||
//! canvas' lifetime. The most recent entry is never evicted, so a
|
//! context's lifetime. The most recent entry is never evicted, so a
|
||||||
//! single texture larger than the whole budget still draws — the cache
|
//! single texture larger than the whole budget still draws — the cache
|
||||||
//! then holds that one entry until something replaces it.
|
//! then holds that one entry until something replaces it.
|
||||||
|
//!
|
||||||
|
//! One cache serves every canvas and sub-canvas on the same GL context
|
||||||
|
//! (`shared_image_cache`), so a surface that is torn down and recreated
|
||||||
|
//! draws its images without uploading them again.
|
||||||
|
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
@@ -38,6 +42,112 @@ use super::GlesCanvas;
|
|||||||
/// the compositor.
|
/// the compositor.
|
||||||
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
pub const IMAGE_CACHE_MAX_BYTES: usize = 32 * 1024 * 1024;
|
||||||
|
|
||||||
|
type ImageKey = ( u32, u32, u64 );
|
||||||
|
|
||||||
|
/// Content-keyed texture cache shared by every canvas on one GL context.
|
||||||
|
pub( super ) struct ImageCache
|
||||||
|
{
|
||||||
|
map: std::collections::HashMap<ImageKey, ( glow::Texture, u32, u32 )>,
|
||||||
|
/// LRU order of keys, least-recent at the front.
|
||||||
|
lru: std::collections::VecDeque<ImageKey>,
|
||||||
|
/// Estimated GPU bytes held (RGBA8: w × h × 4).
|
||||||
|
bytes: usize,
|
||||||
|
/// Pre-rendered themed surfaces (shadows + fill + insets), keyed by
|
||||||
|
/// size and style; see `surface_cache.rs`.
|
||||||
|
surfaces: std::collections::HashMap<u64, ( glow::Texture, u32, u32 )>,
|
||||||
|
surfaces_lru: std::collections::VecDeque<u64>,
|
||||||
|
surfaces_bytes: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Byte budget for the pre-rendered surface textures.
|
||||||
|
pub const SURFACE_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
impl ImageCache
|
||||||
|
{
|
||||||
|
fn new() -> Self
|
||||||
|
{
|
||||||
|
Self
|
||||||
|
{
|
||||||
|
map: std::collections::HashMap::new(),
|
||||||
|
lru: std::collections::VecDeque::new(),
|
||||||
|
bytes: 0,
|
||||||
|
surfaces: std::collections::HashMap::new(),
|
||||||
|
surfaces_lru: std::collections::VecDeque::new(),
|
||||||
|
surfaces_bytes: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub( super ) fn surface( &mut self, key: u64 ) -> Option<( glow::Texture, u32, u32 )>
|
||||||
|
{
|
||||||
|
let hit = *self.surfaces.get( &key )?;
|
||||||
|
if let Some( pos ) = self.surfaces_lru.iter().position( |k| *k == key )
|
||||||
|
{
|
||||||
|
self.surfaces_lru.remove( pos );
|
||||||
|
self.surfaces_lru.push_back( key );
|
||||||
|
}
|
||||||
|
Some( hit )
|
||||||
|
}
|
||||||
|
|
||||||
|
pub( super ) fn insert_surface( &mut self, gl: &glow::Context, key: u64, tex: glow::Texture, w: u32, h: u32 )
|
||||||
|
{
|
||||||
|
self.surfaces.insert( key, ( tex, w, h ) );
|
||||||
|
self.surfaces_bytes += ( w as usize ) * ( h as usize ) * 4;
|
||||||
|
self.surfaces_lru.push_back( key );
|
||||||
|
while self.surfaces_bytes > SURFACE_CACHE_MAX_BYTES && self.surfaces_lru.len() > 1
|
||||||
|
{
|
||||||
|
let Some( old ) = self.surfaces_lru.pop_front() else { break };
|
||||||
|
if let Some( ( t, ow, oh ) ) = self.surfaces.remove( &old )
|
||||||
|
{
|
||||||
|
self.surfaces_bytes = self.surfaces_bytes.saturating_sub( ( ow as usize ) * ( oh as usize ) * 4 );
|
||||||
|
// SAFETY: `t` was created through `gl` in `end_offscreen` and
|
||||||
|
// just left the map, so it is deleted exactly once.
|
||||||
|
unsafe { gl.delete_texture( t ); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delete least-recently-drawn textures until the estimated byte total
|
||||||
|
/// fits [`IMAGE_CACHE_MAX_BYTES`]. The back of the LRU — the entry the
|
||||||
|
/// current draw is about to use — is never removed.
|
||||||
|
fn evict_overflow( &mut self, gl: &glow::Context )
|
||||||
|
{
|
||||||
|
while self.bytes > IMAGE_CACHE_MAX_BYTES && self.lru.len() > 1
|
||||||
|
{
|
||||||
|
let Some( key ) = self.lru.pop_front() else { break };
|
||||||
|
if let Some( ( tex, w, h ) ) = self.map.remove( &key )
|
||||||
|
{
|
||||||
|
self.bytes = self.bytes.saturating_sub( ( w as usize ) * ( h as usize ) * 4 );
|
||||||
|
// SAFETY: `tex` was created through `gl` in `draw_image_data`
|
||||||
|
// and just left the map, so it is deleted exactly once.
|
||||||
|
unsafe { gl.delete_texture( tex ); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local!
|
||||||
|
{
|
||||||
|
static SHARED_IMAGE_CACHES: std::cell::RefCell<Vec<( usize, std::rc::Rc<std::cell::RefCell<ImageCache>> )>> =
|
||||||
|
std::cell::RefCell::new( Vec::new() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cache for the context behind `gl`, created on first use.
|
||||||
|
pub( super ) fn shared_image_cache( gl: &std::sync::Arc<glow::Context> ) -> std::rc::Rc<std::cell::RefCell<ImageCache>>
|
||||||
|
{
|
||||||
|
let key = std::sync::Arc::as_ptr( gl ) as usize;
|
||||||
|
SHARED_IMAGE_CACHES.with( |caches|
|
||||||
|
{
|
||||||
|
let mut caches = caches.borrow_mut();
|
||||||
|
if let Some( ( _, c ) ) = caches.iter().find( |( k, _ )| *k == key )
|
||||||
|
{
|
||||||
|
return std::rc::Rc::clone( c );
|
||||||
|
}
|
||||||
|
let c = std::rc::Rc::new( std::cell::RefCell::new( ImageCache::new() ) );
|
||||||
|
caches.push( ( key, std::rc::Rc::clone( &c ) ) );
|
||||||
|
c
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
|
||||||
impl GlesCanvas
|
impl GlesCanvas
|
||||||
{
|
{
|
||||||
/// Blit RGBA image data scaled to dest rect with opacity.
|
/// Blit RGBA image data scaled to dest rect with opacity.
|
||||||
@@ -62,17 +172,19 @@ impl GlesCanvas
|
|||||||
// depth).
|
// depth).
|
||||||
let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );
|
let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );
|
||||||
|
|
||||||
if !self.image_cache.contains_key( &cache_key )
|
let cache = std::rc::Rc::clone( &self.image_cache );
|
||||||
|
let mut cache = cache.borrow_mut();
|
||||||
|
if !cache.map.contains_key( &cache_key )
|
||||||
{
|
{
|
||||||
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
||||||
self.image_cache.insert( cache_key, ( tex, img_w, img_h ) );
|
cache.map.insert( cache_key, ( tex, img_w, img_h ) );
|
||||||
self.image_cache_bytes += ( img_w as usize ) * ( img_h as usize ) * 4;
|
cache.bytes += ( img_w as usize ) * ( img_h as usize ) * 4;
|
||||||
self.image_cache_lru.push_back( cache_key );
|
cache.lru.push_back( cache_key );
|
||||||
self.evict_image_cache_overflow();
|
cache.evict_overflow( &self.gl );
|
||||||
} else if let Some( pos ) = self.image_cache_lru.iter().position( |k| *k == cache_key )
|
} else if let Some( pos ) = cache.lru.iter().position( |k| *k == cache_key )
|
||||||
{
|
{
|
||||||
self.image_cache_lru.remove( pos );
|
cache.lru.remove( pos );
|
||||||
self.image_cache_lru.push_back( cache_key );
|
cache.lru.push_back( cache_key );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snap to integer pixels. With GL_LINEAR sampling, a
|
// Snap to integer pixels. With GL_LINEAR sampling, a
|
||||||
@@ -90,12 +202,12 @@ impl GlesCanvas
|
|||||||
height: dest.height.round(),
|
height: dest.height.round(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some( ( tex, _, _ ) ) = self.image_cache.get( &cache_key )
|
if let Some( ( tex, _, _ ) ) = cache.map.get( &cache_key )
|
||||||
{
|
{
|
||||||
let mvp = ortho_rect( self.width, self.height, dest );
|
let mvp = ortho_rect( self.width, self.height, dest );
|
||||||
let alpha = opacity * self.global_alpha;
|
let alpha = opacity * self.global_alpha;
|
||||||
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
||||||
// `self.image_cache` so it outlives the call. Eviction ran
|
// the shared cache so it outlives the call. Eviction ran
|
||||||
// before this borrow and never removes the most-recent key,
|
// before this borrow and never removes the most-recent key,
|
||||||
// which is `cache_key`.
|
// which is `cache_key`.
|
||||||
unsafe
|
unsafe
|
||||||
@@ -114,27 +226,6 @@ impl GlesCanvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete least-recently-drawn cached textures until the estimated
|
|
||||||
/// byte total fits [`IMAGE_CACHE_MAX_BYTES`]. The back of the LRU —
|
|
||||||
/// the entry the current draw is about to use — is never removed.
|
|
||||||
fn evict_image_cache_overflow( &mut self )
|
|
||||||
{
|
|
||||||
while self.image_cache_bytes > IMAGE_CACHE_MAX_BYTES && self.image_cache_lru.len() > 1
|
|
||||||
{
|
|
||||||
let Some( key ) = self.image_cache_lru.pop_front() else { break };
|
|
||||||
if let Some( ( tex, w, h ) ) = self.image_cache.remove( &key )
|
|
||||||
{
|
|
||||||
self.image_cache_bytes = self.image_cache_bytes
|
|
||||||
.saturating_sub( ( w as usize ) * ( h as usize ) * 4 );
|
|
||||||
// SAFETY: `tex` was created through `self.gl` in the insert
|
|
||||||
// path above and just left the map, so it is deleted exactly
|
|
||||||
// once. Deleting a bound texture is defined in GLES (the
|
|
||||||
// binding reverts to 0); no draw is in flight here.
|
|
||||||
unsafe { self.gl.delete_texture( tex ); }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw an externally-owned GL texture into `dest`.
|
/// Draw an externally-owned GL texture into `dest`.
|
||||||
///
|
///
|
||||||
/// The caller owns the texture and is responsible for keeping it valid
|
/// The caller owns the texture and is responsible for keeping it valid
|
||||||
|
|||||||
+5
-10
@@ -45,7 +45,7 @@
|
|||||||
//! method — see the module's own doc for when to use the guards
|
//! method — see the module's own doc for when to use the guards
|
||||||
//! and when not to.
|
//! and when not to.
|
||||||
|
|
||||||
use std::collections::{ HashMap, VecDeque };
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use fontdue::Font;
|
use fontdue::Font;
|
||||||
@@ -62,6 +62,7 @@ pub( crate ) mod clip;
|
|||||||
pub( crate ) mod primitives;
|
pub( crate ) mod primitives;
|
||||||
pub( crate ) mod text;
|
pub( crate ) mod text;
|
||||||
pub( crate ) mod image;
|
pub( crate ) mod image;
|
||||||
|
pub( crate ) mod surface_cache;
|
||||||
mod raii;
|
mod raii;
|
||||||
|
|
||||||
// ─── Public types ────────────────────────────────────────────────────────────
|
// ─── Public types ────────────────────────────────────────────────────────────
|
||||||
@@ -356,11 +357,9 @@ pub struct GlesCanvas
|
|||||||
// `DefaultHasher` pass over the bytes per draw call — fast for
|
// `DefaultHasher` pass over the bytes per draw call — fast for
|
||||||
// any reasonable icon size. Bounded by `image.rs`'s
|
// any reasonable icon size. Bounded by `image.rs`'s
|
||||||
// `IMAGE_CACHE_MAX_BYTES` with LRU eviction.
|
// `IMAGE_CACHE_MAX_BYTES` with LRU eviction.
|
||||||
image_cache: HashMap<(u32, u32, u64), (glow::Texture, u32, u32)>,
|
// Shared by every canvas and sub-canvas on the same GL context, so an
|
||||||
// LRU order of `image_cache` keys, least-recent at the front.
|
// overlay or subsurface that is recreated finds its textures resident.
|
||||||
image_cache_lru: VecDeque<(u32, u32, u64)>,
|
image_cache: std::rc::Rc<std::cell::RefCell<image::ImageCache>>,
|
||||||
// Estimated GPU bytes held by `image_cache` (RGBA8: w × h × 4).
|
|
||||||
image_cache_bytes: usize,
|
|
||||||
|
|
||||||
// Gradient LUT cache: FNV-ish hash of the 512×RGBA8 LUT bytes → texture.
|
// Gradient LUT cache: FNV-ish hash of the 512×RGBA8 LUT bytes → texture.
|
||||||
// Gradients are theme-derived and constant across frames; caching avoids
|
// Gradients are theme-derived and constant across frames; caching avoids
|
||||||
@@ -454,10 +453,6 @@ impl Drop for GlesCanvas
|
|||||||
}
|
}
|
||||||
self.glyph_cache.clear();
|
self.glyph_cache.clear();
|
||||||
self.gl.delete_texture( self.atlas_texture );
|
self.gl.delete_texture( self.atlas_texture );
|
||||||
for ( _, ( tex, _, _ ) ) in self.image_cache.drain()
|
|
||||||
{
|
|
||||||
self.gl.delete_texture( tex );
|
|
||||||
}
|
|
||||||
for ( _, tex ) in self.gradient_lut_cache.drain()
|
for ( _, tex ) in self.gradient_lut_cache.drain()
|
||||||
{
|
{
|
||||||
self.gl.delete_texture( tex );
|
self.gl.delete_texture( tex );
|
||||||
|
|||||||
@@ -546,8 +546,8 @@ impl GlesCanvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw a line as a thin axis-aligned rect (diagonal lines fall back to
|
/// Draw a line as a thin axis-aligned rect, or as one rotated quad with
|
||||||
/// stamping small squares).
|
/// round caps when it is diagonal.
|
||||||
pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
|
pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
|
||||||
{
|
{
|
||||||
let dx = x1 - x0;
|
let dx = x1 - x0;
|
||||||
@@ -562,15 +562,60 @@ impl GlesCanvas
|
|||||||
} else if dx.abs() < 0.1 {
|
} else if dx.abs() < 0.1 {
|
||||||
self.fill_rect( Rect { x: min_x - width / 2.0, y: min_y, width, height: dy.abs() }, color, Corners::ZERO );
|
self.fill_rect( Rect { x: min_x - width / 2.0, y: min_y, width, height: dy.abs() }, color, Corners::ZERO );
|
||||||
} else {
|
} else {
|
||||||
let steps = len.ceil() as usize;
|
self.fill_rotated_line( x0, y0, dx, dy, len, color, width );
|
||||||
for i in 0..steps
|
|
||||||
{
|
|
||||||
let t = i as f32 / len;
|
|
||||||
let px = x0 + dx * t;
|
|
||||||
let py = y0 + dy * t;
|
|
||||||
self.fill_rect( Rect { x: px - width / 2.0, y: py - width / 2.0, width, height: width }, color, Corners::ZERO );
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// One quad of `len × width` rotated along `(dx, dy)` from `(x0, y0)`,
|
||||||
|
/// filled through the rect shader so the caps and edges stay
|
||||||
|
/// antialiased. The MVP maps the unit quad straight onto the rotated
|
||||||
|
/// rect, padded by 1 px like `fill_rect` for the SDF band.
|
||||||
|
fn fill_rotated_line( &mut self, x0: f32, y0: f32, dx: f32, dy: f32, len: f32, color: Color, width: f32 )
|
||||||
|
{
|
||||||
|
let half = width / 2.0 + 1.0;
|
||||||
|
let bbox = Rect
|
||||||
|
{
|
||||||
|
x: x0.min( x0 + dx ) - half,
|
||||||
|
y: y0.min( y0 + dy ) - half,
|
||||||
|
width: dx.abs() + 2.0 * half,
|
||||||
|
height: dy.abs() + 2.0 * half,
|
||||||
|
};
|
||||||
|
if self.rect_culled( bbox, 1.0 ) { return; }
|
||||||
|
self.activate_target();
|
||||||
|
|
||||||
|
let pad = 1.0_f32;
|
||||||
|
let cos = dx / len;
|
||||||
|
let sin = dy / len;
|
||||||
|
let len_p = len + 2.0 * pad;
|
||||||
|
let wid_p = width + 2.0 * pad;
|
||||||
|
// Unit-quad origin in line-local space, then rotated into screen space.
|
||||||
|
let lx = -pad;
|
||||||
|
let ly = -pad - width / 2.0;
|
||||||
|
let sx0 = x0 + lx * cos - ly * sin;
|
||||||
|
let sy0 = y0 + lx * sin + ly * cos;
|
||||||
|
let w = self.width as f32;
|
||||||
|
let h = self.height as f32;
|
||||||
|
let mvp = [
|
||||||
|
2.0 * len_p * cos / w, -2.0 * len_p * sin / h, 0.0, 0.0,
|
||||||
|
-2.0 * wid_p * sin / w, -2.0 * wid_p * cos / h, 0.0, 0.0,
|
||||||
|
0.0, 0.0, 1.0, 0.0,
|
||||||
|
2.0 * sx0 / w - 1.0, 1.0 - 2.0 * sy0 / h, 0.0, 1.0,
|
||||||
|
];
|
||||||
|
let alpha = color.a * self.global_alpha;
|
||||||
|
// SAFETY: see module doc; same uniform set as `fill_rect`.
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
self.gl.use_program( Some( self.rect_program ) );
|
||||||
|
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_rect_mvp ), false, &mvp );
|
||||||
|
self.gl.uniform_4_f32( Some( &self.u_rect_color ), color.r, color.g, color.b, alpha );
|
||||||
|
self.gl.uniform_2_f32( Some( &self.u_rect_size ), len, width );
|
||||||
|
self.gl.uniform_4_f32_slice( Some( &self.u_rect_radii ), &Corners::all( width / 2.0 ).to_uniform() );
|
||||||
|
self.gl.uniform_1_f32( Some( &self.u_rect_stroke ), 0.0 );
|
||||||
|
self.gl.uniform_1_f32( Some( &self.u_rect_pad ), pad );
|
||||||
|
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||||
|
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||||
|
self.gl.bind_vertex_array( None );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -327,6 +327,7 @@ impl GlesCanvas
|
|||||||
tex
|
tex
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let image_cache = super::image::shared_image_cache( &gl );
|
||||||
Self
|
Self
|
||||||
{
|
{
|
||||||
gl,
|
gl,
|
||||||
@@ -459,9 +460,7 @@ impl GlesCanvas
|
|||||||
atlas_cursor_y: 0,
|
atlas_cursor_y: 0,
|
||||||
atlas_row_height: 0,
|
atlas_row_height: 0,
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
image_cache: HashMap::new(),
|
image_cache,
|
||||||
image_cache_lru: std::collections::VecDeque::new(),
|
|
||||||
image_cache_bytes: 0,
|
|
||||||
gradient_lut_cache: HashMap::new(),
|
gradient_lut_cache: HashMap::new(),
|
||||||
clip_scissor: None,
|
clip_scissor: None,
|
||||||
clip_layer: None,
|
clip_layer: None,
|
||||||
@@ -675,9 +674,7 @@ impl GlesCanvas
|
|||||||
atlas_cursor_y: 0,
|
atlas_cursor_y: 0,
|
||||||
atlas_row_height: 0,
|
atlas_row_height: 0,
|
||||||
glyph_cache: HashMap::new(),
|
glyph_cache: HashMap::new(),
|
||||||
image_cache: HashMap::new(),
|
image_cache: std::rc::Rc::clone( &self.image_cache ),
|
||||||
image_cache_lru: std::collections::VecDeque::new(),
|
|
||||||
image_cache_bytes: 0,
|
|
||||||
gradient_lut_cache: HashMap::new(),
|
gradient_lut_cache: HashMap::new(),
|
||||||
clip_scissor: None,
|
clip_scissor: None,
|
||||||
clip_layer: None,
|
clip_layer: None,
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
|
//! Pre-rendered themed surfaces. A card drawn with a Gaussian drop shadow,
|
||||||
|
//! a gradient fill and several inset shadows costs half a dozen full-quad
|
||||||
|
//! shader passes; nothing in that chrome changes between frames, so it is
|
||||||
|
//! rendered once into a texture per (size, style) and blitted afterwards.
|
||||||
|
//! The textures live in the per-context [`super::image::ImageCache`].
|
||||||
|
|
||||||
|
use glow::HasContext;
|
||||||
|
|
||||||
|
use crate::types::Rect;
|
||||||
|
|
||||||
|
use super::GlesCanvas;
|
||||||
|
use super::helpers::{ alloc_fbo_tex, ortho_rect };
|
||||||
|
|
||||||
|
/// Canvas state saved while drawing into an offscreen texture.
|
||||||
|
pub( crate ) struct OffscreenTarget
|
||||||
|
{
|
||||||
|
fbo: glow::Framebuffer,
|
||||||
|
saved_fbo: glow::Framebuffer,
|
||||||
|
saved_tex: glow::Texture,
|
||||||
|
saved_size: ( u32, u32 ),
|
||||||
|
saved_clip: Option<Rect>,
|
||||||
|
saved_alpha: f32,
|
||||||
|
saved_layer: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GlesCanvas
|
||||||
|
{
|
||||||
|
pub( crate ) fn cached_surface( &self, key: u64 ) -> Option<( glow::Texture, u32, u32 )>
|
||||||
|
{
|
||||||
|
self.image_cache.borrow_mut().surface( key )
|
||||||
|
}
|
||||||
|
|
||||||
|
pub( crate ) fn insert_cached_surface( &self, key: u64, tex: glow::Texture, w: u32, h: u32 )
|
||||||
|
{
|
||||||
|
self.image_cache.borrow_mut().insert_surface( &self.gl, key, tex, w, h );
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Redirect every draw to a fresh transparent `w × h` texture until
|
||||||
|
/// [`Self::end_offscreen`]. Clip, opacity and path-clip layer are
|
||||||
|
/// suspended meanwhile and restored afterwards.
|
||||||
|
pub( crate ) fn begin_offscreen( &mut self, w: u32, h: u32 ) -> OffscreenTarget
|
||||||
|
{
|
||||||
|
self.flush_clip_layer();
|
||||||
|
// SAFETY: the context is current (canvas invariant); the FBO and
|
||||||
|
// texture are created through `self.gl` and released in
|
||||||
|
// `end_offscreen` / by the cache eviction.
|
||||||
|
let ( fbo, tex ) = unsafe
|
||||||
|
{
|
||||||
|
let fbo = self.gl.create_framebuffer().expect( "create_framebuffer" );
|
||||||
|
let tex = alloc_fbo_tex( &self.gl, self.version, w, h );
|
||||||
|
self.gl.bind_framebuffer( glow::FRAMEBUFFER, Some( fbo ) );
|
||||||
|
self.gl.framebuffer_texture_2d( glow::FRAMEBUFFER, glow::COLOR_ATTACHMENT0, glow::TEXTURE_2D, Some( tex ), 0 );
|
||||||
|
( fbo, tex )
|
||||||
|
};
|
||||||
|
let target = OffscreenTarget
|
||||||
|
{
|
||||||
|
fbo,
|
||||||
|
saved_fbo: self.fbo,
|
||||||
|
saved_tex: self.fbo_tex,
|
||||||
|
saved_size: ( self.width, self.height ),
|
||||||
|
saved_clip: self.clip_scissor,
|
||||||
|
saved_alpha: self.global_alpha,
|
||||||
|
saved_layer: self.clip_layer_active,
|
||||||
|
};
|
||||||
|
self.fbo = fbo;
|
||||||
|
self.fbo_tex = tex;
|
||||||
|
self.width = w;
|
||||||
|
self.height = h;
|
||||||
|
self.clip_scissor = None;
|
||||||
|
self.global_alpha = 1.0;
|
||||||
|
self.clip_layer_active = false;
|
||||||
|
self.clear();
|
||||||
|
target
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restore the canvas target and hand back the offscreen texture.
|
||||||
|
pub( crate ) fn end_offscreen( &mut self, target: OffscreenTarget ) -> glow::Texture
|
||||||
|
{
|
||||||
|
let tex = self.fbo_tex;
|
||||||
|
self.fbo = target.saved_fbo;
|
||||||
|
self.fbo_tex = target.saved_tex;
|
||||||
|
self.width = target.saved_size.0;
|
||||||
|
self.height = target.saved_size.1;
|
||||||
|
self.clip_scissor = target.saved_clip;
|
||||||
|
self.global_alpha = target.saved_alpha;
|
||||||
|
self.clip_layer_active = target.saved_layer;
|
||||||
|
// SAFETY: `target.fbo` was created in `begin_offscreen` and is no
|
||||||
|
// longer bound after `activate_target` below.
|
||||||
|
unsafe { self.gl.delete_framebuffer( target.fbo ); }
|
||||||
|
self.activate_target();
|
||||||
|
tex
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Composite a premultiplied offscreen texture at `( x, y )` with the
|
||||||
|
/// canvas opacity, through the same path a sub-canvas blit takes.
|
||||||
|
pub( crate ) fn blit_texture( &mut self, tex: glow::Texture, w: u32, h: u32, x: f32, y: f32 )
|
||||||
|
{
|
||||||
|
let dest = Rect { x, y, width: w as f32, height: h as f32 };
|
||||||
|
if let Some( clip ) = self.clip_scissor
|
||||||
|
{
|
||||||
|
let outside = dest.x >= clip.x + clip.width || dest.x + dest.width <= clip.x
|
||||||
|
|| dest.y >= clip.y + clip.height || dest.y + dest.height <= clip.y;
|
||||||
|
if outside { return; }
|
||||||
|
}
|
||||||
|
self.activate_target();
|
||||||
|
let mvp = ortho_rect( self.width, self.height, dest );
|
||||||
|
let alpha = self.global_alpha;
|
||||||
|
// SAFETY: `tex` belongs to the shared cache of this context and
|
||||||
|
// outlives the call; the blend state is the premultiplied over
|
||||||
|
// composite the sub-blit shader expects.
|
||||||
|
unsafe
|
||||||
|
{
|
||||||
|
self.gl.use_program( Some( self.sub_blit_program ) );
|
||||||
|
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_subblit_mvp ), false, &mvp );
|
||||||
|
self.gl.uniform_1_f32( Some( &self.u_subblit_opacity ), alpha );
|
||||||
|
self.gl.uniform_1_f32( Some( &self.u_subblit_fade_bottom ), 0.0 );
|
||||||
|
self.gl.uniform_1_f32( Some( &self.u_subblit_height_px ), h as f32 );
|
||||||
|
self.gl.active_texture( glow::TEXTURE0 );
|
||||||
|
self.gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
|
||||||
|
self.gl.uniform_1_i32( Some( &self.u_subblit_sampler ), 0 );
|
||||||
|
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||||
|
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||||
|
self.gl.bind_vertex_array( None );
|
||||||
|
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,6 +109,25 @@ impl<A: App> AppData<A>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Give a settle the app runs internally a first frame to latch onto.
|
||||||
|
/// When only subsurfaces move, a bare frame request keeps the main
|
||||||
|
/// buffer as it is and the subsurface reconcile carries the motion.
|
||||||
|
fn kick_animation( &mut self )
|
||||||
|
{
|
||||||
|
if self.app.subsurface_motion_only()
|
||||||
|
{
|
||||||
|
if self.main.frame_pending { return; }
|
||||||
|
let Some( wl ) = self.main.surface.try_wl_surface().cloned() else { return };
|
||||||
|
let _ = wl.frame( &self.qh, SurfaceFocus::Main );
|
||||||
|
wl.commit();
|
||||||
|
self.main.frame_pending = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.dirty_caches();
|
||||||
|
self.main.request_redraw();
|
||||||
|
self.main.frame_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_release_event
|
fn apply_release_event
|
||||||
(
|
(
|
||||||
&mut self,
|
&mut self,
|
||||||
@@ -145,9 +164,7 @@ impl<A: App> AppData<A>
|
|||||||
// Message, so the update-driven redraw never fires.
|
// Message, so the update-driven redraw never fires.
|
||||||
// Kick the main surface here so `is_animating()` has a
|
// Kick the main surface here so `is_animating()` has a
|
||||||
// frame to latch onto.
|
// frame to latch onto.
|
||||||
self.dirty_caches();
|
self.kick_animation();
|
||||||
self.main.request_redraw();
|
|
||||||
self.main.frame_pending = false;
|
|
||||||
}
|
}
|
||||||
ReleaseEvent::SwipeRight =>
|
ReleaseEvent::SwipeRight =>
|
||||||
{
|
{
|
||||||
@@ -155,9 +172,7 @@ impl<A: App> AppData<A>
|
|||||||
{
|
{
|
||||||
self.pending_msgs.push( msg );
|
self.pending_msgs.push( msg );
|
||||||
}
|
}
|
||||||
self.dirty_caches();
|
self.kick_animation();
|
||||||
self.main.request_redraw();
|
|
||||||
self.main.frame_pending = false;
|
|
||||||
}
|
}
|
||||||
ReleaseEvent::SwipeUp =>
|
ReleaseEvent::SwipeUp =>
|
||||||
{
|
{
|
||||||
@@ -165,9 +180,7 @@ impl<A: App> AppData<A>
|
|||||||
{
|
{
|
||||||
self.pending_msgs.push( msg );
|
self.pending_msgs.push( msg );
|
||||||
}
|
}
|
||||||
self.dirty_caches();
|
self.kick_animation();
|
||||||
self.main.request_redraw();
|
|
||||||
self.main.frame_pending = false;
|
|
||||||
for ss in self.overlays.values_mut()
|
for ss in self.overlays.values_mut()
|
||||||
{
|
{
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
@@ -180,9 +193,7 @@ impl<A: App> AppData<A>
|
|||||||
{
|
{
|
||||||
self.pending_msgs.push( msg );
|
self.pending_msgs.push( msg );
|
||||||
}
|
}
|
||||||
self.dirty_caches();
|
self.kick_animation();
|
||||||
self.main.request_redraw();
|
|
||||||
self.main.frame_pending = false;
|
|
||||||
for ss in self.overlays.values_mut()
|
for ss in self.overlays.values_mut()
|
||||||
{
|
{
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
@@ -195,9 +206,7 @@ impl<A: App> AppData<A>
|
|||||||
{
|
{
|
||||||
self.pending_msgs.push( msg );
|
self.pending_msgs.push( msg );
|
||||||
}
|
}
|
||||||
self.dirty_caches();
|
self.kick_animation();
|
||||||
self.main.request_redraw();
|
|
||||||
self.main.frame_pending = false;
|
|
||||||
for ss in self.overlays.values_mut()
|
for ss in self.overlays.values_mut()
|
||||||
{
|
{
|
||||||
ss.request_redraw();
|
ss.request_redraw();
|
||||||
|
|||||||
@@ -312,6 +312,17 @@ impl Canvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Replace the layout viewport for a subtree drawn on this canvas and
|
||||||
|
/// return the previous one, so the caller can restore it.
|
||||||
|
pub( crate ) fn layout_viewport_override( &mut self, v: Option<( f32, f32 )> ) -> Option<( f32, f32 )>
|
||||||
|
{
|
||||||
|
match self
|
||||||
|
{
|
||||||
|
Canvas::Software( c ) => std::mem::replace( &mut c.layout_viewport, v ),
|
||||||
|
Canvas::Gles( c ) => std::mem::replace( &mut c.layout_viewport, v ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve an explicit [`Length`] in **geometry** space: against
|
/// Resolve an explicit [`Length`] in **geometry** space: against
|
||||||
/// [`Self::viewport_layout`], with this canvas' [`Self::density`].
|
/// [`Self::viewport_layout`], with this canvas' [`Self::density`].
|
||||||
/// Widgets resolve caller-supplied geometry lengths through this so
|
/// Widgets resolve caller-supplied geometry lengths through this so
|
||||||
@@ -785,6 +796,17 @@ impl Canvas
|
|||||||
{
|
{
|
||||||
let corners = corners.into();
|
let corners = corners.into();
|
||||||
|
|
||||||
|
let cacheable = matches!( self, Canvas::Gles( _ ) )
|
||||||
|
&& ( !outer_shadows.is_empty() || !inset_shadows.is_empty() )
|
||||||
|
&& outer_shadows.iter().all( |s| s.blend == crate::theme::BlendMode::Normal )
|
||||||
|
&& inset_shadows.iter().all( |s| s.blend == crate::theme::BlendMode::Normal )
|
||||||
|
&& rect.width >= 1.0 && rect.height >= 1.0;
|
||||||
|
if cacheable
|
||||||
|
{
|
||||||
|
self.fill_surface_cached( rect, fill, outer_shadows, inset_shadows, corners );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for shadow in outer_shadows
|
for shadow in outer_shadows
|
||||||
{
|
{
|
||||||
self.fill_shadow_outer( rect, shadow, corners );
|
self.fill_shadow_outer( rect, shadow, corners );
|
||||||
@@ -798,6 +820,76 @@ impl Canvas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GLES: render the chrome once per (size, style) into a texture held by
|
||||||
|
/// the context's cache and blit it, instead of running the shadow and
|
||||||
|
/// inset shaders over the surface every frame.
|
||||||
|
fn fill_surface_cached
|
||||||
|
(
|
||||||
|
&mut self,
|
||||||
|
rect: Rect,
|
||||||
|
fill: &ThemePaint,
|
||||||
|
outer_shadows: &[Shadow],
|
||||||
|
inset_shadows: &[InsetShadow],
|
||||||
|
corners: Corners,
|
||||||
|
)
|
||||||
|
{
|
||||||
|
use std::hash::{ Hash, Hasher };
|
||||||
|
|
||||||
|
let margin = outer_shadows.iter()
|
||||||
|
.map( |s| s.blur.max( 0.0 ) + s.spread.max( 0.0 ) + s.offset[ 0 ].abs().max( s.offset[ 1 ].abs() ) + 1.0 )
|
||||||
|
.fold( 1.0_f32, f32::max )
|
||||||
|
.ceil();
|
||||||
|
let w = rect.width;
|
||||||
|
let h = rect.height;
|
||||||
|
let tw = ( w.ceil() + 2.0 * margin ) as u32;
|
||||||
|
let th = ( h.ceil() + 2.0 * margin ) as u32;
|
||||||
|
|
||||||
|
let mut hasher = std::hash::DefaultHasher::new();
|
||||||
|
( ( w * 4.0 ).round() as u32, ( h * 4.0 ).round() as u32 ).hash( &mut hasher );
|
||||||
|
for r in [ corners.tl, corners.tr, corners.br, corners.bl ] { r.to_bits().hash( &mut hasher ); }
|
||||||
|
self.dpi_scale().to_bits().hash( &mut hasher );
|
||||||
|
format!( "{fill:?}|{outer_shadows:?}|{inset_shadows:?}" ).hash( &mut hasher );
|
||||||
|
let key = hasher.finish();
|
||||||
|
|
||||||
|
let cached = match self { Canvas::Gles( c ) => c.cached_surface( key ), _ => None };
|
||||||
|
let tex = match cached
|
||||||
|
{
|
||||||
|
Some( ( tex, _, _ ) ) => tex,
|
||||||
|
None =>
|
||||||
|
{
|
||||||
|
let target = match self
|
||||||
|
{
|
||||||
|
Canvas::Gles( c ) => c.begin_offscreen( tw, th ),
|
||||||
|
_ => return,
|
||||||
|
};
|
||||||
|
let local = Rect { x: margin, y: margin, width: w, height: h };
|
||||||
|
for shadow in outer_shadows
|
||||||
|
{
|
||||||
|
self.fill_shadow_outer( local, shadow, corners );
|
||||||
|
}
|
||||||
|
self.fill_paint_rect( local, fill, corners );
|
||||||
|
for inset in inset_shadows
|
||||||
|
{
|
||||||
|
self.fill_shadow_inset( local, inset, corners );
|
||||||
|
}
|
||||||
|
match self
|
||||||
|
{
|
||||||
|
Canvas::Gles( c ) =>
|
||||||
|
{
|
||||||
|
let tex = c.end_offscreen( target );
|
||||||
|
c.insert_cached_surface( key, tex, tw, th );
|
||||||
|
tex
|
||||||
|
}
|
||||||
|
_ => return,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Canvas::Gles( c ) = self
|
||||||
|
{
|
||||||
|
c.blit_texture( tex, tw, th, ( rect.x - margin ).round(), ( rect.y - margin ).round() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
|
pub fn draw_line( &mut self, x0: f32, y0: f32, x1: f32, y1: f32, color: Color, width: f32 )
|
||||||
{
|
{
|
||||||
match self
|
match self
|
||||||
|
|||||||
+29
-2
@@ -112,7 +112,7 @@ impl WallpaperBundle
|
|||||||
/// callers can use this before the surface has been sized.
|
/// callers can use this before the surface has been sized.
|
||||||
pub fn for_size( &self, sw: u32, sh: u32 ) -> ImageData
|
pub fn for_size( &self, sw: u32, sh: u32 ) -> ImageData
|
||||||
{
|
{
|
||||||
if sw == 0 || sh == 0 || sw >= sh
|
if sw == 0 || sh == 0
|
||||||
{
|
{
|
||||||
return self.landscape();
|
return self.landscape();
|
||||||
}
|
}
|
||||||
@@ -127,13 +127,30 @@ impl WallpaperBundle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let cropped = crop_left_to_aspect( &self.landscape, sw, sh );
|
let base = if sw >= sh { self.landscape() } else { crop_left_to_aspect( &self.landscape, sw, sh ) };
|
||||||
|
let cropped = fit_to_surface( base, sw, sh );
|
||||||
let result = ( Arc::clone( &cropped.0 ), cropped.1, cropped.2 );
|
let result = ( Arc::clone( &cropped.0 ), cropped.1, cropped.2 );
|
||||||
*guard = Some( ( key, cropped ) );
|
*guard = Some( ( key, cropped ) );
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Downscale to the surface size when the source is larger: a 4K crop drawn
|
||||||
|
/// on a 720×1440 panel would otherwise be uploaded at 10 MB per texture.
|
||||||
|
fn fit_to_surface( src: ImageData, sw: u32, sh: u32 ) -> ImageData
|
||||||
|
{
|
||||||
|
let ( ref rgba, w, h ) = src;
|
||||||
|
if w <= sw && h <= sh { return src; }
|
||||||
|
let scale = ( sw as f32 / w as f32 ).max( sh as f32 / h as f32 );
|
||||||
|
let nw = ( ( w as f32 * scale ).ceil() as u32 ).max( 1 );
|
||||||
|
let nh = ( ( h as f32 * scale ).ceil() as u32 ).max( 1 );
|
||||||
|
let Some( view ) = image::ImageBuffer::<image::Rgba<u8>, &[u8]>::from_raw( w, h, rgba.as_slice() ) else { return src };
|
||||||
|
// Bilinear: the crop is at most a few times the surface size and this
|
||||||
|
// runs on the UI thread of a phone at the first use of each size.
|
||||||
|
let resized = image::imageops::resize( &view, nw, nh, image::imageops::FilterType::Triangle );
|
||||||
|
( Arc::new( resized.into_raw() ), nw, nh )
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_bytes( bytes: &[u8] ) -> Result<ImageData, image::ImageError>
|
fn decode_bytes( bytes: &[u8] ) -> Result<ImageData, image::ImageError>
|
||||||
{
|
{
|
||||||
use image::GenericImageView as _;
|
use image::GenericImageView as _;
|
||||||
@@ -245,6 +262,16 @@ mod tests
|
|||||||
assert_eq!( ( w, h ), ( 1, 10 ) );
|
assert_eq!( ( w, h ), ( 1, 10 ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn oversized_source_is_downscaled_to_surface()
|
||||||
|
{
|
||||||
|
let bundle = WallpaperBundle::from_decoded( xgrad( 64, 64 ) );
|
||||||
|
let ( _, w, h ) = bundle.for_size( 16, 16 );
|
||||||
|
assert_eq!( ( w, h ), ( 16, 16 ) );
|
||||||
|
let ( _, w, h ) = bundle.for_size( 8, 16 );
|
||||||
|
assert_eq!( ( w, h ), ( 8, 16 ) );
|
||||||
|
}
|
||||||
|
|
||||||
#[ test ]
|
#[ test ]
|
||||||
fn zero_size_returns_landscape()
|
fn zero_size_returns_landscape()
|
||||||
{
|
{
|
||||||
|
|||||||
+29
-1
@@ -6,7 +6,7 @@
|
|||||||
// canonical scenes that exercise the partial-redraw and full-redraw paths.
|
// canonical scenes that exercise the partial-redraw and full-redraw paths.
|
||||||
|
|
||||||
use ltk::core::{ Canvas, RenderOptions, UiSurface };
|
use ltk::core::{ Canvas, RenderOptions, UiSurface };
|
||||||
use ltk::{ button, column, container, spacer, text, Color, Element };
|
use ltk::{ button, column, container, spacer, text, viewport, Color, Element };
|
||||||
|
|
||||||
// ── helpers ───────────────────────────────────────────────────────────────────
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -97,6 +97,34 @@ fn background_fills_every_pixel_when_view_is_empty()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── viewport clipping ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// A viewport without a bottom fade clips on the parent canvas (scissor /
|
||||||
|
// mask) instead of drawing into a sub-canvas: the child must still be cut
|
||||||
|
// at the viewport's height, and nothing of it may leak below.
|
||||||
|
#[ test ]
|
||||||
|
fn viewport_clips_its_child_to_the_assigned_height()
|
||||||
|
{
|
||||||
|
let mut surface = UiSurface::<()>::new( 32, 64 );
|
||||||
|
let child: Element<()> = container::<()>( spacer().height( 48.0 ) )
|
||||||
|
.background( Color::rgb( 1.0, 0.0, 0.0 ) )
|
||||||
|
.into();
|
||||||
|
let view: Element<()> = column::<()>()
|
||||||
|
.padding( 0.0 )
|
||||||
|
.spacing( 0.0 )
|
||||||
|
.push( viewport( child ).height( 16.0 ) )
|
||||||
|
.into();
|
||||||
|
let _ = surface.render(
|
||||||
|
&view,
|
||||||
|
RenderOptions::full_canvas( 32, 64 ).background( Color::rgb( 0.0, 0.0, 0.0 ) ),
|
||||||
|
);
|
||||||
|
let buf = extract_pixels( &surface, 32, 64 );
|
||||||
|
let red_at = |x: u32, y: u32| buf[ ( ( y * 32 + x ) * 4 ) as usize ];
|
||||||
|
assert!( near( red_at( 16, 4 ), 255, 2 ), "the child paints inside the viewport" );
|
||||||
|
assert!( near( red_at( 16, 24 ), 0, 2 ), "the child is clipped right below the viewport" );
|
||||||
|
assert!( near( red_at( 16, 40 ), 0, 2 ), "nothing of the child leaks further down" );
|
||||||
|
}
|
||||||
|
|
||||||
// ── determinism ───────────────────────────────────────────────────────────────
|
// ── determinism ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn counter_view( count: u32 ) -> Element<()>
|
fn counter_view( count: u32 ) -> Element<()>
|
||||||
|
|||||||
Reference in New Issue
Block a user