Skip to main content

PaintCtx

Struct PaintCtx 

Source
pub struct PaintCtx<'a> { /* private fields */ }
Expand description

Context passed to Widget::paint.

Carries the widget’s resolved geometry (as stored in its pod after layout) so paint code can position itself in the parent coordinate space, plus the v1 animation-driver signal (PaintCtx::request_frame): a widget whose paint advances animation state (e.g. a scroll fling) must call it so the shell keeps scheduling frames even absent external input. The flag bubbles up through ChildPod::paint_child and out of crate::app::RenderRoot::paint as a PaintOutcome, mirroring how EventCtx::request_redraw surfaces through crate::event::EventOutcome.

A frame request also carries a TickClass (see PaintCtx::request_frame vs PaintCtx::request_frame_paced): the aggregate class over the whole paint pass — Transition-dominates-CosmeticLoop — surfaces on PaintOutcome::needs_frame_paced_only for the mobile frame gate to pace a purely-cosmetic frame.

Implementations§

Source§

impl<'a> PaintCtx<'a>

Source

pub const MAX_PACED_INTERVAL: Duration

Ceiling on the interval Self::request_frame_paced_at accepts.

10 seconds comfortably clears every real cosmetic cadence in this codebase — a bare Duration::ZERO “theme rate” request, the ~500ms caret blink, muxr’s ~550ms blink, or any plausible slow pulse — while keeping the shell’s downstream pacing arithmetic (frust-shell-common::frame_gate’s interval * 2 / interval.as_nanos() as u64, frust-shell-desktop::paced_wake’s Instant + interval) far below overflow or truncation even at the widest legal input. Self::request_frame_paced_at is the single entry point every paced interval flows through (see Self::merge_paced_interval’s doc comment), so clamping here bounds every downstream consumer for free — this must never change behavior for any existing caller, since every shipped cadence is orders of magnitude under it.

Source

pub fn new(origin: Point, size: Size) -> Self

Create a paint context for a widget at origin with size.

The frame time defaults to FrameTime::ZERO and no theme is threaded in; the render root seeds the real shell clock via PaintCtx::set_frame_time and the active theme via PaintCtx::set_theme before painting the root widget, and both flow to children through ChildPod::paint_child.

Source

