use super::*;
pub(crate) struct RetainedSubview {
source: Option<AnyView>,
node: Option<RenderNode>,
laid_out: Size,
needs_layout: bool,
default_a11y_label: Option<String>,
}
impl RetainedSubview {
pub(crate) fn new(source: AnyView) -> Self {
Self {
source: Some(source),
node: None,
laid_out: Size::zero(),
needs_layout: true,
default_a11y_label: None,
}
}
pub(crate) fn ensure_built(&mut self, renderer: &mut HydrolysisRenderer, env: &Environment) {
if self.node.is_none()
&& let Some(view) = self.source.take()
{
#[cfg(feature = "accessibility")]
{
self.default_a11y_label = renderer.accessibility_label_from_view(&view, env);
}
let view = normalize_layout_view(view, env);
self.node = Some(RenderNode::build(view, env, renderer));
}
}
pub(crate) fn default_a11y_label(&self) -> Option<String> {
self.default_a11y_label.clone()
}
pub(crate) fn map_source(&mut self, f: impl FnOnce(AnyView) -> AnyView) {
let source = self.source.take().expect(
"RetainedSubview::map_source must run before the sub-view is built (source consumed)",
);
self.source = Some(f(source));
}
pub(crate) fn measure_intrinsic(
&mut self,
renderer: &mut HydrolysisRenderer,
env: &Environment,
) -> Size {
self.ensure_built(renderer, env);
self.measure_built(&mut renderer.state, env)
}
pub(crate) fn measure_built(&self, state: &mut HydroState, env: &Environment) -> Size {
let Some(node) = &self.node else {
return Size::zero();
};
node.measure(state, env, ProposalSize::UNSPECIFIED).size
}
pub(crate) fn measure_built_with_proposal(
&self,
state: &mut HydroState,
env: &Environment,
proposal: ProposalSize,
) -> Size {
let Some(node) = &self.node else {
return Size::zero();
};
node.measure(state, env, proposal).size
}
pub(crate) fn patch_and_measure(
&mut self,
renderer: &mut HydrolysisRenderer,
env: &Environment,
proposal: ProposalSize,
) -> (Size, StretchAxis) {
self.ensure_built(renderer, env);
let Some(node) = &mut self.node else {
return (Size::zero(), StretchAxis::None);
};
self.needs_layout |= Self::patch_built(node, renderer);
(
node.measure(&mut renderer.state, env, proposal).size,
node.stretch(),
)
}
pub(crate) fn stretch_axis(&self) -> StretchAxis {
self.node
.as_ref()
.map_or(StretchAxis::None, RenderNode::stretch)
}
fn collect_dynamic_identities_into(&self, out: &mut FxHashSet<usize>) {
if let Some(node) = &self.node {
node.collect_dynamic_identities_into(out);
}
}
fn patch_built(node: &mut RenderNode, renderer: &mut HydrolysisRenderer) -> bool {
let structural = node.patch(renderer);
if structural {
renderer.note_subview_structural_change();
}
structural
}
fn patch_for_parent(&mut self, renderer: &mut HydrolysisRenderer) -> bool {
let structural = self.node.as_mut().is_some_and(|node| node.patch(renderer));
self.needs_layout |= structural;
structural
}
pub(crate) fn flush_in_rect(
&mut self,
renderer: &mut HydrolysisRenderer,
ctx: RenderContext,
env: &Environment,
rect: vello::kurbo::Rect,
) {
if rect.width() <= 0.0 || rect.height() <= 0.0 {
return;
}
self.ensure_built(renderer, env);
let Some(node) = &mut self.node else {
return;
};
let structural = Self::patch_built(node, renderer);
#[allow(clippy::cast_possible_truncation)]
let size = Size::new(rect.width() as f32, rect.height() as f32);
self.needs_layout |= structural;
if self.needs_layout || size != self.laid_out {
node.layout(renderer, env, size);
self.laid_out = size;
self.needs_layout = false;
}
let child_ctx = ctx.child(
vello::kurbo::Affine::translate((rect.x0, rect.y0)),
vello::kurbo::Rect::new(0.0, 0.0, rect.width(), rect.height()),
);
node.flush(renderer, child_ctx, env);
}
pub(crate) fn flush_in_ctx(
&mut self,
renderer: &mut HydrolysisRenderer,
ctx: RenderContext,
env: &Environment,
size: Size,
) {
if size.width <= 0.0 || size.height <= 0.0 {
return;
}
self.ensure_built(renderer, env);
let Some(node) = &mut self.node else {
return;
};
let structural = Self::patch_built(node, renderer);
self.needs_layout |= structural;
if self.needs_layout || size != self.laid_out {
node.layout(renderer, env, size);
self.laid_out = size;
self.needs_layout = false;
}
node.flush(renderer, ctx, env);
}
pub(crate) fn render_built_scene(
&mut self,
renderer: &mut HydrolysisRenderer,
env: &Environment,
size: Size,
) -> NavigationCapturedScene {
self.ensure_built(renderer, env);
let mut scene = vello::Scene::new();
let Some(node) = &mut self.node else {
return NavigationCapturedScene::default();
};
let structural = Self::patch_built(node, renderer);
self.needs_layout |= structural;
if self.needs_layout || size != self.laid_out {
node.layout(renderer, env, size);
self.laid_out = size;
self.needs_layout = false;
}
let local_ctx = RenderContext::with_transforms(
vello::kurbo::Rect::new(0.0, 0.0, f64::from(size.width), f64::from(size.height)),
vello::kurbo::Affine::IDENTITY,
vello::kurbo::Affine::IDENTITY,
);
renderer.begin_navigation_scene_capture();
core::mem::swap(renderer.scene_mut(), &mut scene);
node.flush(renderer, local_ctx, env);
core::mem::swap(renderer.scene_mut(), &mut scene);
renderer.finish_navigation_scene_capture(scene)
}
pub(crate) fn render_built_navigation_scene_inactive(
&mut self,
renderer: &mut HydrolysisRenderer,
env: &Environment,
size: Size,
) -> NavigationCapturedScene {
let previous_hit_test_opacity = renderer.hit_test.hit_test_opacity;
renderer.hit_test.hit_test_opacity = 0.0;
#[cfg(feature = "accessibility")]
renderer.push_accessibility_suppression();
let scene = self.render_built_scene(renderer, env, size);
#[cfg(feature = "accessibility")]
renderer.pop_accessibility_suppression();
renderer.hit_test.hit_test_opacity = previous_hit_test_opacity;
scene
}
}
pub(crate) struct VisibleSubviewCache<K: Eq + core::hash::Hash + Clone> {
entries: std::collections::HashMap<K, RetainedSubview>,
touched: std::collections::HashSet<K>,
}
impl<K: Eq + core::hash::Hash + Clone> VisibleSubviewCache<K> {
pub(crate) fn new() -> Self {
Self {
entries: std::collections::HashMap::new(),
touched: std::collections::HashSet::new(),
}
}
pub(crate) fn begin_frame(&mut self) {
self.touched.clear();
}
pub(crate) fn entry(
&mut self,
key: K,
build: impl FnOnce() -> AnyView,
) -> &mut RetainedSubview {
self.touched.insert(key.clone());
self.entries
.entry(key)
.or_insert_with(|| RetainedSubview::new(build()))
}
pub(crate) fn get(&self, key: &K) -> Option<&RetainedSubview> {
self.entries.get(key)
}
pub(crate) fn patch_for_parent(&mut self, renderer: &mut HydrolysisRenderer) -> bool {
self.entries.values_mut().fold(false, |changed, entry| {
entry.patch_for_parent(renderer) | changed
})
}
pub(crate) fn collect_dynamic_identities_into(&self, out: &mut FxHashSet<usize>) {
for entry in self.entries.values() {
entry.collect_dynamic_identities_into(out);
}
}
pub(crate) fn end_frame(&mut self) {
let touched = &self.touched;
self.entries.retain(|key, _| touched.contains(key));
}
}
pub(crate) struct WrapperNode {
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
pub(super) effect: WrapperEffect,
pub(super) env: Environment,
pub(super) child: RenderNode,
}
pub(crate) trait WidgetBehavior {
fn render(
self: Rc<Self>,
renderer: &mut HydrolysisRenderer,
ctx: RenderContext,
env: &Environment,
);
fn measure(
&self,
state: &mut HydroState,
proposal: ProposalSize,
env: &Environment,
) -> ViewDimensions;
}
pub(crate) struct WidgetNode {
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
pub(super) behavior: Rc<dyn WidgetBehavior>,
pub(super) stretch: StretchAxis,
pub(super) env: Environment,
}
pub(super) enum WrapperEffect {
LayoutPriority(LayoutPriority),
NavigationTransitionSource(RawId),
NavigationTransitionDestination(RawId),
Clip(ClipShape),
Border(Border),
Shadow(Shadow),
Cursor(Cursor),
Draggable(Draggable),
DropDestination(DropDestinationHandles),
ContextMenu(ResolvedContextMenu),
Hittable(Hittable),
OnEvent(Rc<RefCell<OnEvent>>),
GestureObserver(GestureObserverEffect),
Focused(Focused),
LifeCycle(LifeCycleEffect),
}
pub(crate) struct LifeCycleEffect {
pub(super) appear: Cell<Option<DeferredLifeCycleHook>>,
pub(super) disappear: Option<DeferredLifeCycleHook>,
}
impl Drop for LifeCycleEffect {
fn drop(&mut self) {
if let Some(hook) = self.disappear.take() {
hook.call();
}
}
}
pub(crate) struct GestureObserverEffect {
pub(crate) gesture: Gesture,
pub(crate) action: Rc<RefCell<BoxedAction<()>>>,
#[cfg(feature = "accessibility")]
pub(crate) default_a11y_label: Option<String>,
pub(crate) gesture_group_identity: usize,
}
pub(crate) struct ColorNode {
pub(crate) color: Computed<ResolvedColor>,
}
pub(crate) struct TextNode {
#[cfg(feature = "accessibility")]
pub(crate) accessibility_identity: Rc<()>,
pub(crate) content: Computed<StyledStr>,
pub(crate) alignment: Computed<HorizontalAlignment>,
pub(crate) line_limit: Option<usize>,
}
pub(crate) struct ContainerNode {
#[cfg(feature = "accessibility")]
pub(crate) accessibility_identity: Rc<()>,
pub(crate) layout: Box<dyn Layout>,
pub(crate) children: Vec<RenderNode>,
#[cfg(feature = "accessibility")]
pub(crate) accessibility_child_env: Option<Environment>,
pub(crate) placed: Vec<Rect>,
pub(crate) _guards: Vec<BoxWatcherGuard>,
}
pub(crate) struct OpacityNode {
pub(crate) value: Opacity,
pub(crate) child: RenderNode,
}
pub(crate) struct ScaleNode {
pub(crate) value: Scale,
pub(crate) child: RenderNode,
}
pub(crate) struct RotationNode {
pub(crate) value: Rotation,
pub(crate) child: RenderNode,
}
pub(crate) struct OffsetNode {
pub(crate) value: Offset,
pub(crate) child: RenderNode,
}
pub(crate) struct ScrollNode {
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
pub(super) axis: ScrollAxis,
pub(super) child: RenderNode,
pub(super) controller: Option<ScrollController<Point>>,
pub(super) applied_scroll_generation: Cell<i32>,
pub(super) handle: Option<ScrollHandle>,
pub(super) content_size: Size,
pub(super) viewport: Size,
pub(super) env: Environment,
}
pub(crate) struct RetainNode {
pub(super) _retain: Retain,
pub(super) child: RenderNode,
}
pub(crate) struct EnvNode {
pub(super) env: Environment,
pub(super) child: RenderNode,
}
pub(crate) struct SceneViewNode {
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
pub(super) content: RefCell<Box<dyn waterui_graphics::SceneContent>>,
}
pub(crate) struct GpuSurfaceNode {
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
pub(super) runtime: Rc<RefCell<EmbeddedGpuSurfaceRuntime>>,
}
pub(crate) struct ViewEffectNode {
pub(super) runtime: Rc<RefCell<ViewEffectRuntime>>,
pub(super) child: RefCell<RenderNode>,
pub(super) laid_out: Cell<Size>,
pub(super) env: Environment,
}
pub(crate) struct AppliedFilterNode {
pub(super) runtime: Rc<RefCell<AppliedFilterRuntime>>,
pub(super) child: RenderNode,
pub(super) env: Environment,
}
impl GpuSurfaceNode {
pub(crate) fn flush(&self, renderer: &mut HydrolysisRenderer, ctx: RenderContext) {
let hit_rect = transformed_rect(ctx.hit_transform, ctx.bounds);
renderer.push_gpu_surface_layer(
GpuSurfaceSource::Owned(Rc::clone(&self.runtime)),
ctx.transform,
ctx.bounds,
hit_rect,
);
if self.runtime.borrow().wants_input_events() {
renderer.register_gpu_surface_input_target(
ctx.bounds,
ctx.hit_transform,
Rc::clone(&self.runtime),
);
return;
}
let runtime = Rc::clone(&self.runtime);
renderer.register_trackpad_pan_target(hit_rect, move |dx, dy, phase| {
runtime.borrow_mut().handle_trackpad_pan(dx, dy, phase)
});
}
}
impl ViewEffectNode {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn flush(&self, renderer: &mut HydrolysisRenderer, ctx: RenderContext) {
let (device, queue) = {
let (device, queue) = renderer.state().frame_resources();
(device.clone(), queue.clone())
};
if !ViewEffectRuntime::ensure_setup(
&self.runtime,
renderer.effect_setup_resources(&device, &queue),
renderer.frame_signals(),
) {
return;
}
let mut runtime = self.runtime.borrow_mut();
let input_width = (ctx.bounds.width().max(1.0).round()) as u32;
let input_height = (ctx.bounds.height().max(1.0).round()) as u32;
let output_size = runtime.effect().output_size();
let (output_width, output_height) = output_size.compute(input_width, input_height);
assert!(
!(output_width == 0 || output_height == 0),
"hydrolysis ViewEffect requires non-zero output dimensions"
);
let (input_texture, input_view) = {
let (texture, view) = runtime.input_texture(&device, input_width, input_height);
(texture.clone(), view.clone())
};
renderer.render_child_node_to_texture(
&self.child.borrow(),
ctx,
&self.env,
ChildTextureTarget {
texture: &input_texture,
view: &input_view,
format: wgpu::TextureFormat::Rgba8Unorm,
width: input_width,
height: input_height,
},
);
let (output_texture, output_view) = {
let (texture, view) = runtime.output_texture(&device, output_width, output_height);
(texture.clone(), view.clone())
};
let input = ViewEffectInput {
device: &device,
queue: &queue,
texture: &input_texture,
view: input_view,
format: wgpu::TextureFormat::Rgba8Unorm,
width: input_width,
height: input_height,
};
let output = ViewEffectOutput {
device: &device,
queue: &queue,
texture: &output_texture,
view: output_view,
format: wgpu::TextureFormat::Rgba8Unorm,
width: output_width,
height: output_height,
};
let needs_redraw = runtime.effect_mut().render(&input, &output);
if needs_redraw {
renderer.signals.request_refresh();
}
let image = runtime.register_output_image(
&mut renderer.vello_renderer,
output_texture,
output_width,
output_height,
);
drop(runtime);
renderer.compositor.active_filter_images.push(image.clone());
let image_transform = vello::kurbo::Affine::translate((ctx.bounds.x0, ctx.bounds.y0))
* vello::kurbo::Affine::scale_non_uniform(
ctx.bounds.width() / f64::from(output_width),
ctx.bounds.height() / f64::from(output_height),
);
renderer.scene.draw_image(
&vello::peniko::ImageBrush::new(image),
ctx.transform * image_transform,
);
}
}
impl AppliedFilterNode {
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub(crate) fn flush(&self, renderer: &mut HydrolysisRenderer, ctx: RenderContext) {
let (device, queue) = {
let (device, queue) = renderer.state().frame_resources();
(device.clone(), queue.clone())
};
if !AppliedFilterRuntime::ensure_setup(
&self.runtime,
renderer.effect_setup_resources(&device, &queue),
renderer.frame_signals(),
) {
return;
}
let width = (ctx.bounds.width().max(1.0).round()) as u32;
let height = (ctx.bounds.height().max(1.0).round()) as u32;
let (input_texture, input_view) = {
let mut runtime = self.runtime.borrow_mut();
let (texture, view) = runtime.input_texture(&device, width, height);
(texture.clone(), view.clone())
};
let capture_started_at = Instant::now();
renderer.render_child_node_to_texture(
&self.child,
ctx,
&self.env,
ChildTextureTarget {
texture: &input_texture,
view: &input_view,
format: wgpu::TextureFormat::Rgba8Unorm,
width,
height,
},
);
renderer.frame_applied_filter_capture += capture_started_at.elapsed();
let effect_started_at = Instant::now();
let (image, needs_redraw) = self.runtime.borrow_mut().render_output(
&device,
&queue,
&mut renderer.vello_renderer,
width,
height,
);
renderer.frame_applied_filter_effect += effect_started_at.elapsed();
renderer.frame_applied_filter_count = renderer
.frame_applied_filter_count
.checked_add(1)
.expect("hydrolysis applied filter counter overflow");
if needs_redraw {
renderer.request_redraw();
}
let image_transform = vello::kurbo::Affine::translate((ctx.bounds.x0, ctx.bounds.y0))
* vello::kurbo::Affine::scale_non_uniform(
ctx.bounds.width() / f64::from(image.width),
ctx.bounds.height() / f64::from(image.height),
);
let scene = renderer.scene_mut();
scene.draw_image(
&vello::peniko::ImageBrush::new(image),
ctx.transform * image_transform,
);
}
}
pub(crate) struct DynamicHostNode {
pub(super) source: waterui_core::dynamic::Dynamic,
pub(super) pending: Rc<RefCell<Option<AnyView>>>,
pub(super) env: Environment,
pub(super) child: RenderNode,
}
impl TextNode {
#[cfg(feature = "accessibility")]
pub(super) fn emit_accessibility(
&self,
renderer: &mut HydrolysisRenderer,
ctx: RenderContext,
styled: &StyledStr,
env: &Environment,
) {
if env
.get::<AccessibilityHidden>()
.is_some_and(AccessibilityHidden::is_hidden)
{
return;
}
let plain = styled.to_semantic().to_string();
let default_label = (!plain.is_empty()).then_some(plain);
let Some(label) = renderer.resolve_accessibility_label(env, default_label) else {
return;
};
let mut node = AccessibilityNode::new(
renderer.resolve_accessibility_role(env, AccessibilityNodeRole::Label),
);
node.set_label(label);
let _ = renderer.register_accessibility_node(
node,
transformed_rect(ctx.hit_transform, ctx.bounds),
env,
None,
);
}
#[cfg(not(feature = "accessibility"))]
#[allow(
clippy::unused_self,
reason = "parity with the accessibility-enabled signature"
)]
pub(super) fn emit_accessibility(
&self,
_renderer: &mut HydrolysisRenderer,
_ctx: RenderContext,
_styled: &StyledStr,
_env: &Environment,
) {
}
}
#[cfg(feature = "accessibility")]
pub(super) fn emit_graphics_image_accessibility(
renderer: &mut HydrolysisRenderer,
ctx: RenderContext,
env: &Environment,
) {
if env
.get::<AccessibilityHidden>()
.is_some_and(AccessibilityHidden::is_hidden)
{
return;
}
let mut node = AccessibilityNode::new(
renderer.resolve_accessibility_role(env, AccessibilityNodeRole::Image),
);
if let Some(label) = renderer.resolve_accessibility_label(env, None) {
node.set_label(label);
}
let _ = renderer.register_accessibility_node(
node,
transformed_rect(ctx.hit_transform, ctx.bounds),
env,
None,
);
}
#[cfg(not(feature = "accessibility"))]
pub(super) fn emit_graphics_image_accessibility(
_renderer: &mut HydrolysisRenderer,
_ctx: RenderContext,
_env: &Environment,
) {
}