Packages

Write native GUI directly from Elixir using declarative API.

Current section

Files

Jump to
emerge native emerge_skia src tree scene.rs
Raw

native/emerge_skia/src/tree/scene.rs

use std::sync::Arc;
use super::element::{Element, Frame, RetainedPaintPhase};
use super::geometry::{ClipShape, Rect, ShapeBounds};
use super::scrollbar as tree_scrollbar;
use super::scrollbar::ScrollbarMetrics;
use super::transform::{Affine2, InteractionClip, element_transform};
#[derive(Clone, Copy, Debug)]
struct LocalSceneGeometry {
self_shape: ShapeBounds,
host_clip: ClipShape,
scrollbar_x: Option<ScrollbarMetrics>,
scrollbar_y: Option<ScrollbarMetrics>,
}
#[derive(Clone, Debug, Default)]
pub struct SceneContext {
pub scroll_dx: f32,
pub scroll_dy: f32,
pub visible_clip: Option<ClipShape>,
pub nearby_visible_clip: Option<ClipShape>,
pub front_nearby_subtree: bool,
pub front_nearby_root: bool,
pub interaction_transform: Affine2,
pub interaction_clips: Arc<[InteractionClip]>,
pub nearby_interaction_clips: Arc<[InteractionClip]>,
}
#[derive(Clone, Debug)]
pub struct ResolvedNodeState {
pub frame: Frame,
pub adjusted_frame: Frame,
pub render_frame: Frame,
pub adjusted_render_frame: Frame,
pub self_shape: ShapeBounds,
pub host_clip: ClipShape,
pub visible: bool,
pub child_visible_clip: ClipShape,
pub nearby_visible_clip: Option<ClipShape>,
pub scrollbar_x: Option<ScrollbarMetrics>,
pub scrollbar_y: Option<ScrollbarMetrics>,
pub local_scroll_x: f32,
pub local_scroll_y: f32,
pub clip_nearby: bool,
pub front_nearby_subtree: bool,
pub front_nearby_root: bool,
pub interaction_transform: Affine2,
pub interaction_inverse: Option<Affine2>,
pub interaction_clips: Arc<[InteractionClip]>,
pub nearby_interaction_clips: Arc<[InteractionClip]>,
}
impl ResolvedNodeState {
fn accumulated_scroll(&self) -> (f32, f32) {
(
self.frame.x - self.adjusted_frame.x,
self.frame.y - self.adjusted_frame.y,
)
}
}
pub fn resolve_node_state(element: &Element, ctx: SceneContext) -> Option<ResolvedNodeState> {
let frame = element.layout.frame?;
let render_frame = element.layout.render_frame.unwrap_or(frame);
let scene = local_scene_geometry(
render_frame,
&element.layout.effective,
(element.layout.scroll_x, element.layout.scroll_y),
element.runtime.scrollbar_hover_axis,
);
let adjusted_frame = Frame {
x: frame.x - ctx.scroll_dx,
y: frame.y - ctx.scroll_dy,
..frame
};
let adjusted_render_frame = Frame {
x: render_frame.x - ctx.scroll_dx,
y: render_frame.y - ctx.scroll_dy,
..render_frame
};
let self_shape = scene.self_shape.offset(ctx.scroll_dx, ctx.scroll_dy);
let clip_nearby = element.layout.effective.clip_nearby.unwrap_or(false);
let inherited_clip = if ctx.front_nearby_subtree {
ctx.nearby_visible_clip
} else {
ctx.visible_clip
};
let host_clip = scene.host_clip.offset(ctx.scroll_dx, ctx.scroll_dy);
let local_transform = element_transform(adjusted_render_frame, &element.layout.effective);
let child_visible_clip = if local_transform.is_identity() {
inherited_clip
.map(|clip| super::geometry::intersect_clip(Some(clip), host_clip))
.unwrap_or(host_clip)
} else {
host_clip
};
let nearby_visible_clip = if clip_nearby {
Some(if local_transform.is_identity() {
super::geometry::intersect_clip(ctx.nearby_visible_clip, host_clip)
} else {
host_clip
})
} else {
ctx.nearby_visible_clip
};
let scrollbar_x = scene
.scrollbar_x
.map(|metrics| offset_scrollbar_metrics(metrics, &ctx));
let scrollbar_y = scene
.scrollbar_y
.map(|metrics| offset_scrollbar_metrics(metrics, &ctx));
let interaction_transform = ctx.interaction_transform.then(local_transform);
let interaction_inverse = interaction_transform.inverse();
let visible = visible_in_inherited_clip(
self_shape.rect,
interaction_transform,
inherited_clip,
ctx.interaction_transform,
);
Some(ResolvedNodeState {
frame,
adjusted_frame,
render_frame,
adjusted_render_frame,
self_shape,
host_clip,
visible,
child_visible_clip,
nearby_visible_clip,
scrollbar_x,
scrollbar_y,
local_scroll_x: element.layout.scroll_x,
local_scroll_y: element.layout.scroll_y,
clip_nearby,
front_nearby_subtree: ctx.front_nearby_subtree,
front_nearby_root: ctx.front_nearby_root,
interaction_transform,
interaction_inverse,
interaction_clips: ctx.interaction_clips,
nearby_interaction_clips: ctx.nearby_interaction_clips,
})
}
fn visible_in_inherited_clip(
rect: Rect,
rect_to_screen: Affine2,
inherited_clip: Option<ClipShape>,
clip_to_screen: Affine2,
) -> bool {
if rect.width <= 0.0 || rect.height <= 0.0 {
return false;
}
let Some(clip) = inherited_clip else {
return true;
};
let rect_bounds = rect_to_screen.map_rect_aabb(rect);
let clip_bounds = clip_to_screen.map_rect_aabb(clip.rect);
rect_bounds.intersect(clip_bounds).is_some()
}
fn local_scene_geometry(
frame: Frame,
attrs: &super::attrs::Attrs,
scroll: (f32, f32),
hover_axis: Option<super::attrs::ScrollbarHoverAxis>,
) -> LocalSceneGeometry {
LocalSceneGeometry {
self_shape: super::geometry::self_shape(frame, attrs),
host_clip: super::geometry::host_clip_shape(frame, attrs),
scrollbar_x: tree_scrollbar::horizontal_metrics(frame, attrs, scroll.0, hover_axis),
scrollbar_y: tree_scrollbar::vertical_metrics(frame, attrs, scroll.1, hover_axis),
}
}
fn interaction_clip_chain_with(
clips: &[InteractionClip],
clip: InteractionClip,
) -> Arc<[InteractionClip]> {
let mut next = Vec::with_capacity(clips.len() + 1);
next.extend_from_slice(clips);
next.push(clip);
Arc::from(next.into_boxed_slice())
}
pub fn child_context(state: ResolvedNodeState, phase: RetainedPaintPhase) -> SceneContext {
let (scroll_dx, scroll_dy) = state.accumulated_scroll();
let interaction_clips = if matches!(phase, RetainedPaintPhase::Overlay(_)) {
state.interaction_clips.clone()
} else {
interaction_clip_chain_with(
&state.interaction_clips,
InteractionClip::new(state.host_clip, state.interaction_transform),
)
};
let nearby_interaction_clips = if state.clip_nearby {
interaction_clip_chain_with(
&state.nearby_interaction_clips,
InteractionClip::new(state.host_clip, state.interaction_transform),
)
} else {
state.nearby_interaction_clips.clone()
};
match phase {
RetainedPaintPhase::Children => SceneContext {
scroll_dx: scroll_dx + state.local_scroll_x,
scroll_dy: scroll_dy + state.local_scroll_y,
visible_clip: Some(state.child_visible_clip),
nearby_visible_clip: state.nearby_visible_clip,
front_nearby_subtree: state.front_nearby_subtree,
front_nearby_root: false,
interaction_transform: state.interaction_transform,
interaction_clips,
nearby_interaction_clips,
},
RetainedPaintPhase::BehindContent => SceneContext {
scroll_dx,
scroll_dy,
visible_clip: Some(state.child_visible_clip),
nearby_visible_clip: state.nearby_visible_clip,
front_nearby_subtree: state.front_nearby_subtree,
front_nearby_root: false,
interaction_transform: state.interaction_transform,
interaction_clips,
nearby_interaction_clips,
},
RetainedPaintPhase::Overlay(_) => SceneContext {
scroll_dx,
scroll_dy,
visible_clip: state.nearby_visible_clip,
nearby_visible_clip: state.nearby_visible_clip,
front_nearby_subtree: true,
front_nearby_root: true,
interaction_transform: state.interaction_transform,
interaction_clips: nearby_interaction_clips.clone(),
nearby_interaction_clips,
},
}
}
fn offset_scrollbar_metrics(metrics: ScrollbarMetrics, ctx: &SceneContext) -> ScrollbarMetrics {
ScrollbarMetrics {
track_x: metrics.track_x - ctx.scroll_dx,
track_y: metrics.track_y - ctx.scroll_dy,
thumb_x: metrics.thumb_x - ctx.scroll_dx,
thumb_y: metrics.thumb_y - ctx.scroll_dy,
track_start: metrics.track_start
- match metrics.axis {
super::scrollbar::ScrollbarAxis::X => ctx.scroll_dx,
super::scrollbar::ScrollbarAxis::Y => ctx.scroll_dy,
},
thumb_start: metrics.thumb_start
- match metrics.axis {
super::scrollbar::ScrollbarAxis::X => ctx.scroll_dx,
super::scrollbar::ScrollbarAxis::Y => ctx.scroll_dy,
},
..metrics
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tree::attrs::Attrs;
use crate::tree::element::{Element, ElementKind, NearbySlot, NodeId};
use crate::tree::geometry::Rect;
fn make_element(id: u8, attrs: Attrs, frame: Frame) -> Element {
let mut element = Element::with_attrs(
NodeId::from_term_bytes(vec![id]),
ElementKind::El,
Vec::new(),
attrs,
);
element.layout.frame = Some(frame);
element
}
#[test]
fn child_context_always_applies_host_clip_for_children() {
let parent = make_element(
1,
Attrs::default(),
Frame {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
content_width: 100.0,
content_height: 50.0,
},
);
let state = resolve_node_state(
&parent,
SceneContext {
visible_clip: Some(ClipShape {
rect: Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 300.0,
},
radii: None,
}),
..SceneContext::default()
},
)
.expect("state should resolve");
let child_visible_clip = state.child_visible_clip;
let child_ctx = child_context(state, RetainedPaintPhase::Children);
assert_eq!(
child_ctx.visible_clip,
Some(child_visible_clip),
"all hosts should tighten child visibility to the host clip"
);
}
#[test]
fn child_context_always_applies_host_clip_for_behind_content() {
let attrs = Attrs {
scrollbar_y: Some(true),
..Attrs::default()
};
let parent = make_element(
2,
attrs,
Frame {
x: 10.0,
y: 20.0,
width: 100.0,
height: 50.0,
content_width: 100.0,
content_height: 50.0,
},
);
let state = resolve_node_state(
&parent,
SceneContext {
visible_clip: Some(ClipShape {
rect: Rect {
x: 0.0,
y: 0.0,
width: 400.0,
height: 300.0,
},
radii: None,
}),
..SceneContext::default()
},
)
.expect("state should resolve");
let child_visible_clip = state.child_visible_clip;
let child_ctx = child_context(state, RetainedPaintPhase::BehindContent);
assert_eq!(
child_ctx.visible_clip,
Some(child_visible_clip),
"behind content should render inside the host clip"
);
}
#[test]
fn resolve_node_state_keeps_scrolled_inherited_clip_in_screen_space() {
let parent_attrs = Attrs {
scrollbar_y: Some(true),
scroll_y: Some(20.0),
..Attrs::default()
};
let parent = make_element(
3,
parent_attrs,
Frame {
x: 0.0,
y: 100.0,
width: 120.0,
height: 50.0,
content_width: 120.0,
content_height: 150.0,
},
);
let child = make_element(
4,
Attrs::default(),
Frame {
x: 0.0,
y: 130.0,
width: 120.0,
height: 20.0,
content_width: 120.0,
content_height: 20.0,
},
);
let parent_state = resolve_node_state(&parent, SceneContext::default())
.expect("parent state should resolve");
let child_ctx = child_context(parent_state.clone(), RetainedPaintPhase::Children);
let child_state =
resolve_node_state(&child, child_ctx.clone()).expect("child state should resolve");
assert_eq!(child_state.adjusted_frame.y, 110.0);
assert_eq!(
child_ctx.visible_clip,
Some(parent_state.child_visible_clip),
"child inherited clip should stay in screen space after parent scroll"
);
}
#[test]
fn resolve_node_state_uses_current_frame_geometry() {
let frame = Frame {
x: 40.0,
y: 60.0,
width: 120.0,
height: 50.0,
content_width: 120.0,
content_height: 140.0,
};
let attrs = Attrs {
scrollbar_y: Some(true),
..Attrs::default()
};
let mut element = make_element(8, attrs, frame);
element.layout.frame = Some(frame);
let state =
resolve_node_state(&element, SceneContext::default()).expect("state should resolve");
assert_eq!(state.self_shape.rect.x, 40.0);
assert_eq!(state.self_shape.rect.y, 60.0);
assert_eq!(state.host_clip.rect.x, 40.0);
assert_eq!(state.host_clip.rect.y, 60.0);
assert_eq!(state.scrollbar_y.expect("scrollbar").track_y, 60.0);
}
#[test]
fn child_context_accumulates_ancestor_and_local_scroll_for_grandchildren() {
let root_attrs = Attrs {
scrollbar_y: Some(true),
scroll_y: Some(20.0),
..Attrs::default()
};
let root = make_element(
5,
root_attrs,
Frame {
x: 0.0,
y: 0.0,
width: 120.0,
height: 80.0,
content_width: 120.0,
content_height: 200.0,
},
);
let child_attrs = Attrs {
scrollbar_y: Some(true),
scroll_y: Some(5.0),
..Attrs::default()
};
let child = make_element(
6,
child_attrs,
Frame {
x: 0.0,
y: 30.0,
width: 120.0,
height: 60.0,
content_width: 120.0,
content_height: 120.0,
},
);
let grandchild = make_element(
7,
Attrs::default(),
Frame {
x: 0.0,
y: 50.0,
width: 120.0,
height: 20.0,
content_width: 120.0,
content_height: 20.0,
},
);
let root_state =
resolve_node_state(&root, SceneContext::default()).expect("root state should resolve");
let child_state = resolve_node_state(
&child,
child_context(root_state, RetainedPaintPhase::Children),
)
.expect("child state should resolve");
let grandchild_ctx = child_context(child_state, RetainedPaintPhase::Children);
let grandchild_state = resolve_node_state(&grandchild, grandchild_ctx.clone())
.expect("grandchild should resolve");
assert_eq!(grandchild_ctx.scroll_dy, 25.0);
assert_eq!(grandchild_state.adjusted_frame.y, 25.0);
}
#[test]
fn overlay_child_context_marks_only_overlay_root_as_front_nearby_root() {
let host = make_element(
9,
Attrs::default(),
Frame {
x: 0.0,
y: 0.0,
width: 160.0,
height: 80.0,
content_width: 160.0,
content_height: 80.0,
},
);
let overlay = make_element(
10,
Attrs::default(),
Frame {
x: 20.0,
y: 10.0,
width: 120.0,
height: 60.0,
content_width: 120.0,
content_height: 60.0,
},
);
let descendant = make_element(
11,
Attrs::default(),
Frame {
x: 30.0,
y: 20.0,
width: 80.0,
height: 20.0,
content_width: 80.0,
content_height: 20.0,
},
);
let host_state =
resolve_node_state(&host, SceneContext::default()).expect("host state should resolve");
let overlay_ctx =
child_context(host_state, RetainedPaintPhase::Overlay(NearbySlot::InFront));
assert!(overlay_ctx.front_nearby_subtree);
assert!(overlay_ctx.front_nearby_root);
let overlay_state =
resolve_node_state(&overlay, overlay_ctx).expect("overlay should resolve");
assert!(overlay_state.front_nearby_subtree);
assert!(overlay_state.front_nearby_root);
let descendant_ctx = child_context(overlay_state, RetainedPaintPhase::Children);
assert!(descendant_ctx.front_nearby_subtree);
assert!(!descendant_ctx.front_nearby_root);
let descendant_state =
resolve_node_state(&descendant, descendant_ctx).expect("descendant should resolve");
assert!(descendant_state.front_nearby_subtree);
assert!(!descendant_state.front_nearby_root);
}
}