pub fn with_theme(self, theme: &'a dyn Any) -> Self

Attach the app’s active theme, type-erased. Chainable builder mirroring LayoutCtx::with_theme — used by widget unit tests that paint against a known theme; the render root threads it via PaintCtx::set_theme.

Source

pub fn with_translucent(self, translucent: bool) -> Self

Mark this paint pass as running against a translucent (“Mode B”) surface. Chainable builder mirroring PaintCtx::with_theme — used by widget unit tests exercising the platform-view hole-punch; the render root threads the real flag via PaintCtx::set_translucent (see PaintCtx::is_translucent).

Source

pub fn theme_as<T: Any>(&self) -> Option<&T>

Recover the threaded theme as &T, or None if no theme was threaded into this pass (a supported state — bare-core tests and pre-theme apps) or its concrete type differs from T.

The paint-pass mirror of LayoutCtx::theme_as. Widgets that read frust_theme::Theme downcast through this (or the Theme::from_paint_ctx convenience wrapper).

Source

pub fn window_insets(&self) -> WindowInsets

The window’s insets (WindowInsets) for this paint pass (a cheap copy). The paint-pass mirror of LayoutCtx::window_insets: global/origin-independent, so every widget reads the same value (unless a consuming ancestor narrowed it via PaintCtx::with_window_insets); defaults to the zero inset when no shell pushed one.

Source

pub fn with_window_insets<R>( &mut self, insets: WindowInsets, f: impl FnOnce(&mut Self) -> R, ) -> R

Run f with insets installed as this context’s window insets, then restore the previous value and return f’s result.

The paint-pass mirror of LayoutCtx::with_window_insets. The value is otherwise root-seeded and copied down unchanged by ChildPod::paint_child; because paint_child copies it from the parent context it is handed, wrapping paint_child in this scope hands the override to the whole painted subtree. SafeArea does exactly that with the same consumed value it laid its child out under, so a widget that reads insets at paint time sees what it was laid out with.

The restore is a plain assignment after f returns — there is no drop guard, so if f panics the override is not undone (the pass is being unwound anyway).

Source

pub fn presented_frames(&self) -> Option<u64>

The shell’s running count of frames the render thread has actually presented, or None when no shell wired one in (bare-core tests, pre-wiring shells) — a supported state, so a widget can fall back to a paint-cadence measure.

Under the render-thread split the UI thread paints faster than the render thread presents (a gate-skipped or coalesced frame is never presented), so a widget measuring frames per second must difference this presented count — not its own paint count — to report the rate a user actually sees (examples/shadertoy’s HUD is the reference consumer). A widget only ever differences two reads (wrapping_sub); the absolute value is a free-running monotonic counter it must never interpret directly. Seeded from the shell at the root (crate::app::RenderRoot::paint) and threaded unchanged into every child by ChildPod::paint_child, mirroring frame_time.

Source

pub fn origin(&self) -> Point

The widget’s absolute origin in window-space coordinates.

This origin is accumulated as the paint pass descends the widget tree: ChildPod::paint_child adds the child’s parent-relative origin to the parent’s already-absolute ctx.origin(), threading the result down through nested levels. Contrast ChildPod::origin, which is parent-relative and correct as documented, and EventCtx::origin, which is also parent-relative — the event pass translates the event into the child’s local space instead of accumulating the origin. This is therefore the only context origin an overlay, popup, or reported window-space rect may be anchored from.

Source

pub fn size(&self) -> Size

The widget’s resolved size.

Source

pub fn has_focus(&self) -> bool

Whether the widget being painted holds the focus path.

Threaded down from the widget’s pod (ChildPod::is_focused, seeded at the root from RenderRoot::focus_active), this is the authoritative focus signal during paint — a widget must prefer it over any focus flag it tracks internally. A container-routed blur clears the pod’s focus path but never dispatches to the widget’s event(), so a widget-internal flag can lag; reading has_focus() here (and self-correcting the internal flag against it) lets the widget converge one frame after the blur. Mirrors EventCtx::has_focus.

Source

pub fn is_hovered(&self) -> bool

Whether the pointer is over the widget being painted or over a descendant of it — i.e. whether this widget is on the recorded hover path.

A container therefore reads true while the pointer is over a claiming child, the way CSS :hover applies to an element while the pointer is over one of its descendants; a sibling or any other off-path widget reads false.

This is the authoritative hover signal, for the same reason PaintCtx::has_focus is authoritative for focus, only more strongly: a pointer leaving a widget routes its next move to whatever it moved onto, so the widget it left never receives an event telling it so. A hover consumer keeps its own hover flag (that is what earns it a repaint on entry — see EventCtx::claim_hover for the whole contract) and self-corrects that flag from this read every paint, which is what fixes it whenever an event never came.

Hover is opt-in: a widget in a tree where nothing ever calls claim_hover always reads false here.

Source

pub fn frame_time(&self) -> FrameTime

The shell-provided time for this frame (monotonic, arbitrary origin).

This is the single shared clock the whole paint pass sees: seeded from the shell at the root (crate::app::RenderRoot::paint) and threaded unchanged into every child by ChildPod::paint_child, so sibling and nested animations advance against one consistent timestamp. A widget may only difference it against an earlier frame_time it stored (via FrameTime::saturating_sub / crate::anim::AnimationController::advance), never interpret it absolutely — the origin varies per shell. Defaults to FrameTime::ZERO when no clock was threaded in (leaf unit tests).

Source

pub fn request_frame(&mut self)

Signal that this paint advanced animation state and needs to be re-invoked to continue, even with no intervening input event.

The desktop shell honors this with a window.request_redraw() (its ControlFlow::Wait loop would otherwise idle); the mobile shells’ continuous per-frame loops already schedule the next frame and can ignore it. Mirrors EventCtx::request_redraw.

This requests a TickClass::Transition frame — the unpaced, every-vsync class, unchanged from today’s behavior. A widget whose next frame is a pacable decorative loop calls Self::request_frame_paced (or Self::request_frame_class) instead so the mobile frame gate may throttle it.

Source

pub fn request_frame_paced(&mut self)

Request a continuation frame whose next tick is a pacable decorative loop (TickClass::CosmeticLoop) — a shimmer, an idle pulse, a spinner whose exact cadence is imperceptible.

Bubbles like Self::request_frame, but leaves the frame paceable: only if every request this frame is CosmeticLoop may the frame gate throttle it (see TickClass’s max-lattice aggregation). Any concurrent Self::request_frame/Self::request_layout elsewhere in the tree re-forces every-vsync cadence, so a paced request is never a downgrade of user-visible motion.

This names no interval, which means “at the theme’s own MotionScheme::cosmetic_loop_rate” — exactly request_frame_paced_at(Duration::ZERO), since that rate is the cap every paced request resolves against (see Self::request_frame_paced_at).

Source

pub fn request_frame_paced_at(&mut self, interval: Duration)

Request a pacable decorative repaint no more often than once per interval — Self::request_frame_paced with an explicit cadence, for a loop far slower than the theme’s cosmetic rate (a ~500ms caret blink against a 30Hz shimmer cap).

Same class as Self::request_frame_paced (TickClass::CosmeticLoop); only the requested cadence differs. Two contracts govern the value:

  • MIN-lattice aggregation. Multiple paced requests in one paint pass fold to the tightest interval, so every requester is repainted at least as often as it asked. A 30Hz shimmer (a bare Self::request_frame_paced, i.e. Duration::ZERO) beside a 500ms caret paces the frame at 30Hz — the caret is then simply repainted more often than it needs, which is visually indistinguishable from its own cadence and costs nothing beyond frames the shimmer already forced. That asymmetry is by design: a slow request can never starve a fast one.
  • The theme rate is a ceiling, not a target. The frame gate resolves the aggregate against 1 / MotionScheme::cosmetic_loop_rate and takes the longer of the two, so an interval tighter than the cap is clamped to it. Motion that genuinely must run every vsync is not cosmetic — use Self::request_frame (TickClass::Transition) for that.

frust-core never reads a clock or a theme, so neither the MIN-lattice fold above nor the shell-side theme-cap clamp happens here: the aggregate rides out on PaintOutcome::paced_interval and the shell’s frame gate resolves it.

One clamp DOES happen here, though: interval is capped at Self::MAX_PACED_INTERVAL before it is folded in, so no caller (buggy or otherwise) can push a runaway value out to the shell’s pacing arithmetic. See that constant’s doc comment for the full rationale. This is a pure ceiling, never a target — every real cadence in this codebase (a bare Duration::ZERO “theme rate” request, the ~500ms caret blink above, or any plausible slow pulse) sits far below it and passes through completely unchanged.

Source

pub fn request_frame_class(&mut self, class: TickClass)

Request a continuation frame of an explicit TickClass — the general form behind Self::request_frame (Transition) and Self::request_frame_paced (CosmeticLoop, at the theme’s own rate).

Always sets needs_frame; a TickClass::Transition request additionally marks the aggregate unpaced (the max-lattice OR — see TickClass). A CosmeticLoop request never clears an already-unpaced aggregate, and — naming no interval — folds Duration::ZERO into the MIN-lattice like Self::request_frame_paced does.

Source

pub fn needs_frame(&self) -> bool

Whether a continuation frame was requested during this (sub)paint.

Source

pub fn frame_class(&self) -> Option<TickClass>

The aggregate TickClass requested during this (sub)paint, or None if no frame was requested.

Follows the TickClass max-lattice: TickClass::Transition if any request this pass was Transition-class (unpaced), else TickClass::CosmeticLoop when at least one paced request was made and no Transition one was. None means “as today — no continuation frame”.

Source

pub fn needs_frame_paced_only(&self) -> bool

Whether a frame was requested and every request this (sub)paint was TickClass::CosmeticLoop — the paced-only state the mobile frame gate may throttle. Convenience for frame_class() == Some(TickClass::CosmeticLoop).

Source

pub fn paced_interval(&self) -> Option<Duration>

The tightest (MIN) interval any paced request named during this (sub)paint, or None if no paced request was made at all.

Duration::ZERO — what a bare Self::request_frame_paced folds in — means “at the theme’s own cosmetic_loop_rate”, so Some(Duration::ZERO) and None resolve identically at the frame gate; the distinction is only whether any paced request was made. Meaningful only while Self::frame_class is TickClass::CosmeticLoop — a concurrent Transition request makes the whole frame unpaced, at which point no interval applies (see PaintOutcome::paced_interval).

Source

pub fn request_layout(&mut self)

Signal that this paint advanced animation state that changes the widget’s layout (not just its paint), so layout must re-run next frame.

This is the layout counterpart to Self::request_frame: a widget whose animation only repaints (a color fade, a caret blink) calls request_frame alone and stays layout-free under the mobile intra-frame layout skip, whereas a widget whose animation resizes/repositions its children (an expanding accordion) calls this so layout is re-run while the animation is in flight. The flag bubbles up through ChildPod::paint_child exactly like needs_frame and out of crate::app::RenderRoot::paint as PaintOutcome::needs_layout, which folds into the render root’s pending crate::view::ChangeFlags (LAYOUT) so the next frame relayouts.

Calling this also implies Self::request_frame (a widget animating its layout necessarily wants another frame), so a caller needs only one call per animating-layout frame. That implied frame is TickClass::Transition (unpaced): a layout animation is user-visible motion, so it never leaves the frame paceable.

Source

pub fn needs_layout(&self) -> bool

Whether a layout re-run was requested during this (sub)paint.

Source

pub fn publish_ime_state(&mut self, state: ImeState)

Publish the focused editable’s current IME surface during paint.

The event pass (EventCtx::publish_ime_state) refreshes the shell’s IME view on every edit, but an app-driven controlled change — a submit clearing the field, applied by the next rebuild rather than by an event — never crosses the event pass, so the event-published value goes stale. A focused editable therefore also republishes here, in the paint that runs after every rebuild, so crate::app::RenderRoot::ime_state tracks the field’s current text/caret regardless of what drove the change. Bubbles up through ChildPod::paint_child, mirroring Self::request_frame.

Source

pub fn take_ime_state(&mut self) -> Option<ImeState>

Take the IME surface published during this (sub)paint, if any.

Source

pub fn publish_platform_view(&mut self, frame: PlatformViewFrame)

Publish a platform-view child’s paint-time frame (a PlatformViewSlot) for this paint pass.

Pushes onto a Vec rather than setting an Option — deliberately NOT the same shape as PaintCtx::publish_ime_state. IME state has a single focused surface at most, so an overwrite is correct there; a platform view has no such “the” instance, so two slots publishing in one pass must both survive. ChildPod::paint_child bubbles this by extend, never overwrite, for exactly that reason.

Source

pub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame>

Take (and clear) every platform-view frame published during this (sub)paint, in paint order.

Source

pub fn report_input_shield(&mut self, rect: Rect)

Report an absolute-coordinate region where frust content painted OVER a platform-view slot must keep winning pointer input (the “z-shield”).

The auto-collection half of the Mode B input contract: an interactive slot hands a touch-DOWN inside its rect to the native sibling, EXCEPT inside a shield. frust-widgets’ shield(child) wrapper is the reporter — it paints its child unchanged and reports its own painted rect here — so an app marks chrome that overlaps a slot rather than hand-listing rects on the slot itself.

Core stays dumb, exactly as it does for PlatformViewFrame: this is a flat, pass-scoped rect list with no slot association at all. The shell-side differ (frust-shell-common::platform_view) owns the intersection rule that decides which shields belong to which slot. Pushes (never overwrites) — see the input_shields field doc.

Source

pub fn take_input_shields(&mut self) -> Vec<Rect>

Take (and clear) every z-shield rect reported during this (sub)paint, in paint order. The PaintCtx::take_platform_views sibling for the shield channel (see PaintCtx::report_input_shield).

Source

pub fn register_overlay(&mut self, entry: OverlayEntry)

Float entry’s pod above the whole app for this frame, and register its rect for the next frame’s input routing.

§The owner must not paint the pod

A registered pod is painted by crate::app::RenderRoot::paint, after the main tree — that is the only way it escapes its owner’s paint order and every ancestor’s clip. An owner that also paints it itself draws the surface twice: once clipped in place, once floated.

§Per pass, in registration order

The registry is cleared when each paint pass begins, so a surface stays alive only while its owner keeps registering it — there is nothing to unregister, and an owner that stops (or is unmounted) simply disappears from the routing table after the next paint. Within a band, later registration paints and hit-tests above earlier; the band itself outranks registration order.

OverlayEntry::window_rect is absolute logical window space, so an owner computes it from PaintCtx::origin — the only absolute anchor a widget has. Recomputing it every paint is what makes an anchored surface follow its owner with no subscription of any kind.

Registering outside a root-driven paint pass (a leaf unit test painting a bare PaintCtx) is harmless: the entry is dropped by the next real pass’s clear rather than leaking into it.

Source

pub fn publish_selection_toolbar(&mut self, request: SelectionToolbarRequest)

Publish “this field is focused, these verbs apply, and here is where a menu would go” for this frame.

Resolved by crate::app::RenderRoot::paint into RenderRoot::selection_toolbar plus a generation a shell diffs (selection_toolbar_generation), for the platform edit-menu route.

Publish under either policy. A field drawing its own toolbar through the overlay portal (crate::selection_toolbar::SelectionToolbarPolicy::Framework) publishes this too: it costs one pointer-sized write, and it keeps a single code path rather than one per route.

A focused field publishes whether or not it has a selection, and whether or not any bar is up. The verbs are a level a platform responder chain must be able to read at any moment — a hardware Cmd+C/X/V/A is asked for with no menu on screen — so gating the publish on a bar being open is what used to leave those shortcuts unanswerable. Whether a menu should be presented rides in the request’s own flag instead.

Pass-scoped and last-writer-wins, like every other paint-time request: a pass in which nothing publishes resolves to “no field is focused”, which is what puts the toolbar away on a blur without any widget having to retract anything.

Source

pub fn report_hero(&mut self, tag: &str, bounds: Rect) -> HeroDirective

Report a tagged (“hero”) element’s absolute paint bounds and read back what it should do this frame.

A no-op returning HeroDirective::Normal unless a container installed a reporter via PaintCtx::with_hero_registry over this subtree (the normal case — no shared-element transition in flight). When a reporter is installed, bounds is recorded (page-local, i.e. relative to the reporter’s reference origin, so it stays stable under a page’s per-frame animated transition offset), and the directive the installer set for tag is returned — HeroDirective::Suppress (skip painting, this endpoint is morphed by its counterpart) or HeroDirective::Morph (repaint under a rect→rect transform to the morph destination).

Source

pub fn hero_active(&self) -> bool

Whether a shared-element (“hero”) transition is currently in flight over this subtree — i.e. an ancestor installed a hero reporter via PaintCtx::with_hero_registry, the same condition that makes PaintCtx::report_hero record rather than no-op.

The public, boolean sibling of the crate-private PaintCtx::hero_ref, exposed so a container that culls far-offscreen children (a Flex under a ScrollView) can stop culling while a hero is morphing: a tagged descendant scrolled beyond the warm margin would otherwise never paint, and so never report its bounds (PaintCtx::report_hero) for the morph. false in the normal case (no transition), so culling is unaffected off the transition path.

Source

pub fn with_hero_registry( &mut self, registry: &RefCell<HeroFrames>, f: impl FnOnce(&mut PaintCtx<'_>), )

Run f with a paint context that has registry installed as the tagged-rect (“hero”) reporter, threading this context’s clock/theme/ focus/geometry down unchanged. A container paints a subtree inside the closure; descendants report through PaintCtx::report_hero, and the container reads the captured rects back from registry afterward. Any continuation-frame request or IME publish made inside bubbles back onto self, mirroring ChildPod::paint_child’s absorb.

Source

pub fn visible_rect(&self) -> Option<Rect>

The absolute-coordinate visible rectangle a scroll ancestor has threaded down, or None when no viewport constraint is in effect (paint everything). A container that culls offscreen children (Flex under a ScrollView) tests each child’s absolute bounds against this; a widget with no interest in culling ignores it entirely. See PaintCtx::constrain_visible_rect for how a scroll surface sets it.

Source

pub fn constrain_visible_rect(&mut self, rect: Rect)

Constrain the threaded visible rectangle to rect (in absolute paint coordinates), the seam a ScrollView uses to publish its viewport to descendants.

When no rect is threaded yet this installs rect; when one already is (a nested scroll surface), the two are intersected — the visible region can only ever narrow, never widen, as scroll surfaces nest, so an inner viewport never re-reveals content an outer one clipped away. The value flows to children unchanged via ChildPod::paint_child, mirroring how window_insets/theme are threaded.

Source

pub fn is_translucent(&self) -> bool

Whether the shell created a translucent (alpha-channel, “Mode B”) GPU surface for this frame — threaded from the render root and copied down to every descendant like the theme/insets.

false in the normal opaque (“Mode A”) case, bare-core tests, and every desktop app. The platform-view hole-punch reads it: a slot only clears its rect (PaintScene::clear_rect) when this is true, so punching never erases app content on an opaque surface (see frust-widgets’ PlatformViewWidget::paint).

Auto Trait Implementations§

§

impl<'a> !RefUnwindSafe for PaintCtx<'a>

§

impl<'a> !Send for PaintCtx<'a>

§

impl<'a> !Sync for PaintCtx<'a>

§

impl<'a> !UnwindSafe for PaintCtx<'a>

§

impl<'a> Freeze for PaintCtx<'a>

§

impl<'a> Unpin for PaintCtx<'a>

§

impl<'a> UnsafeUnpin for PaintCtx<'a>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> StorageAccess<T> for T

Source§

fn as_borrowed(&self) -> &T

Borrows the value.
Source§

fn into_taken(self) -> T

Takes the value.
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.