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>
impl<'a> PaintCtx<'a>
Sourcepub const MAX_PACED_INTERVAL: Duration
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.
Sourcepub fn new(origin: Point, size: Size) -> Self
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.
Sourcepub fn with_theme(self, theme: &'a dyn Any) -> Self
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.
Sourcepub fn with_translucent(self, translucent: bool) -> Self
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).
Sourcepub fn theme_as<T: Any>(&self) -> Option<&T>
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).
Sourcepub fn window_insets(&self) -> WindowInsets
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.
Sourcepub fn with_window_insets<R>(
&mut self,
insets: WindowInsets,
f: impl FnOnce(&mut Self) -> R,
) -> R
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).
Sourcepub fn presented_frames(&self) -> Option<u64>
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.
Sourcepub fn origin(&self) -> Point
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.
Sourcepub fn has_focus(&self) -> bool
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.
Sourcepub fn is_hovered(&self) -> bool
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.
Sourcepub fn frame_time(&self) -> FrameTime
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).
Sourcepub fn request_frame(&mut self)
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.
Sourcepub fn request_frame_paced(&mut self)
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).
Sourcepub fn request_frame_paced_at(&mut self, interval: Duration)
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_rateand 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 — useSelf::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.
Sourcepub fn request_frame_class(&mut self, class: TickClass)
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.
Sourcepub fn needs_frame(&self) -> bool
pub fn needs_frame(&self) -> bool
Whether a continuation frame was requested during this (sub)paint.
Sourcepub fn frame_class(&self) -> Option<TickClass>
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”.
Sourcepub fn needs_frame_paced_only(&self) -> bool
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).
Sourcepub fn paced_interval(&self) -> Option<Duration>
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).
Sourcepub fn request_layout(&mut self)
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.
Sourcepub fn needs_layout(&self) -> bool
pub fn needs_layout(&self) -> bool
Whether a layout re-run was requested during this (sub)paint.
Sourcepub fn publish_ime_state(&mut self, state: ImeState)
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.
Sourcepub fn take_ime_state(&mut self) -> Option<ImeState>
pub fn take_ime_state(&mut self) -> Option<ImeState>
Take the IME surface published during this (sub)paint, if any.
Sourcepub fn publish_platform_view(&mut self, frame: PlatformViewFrame)
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.
Sourcepub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame>
pub fn take_platform_views(&mut self) -> Vec<PlatformViewFrame>
Take (and clear) every platform-view frame published during this (sub)paint, in paint order.
Sourcepub fn report_input_shield(&mut self, rect: Rect)
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.
Sourcepub fn take_input_shields(&mut self) -> Vec<Rect>
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).
Sourcepub fn register_overlay(&mut self, entry: OverlayEntry)
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.
Sourcepub fn publish_selection_toolbar(&mut self, request: SelectionToolbarRequest)
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.
Sourcepub fn report_hero(&mut self, tag: &str, bounds: Rect) -> HeroDirective
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).
Sourcepub fn hero_active(&self) -> bool
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.
Sourcepub fn with_hero_registry(
&mut self,
registry: &RefCell<HeroFrames>,
f: impl FnOnce(&mut PaintCtx<'_>),
)
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.
Sourcepub fn visible_rect(&self) -> Option<Rect>
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.
Sourcepub fn constrain_visible_rect(&mut self, rect: Rect)
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.
Sourcepub fn is_translucent(&self) -> bool
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).