use std::rc::Rc;
use inset_embedder::{
Matrix4, Offset, Rect, SceneBuilder, Size, SystemUiOverlayStyle, TargetPlatform, View, ViewRef,
};
use inset_foundation::{App, Handle};
use inset_gestures::{HitTestEntry, HitTestResult, HitTestTarget, PointerEvent};
use inset_services::SystemChrome;
use crate::box_::{AnyRenderBox, BoxConstraints, BoxHitTestResult};
use crate::layer::{ContainerLayer, ErasedLayer, OffsetLayerMixin, TransformLayer};
use crate::object::{
AnyRenderObject, Constraints, RenderHandle, RenderObject, RenderObjectData, RenderObjectVTable,
resolve,
};
use crate::painting_context::PaintingContext;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ViewConfiguration {
pub logical_constraints: BoxConstraints,
pub physical_constraints: BoxConstraints,
pub device_pixel_ratio: f64,
}
impl ViewConfiguration {
pub const fn new(
physical_constraints: BoxConstraints,
logical_constraints: BoxConstraints,
device_pixel_ratio: f64,
) -> ViewConfiguration {
ViewConfiguration {
logical_constraints,
physical_constraints,
device_pixel_ratio,
}
}
pub fn from_view(view: &dyn View) -> ViewConfiguration {
let metrics = view.metrics();
let physical_constraints =
BoxConstraints::from_view_constraints(metrics.physical_constraints);
let device_pixel_ratio = metrics.device_pixel_ratio;
ViewConfiguration {
logical_constraints: physical_constraints / device_pixel_ratio,
physical_constraints,
device_pixel_ratio,
}
}
pub fn to_matrix(&self) -> Matrix4 {
Matrix4::scale(
self.device_pixel_ratio as f32,
self.device_pixel_ratio as f32,
)
}
pub fn should_update_matrix(&self, old_configuration: &ViewConfiguration) -> bool {
old_configuration.device_pixel_ratio != self.device_pixel_ratio
}
pub fn to_physical_size(&self, logical_size: Size) -> Size {
self.physical_constraints
.constrain(logical_size * self.device_pixel_ratio)
}
}
impl Default for ViewConfiguration {
fn default() -> ViewConfiguration {
ViewConfiguration::new(
BoxConstraints::new().max_width(0.0).max_height(0.0),
BoxConstraints::new().max_width(0.0).max_height(0.0),
1.0,
)
}
}
pub struct RenderView {
render_object: RenderObjectData,
child: Option<AnyRenderBox>,
size: Size,
configuration: Option<ViewConfiguration>,
view: ViewRef,
root_transform: Option<Matrix4>,
pub automatic_system_ui_adjustment: bool,
}
impl RenderView {
pub fn new(
app: &mut App,
child: Option<AnyRenderBox>,
configuration: Option<ViewConfiguration>,
view: ViewRef,
) -> RenderHandle<RenderView> {
let this = RenderHandle::from_handle(app.create(RenderView {
render_object: RenderObjectData::new(),
child: None,
size: Size::ZERO,
configuration: None,
view,
root_transform: None,
automatic_system_ui_adjustment: true,
}));
let data = this.render_object_data_mut(app);
data.object_vtable = Some(&VTABLE);
data.was_repaint_boundary = true;
data.needs_compositing = true;
if let Some(configuration) = configuration {
this.set_configuration(app, configuration);
}
this.set_child(app, child);
this
}
pub fn as_object(self: RenderHandle<Self>) -> AnyRenderObject {
AnyRenderObject::from_vtable(self.id(), const { &VTABLE })
}
pub fn child(self: RenderHandle<Self>, app: &App) -> Option<AnyRenderBox> {
self.get(app).child
}
pub fn set_child(self: RenderHandle<Self>, app: &mut App, value: Option<AnyRenderBox>) {
if let Some(old) = self.get(app).child {
self.as_object().drop_child(app, old.as_object());
}
self.get_mut(app).child = value;
if let Some(new) = value {
self.as_object().adopt_child(app, new.as_object());
}
}
pub fn size(self: RenderHandle<Self>, app: &App) -> Size {
self.get(app).size
}
pub fn configuration(self: RenderHandle<Self>, app: &App) -> ViewConfiguration {
self.get(app)
.configuration
.expect("RenderView has not been given a configuration yet")
}
pub fn set_configuration(self: RenderHandle<Self>, app: &mut App, value: ViewConfiguration) {
if self.get(app).configuration == Some(value) {
return;
}
let old_configuration = self.get(app).configuration;
self.get_mut(app).configuration = Some(value);
if self.get(app).root_transform.is_none() {
return;
}
if old_configuration.is_none_or(|old| value.should_update_matrix(&old)) {
let root_layer = self.update_matrices_and_create_new_root_layer(app);
self.as_object()
.replace_root_layer(app, root_layer.as_offset_layer());
}
debug_assert!(self.get(app).root_transform.is_some());
self.as_object().mark_needs_layout(app);
}
pub fn has_configuration(self: RenderHandle<Self>, app: &App) -> bool {
self.get(app).configuration.is_some()
}
pub fn constraints(self: RenderHandle<Self>, app: &App) -> BoxConstraints {
debug_assert!(
self.has_configuration(app),
"Constraints are not available because RenderView has not been given a configuration yet."
);
self.configuration(app).logical_constraints
}
pub fn flutter_view(self: RenderHandle<Self>, app: &App) -> ViewRef {
self.get(app).view.clone()
}
pub fn prepare_initial_frame(self: RenderHandle<Self>, app: &mut App) {
debug_assert!(
self.as_object().owner(app).is_some(),
"attach the RenderView to a PipelineOwner before calling prepare_initial_frame"
);
debug_assert!(
self.get(app).root_transform.is_none(),
"prepare_initial_frame must only be called once"
);
debug_assert!(
self.has_configuration(app),
"set a configuration before calling prepare_initial_frame"
);
self.as_object().schedule_initial_layout(app);
let root_layer = self.update_matrices_and_create_new_root_layer(app);
self.as_object()
.schedule_initial_paint(app, root_layer.as_container_layer());
debug_assert!(self.get(app).root_transform.is_some());
}
fn update_matrices_and_create_new_root_layer(
self: RenderHandle<Self>,
app: &mut App,
) -> Handle<TransformLayer> {
debug_assert!(self.has_configuration(app));
let root_transform = self.configuration(app).to_matrix();
self.get_mut(app).root_transform = Some(root_transform);
let root_layer = TransformLayer::new(app);
root_layer.set_transform(app, root_transform);
root_layer.attach(app, self.id());
debug_assert!(self.get(app).root_transform.is_some());
root_layer
}
pub fn hit_test(
self: RenderHandle<Self>,
app: &mut App,
result: &mut HitTestResult,
position: Offset,
) -> bool {
if let Some(child) = self.child(app) {
child.hit_test(app, &mut BoxHitTestResult::wrap(result), position);
}
result.add(HitTestEntry::new(self));
true
}
pub fn composite_frame(self: RenderHandle<Self>, app: &mut App) {
debug_assert!(
self.has_configuration(app),
"set the RenderView configuration before calling composite_frame"
);
debug_assert!(
self.get(app).root_transform.is_some(),
"call prepare_initial_frame before calling composite_frame"
);
let layer = self
.as_object()
.layer(app)
.expect("call prepare_initial_frame before calling composite_frame");
let scene = layer.build_scene(app, SceneBuilder::new());
if self.get(app).automatic_system_ui_adjustment {
self.update_system_chrome(app);
}
debug_assert!(
self.configuration(app)
.logical_constraints
.is_satisfied_by(self.size(app))
);
self.get(app).view.present(std::sync::Arc::new(scene));
}
fn update_system_chrome(self: RenderHandle<Self>, app: &mut App) {
let Some(layer) = self.as_object().layer(app) else {
return;
};
let bounds = self.paint_bounds(app);
let padding = self.get(app).view.metrics().padding;
let top = Offset::new(
bounds.center().dx(),
padding.top / 2.0,
);
let bottom = Offset::new(
bounds.center().dx(),
bounds.bottom - 1.0 - padding.bottom / 2.0,
);
let layer = layer.as_layer();
let upper = layer.find::<SystemUiOverlayStyle>(app, top);
let is_android = app.platform().target_platform() == TargetPlatform::Android;
let lower = is_android
.then(|| layer.find::<SystemUiOverlayStyle>(app, bottom))
.flatten();
let Some(style) = merged_overlay_style(upper, lower, is_android) else {
return;
};
SystemChrome::set_system_ui_overlay_style(app, &style);
}
pub fn paint_bounds(self: RenderHandle<Self>, app: &App) -> Rect {
Offset::ZERO & (self.size(app) * self.configuration(app).device_pixel_ratio)
}
}
fn merged_overlay_style(
upper: Option<Rc<SystemUiOverlayStyle>>,
lower: Option<Rc<SystemUiOverlayStyle>>,
is_android: bool,
) -> Option<SystemUiOverlayStyle> {
if let (Some(upper), Some(lower)) = (&upper, &lower) {
return Some(SystemUiOverlayStyle {
status_bar_brightness: upper.status_bar_brightness,
status_bar_icon_brightness: upper.status_bar_icon_brightness,
status_bar_color: upper.status_bar_color,
system_status_bar_contrast_enforced: upper.system_status_bar_contrast_enforced,
system_navigation_bar_color: lower.system_navigation_bar_color,
system_navigation_bar_divider_color: lower.system_navigation_bar_divider_color,
system_navigation_bar_icon_brightness: lower.system_navigation_bar_icon_brightness,
system_navigation_bar_contrast_enforced: lower.system_navigation_bar_contrast_enforced,
});
}
let defined = upper.or(lower)?;
if !is_android {
return Some(SystemUiOverlayStyle {
status_bar_brightness: defined.status_bar_brightness,
status_bar_icon_brightness: defined.status_bar_icon_brightness,
status_bar_color: defined.status_bar_color,
system_status_bar_contrast_enforced: defined.system_status_bar_contrast_enforced,
..SystemUiOverlayStyle::new()
});
}
Some(*defined)
}
impl RenderObject for RenderView {
fn render_object_data(self: RenderHandle<Self>, app: &App) -> &RenderObjectData {
&self.get(app).render_object
}
fn render_object_data_mut(self: RenderHandle<Self>, app: &mut App) -> &mut RenderObjectData {
&mut self.get_mut(app).render_object
}
fn perform_resize(self: RenderHandle<Self>, _app: &mut App) {
panic!("RenderView is never sized by its parent");
}
fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
debug_assert!(self.get(app).root_transform.is_some());
let constraints = self.constraints(app);
let sized_by_child = !constraints.is_tight();
let child = self.child(app);
if let Some(child) = child {
child.layout(app, constraints, sized_by_child);
}
let size = match child {
Some(child) if sized_by_child => child.size(app),
_ => constraints.smallest(),
};
self.get_mut(app).size = size;
debug_assert!(size.is_finite());
debug_assert!(constraints.is_satisfied_by(size));
}
fn visit_children(
self: RenderHandle<Self>,
app: &App,
visitor: &mut dyn FnMut(AnyRenderObject),
) {
if let Some(child) = self.child(app) {
visitor(child.as_object());
}
}
fn apply_paint_transform(
self: RenderHandle<Self>,
app: &App,
child: AnyRenderObject,
transform: &mut Matrix4,
) {
let root_transform = self
.get(app)
.root_transform
.as_ref()
.expect("the view has a configuration");
crate::object::multiply(transform, root_transform);
debug_assert!(child.parent(app).map(AnyRenderObject::id) == Some(self.id()));
}
fn is_repaint_boundary(self: RenderHandle<Self>, _app: &App) -> bool {
true
}
fn paint(
self: RenderHandle<Self>,
app: &mut App,
context: &mut PaintingContext,
offset: Offset,
) {
if let Some(child) = self.child(app) {
context.paint_child(app, child.as_object(), offset);
}
}
}
impl HitTestTarget for RenderHandle<RenderView> {
fn handle_event(&self, _app: &mut App, _event: &PointerEvent, _entry: &HitTestEntry) {}
}
static VTABLE: RenderObjectVTable = RenderObjectVTable::of::<RenderView>(
|app, id, child| <RenderView as RenderObject>::setup_parent_data(resolve(id), app, child),
|app, id| RenderView::paint_bounds(resolve(id), app),
|app, id, child, transform| {
<RenderView as RenderObject>::apply_paint_transform(resolve(id), app, child, transform)
},
|app, id| <RenderView as RenderObject>::perform_resize(resolve(id), app),
|app, id| crate::object::RenderObjectBase::mark_needs_layout(resolve::<RenderView>(id), app),
None,
None,
);
#[cfg(test)]
mod tests {
use inset_foundation::AppCell;
use std::cell::Cell;
use std::rc::Rc;
use inset_embedder::{ViewId, ViewMetrics};
use super::*;
use crate::box_::RenderBox;
use crate::layer::TransformLayer;
use crate::pipeline_owner::PipelineOwner;
use crate::proxy_box::RenderConstrainedBox;
use crate::shifted_box::RenderPadding;
use inset_painting::EdgeInsetsGeometry;
struct TestView {
metrics: ViewMetrics,
presented: Rc<Cell<u32>>,
}
impl View for TestView {
fn id(&self) -> ViewId {
ViewId(0)
}
fn metrics(&self) -> ViewMetrics {
self.metrics
}
fn present(&self, _scene: std::sync::Arc<inset_embedder::Scene>) {
self.presented.set(self.presented.get() + 1);
}
}
fn test_view(presented: &Rc<Cell<u32>>) -> ViewRef {
Rc::new(TestView {
metrics: ViewMetrics::default(),
presented: Rc::clone(presented),
})
}
fn create_view_configuration(size: Size, device_pixel_ratio: f64) -> ViewConfiguration {
let constraints = BoxConstraints::tight(size);
ViewConfiguration::new(
constraints * device_pixel_ratio,
constraints,
device_pixel_ratio,
)
}
#[test]
fn view_configuration_equality() {
let a = create_view_configuration(Size::new(800.0, 600.0), 1.0);
let b = create_view_configuration(Size::new(800.0, 600.0), 1.0);
let c = create_view_configuration(Size::new(800.0, 600.0), 3.0);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn constraints_are_derived_from_configuration() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let presented = Rc::new(Cell::new(0));
let view = RenderView::new(&mut app, None, None, test_view(&presented));
assert!(!view.has_configuration(&app));
let constraints = BoxConstraints::new()
.min_width(1.0)
.max_width(2.0)
.min_height(3.0)
.max_height(4.0);
view.set_configuration(
&mut app,
ViewConfiguration::new(constraints * 3.0, constraints, 3.0),
);
assert_eq!(view.constraints(&app), constraints);
}
#[test]
fn configuration_can_change_before_prepare_initial_frame() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let presented = Rc::new(Cell::new(0));
let view = RenderView::new(&mut app, None, None, test_view(&presented));
view.set_configuration(
&mut app,
create_view_configuration(Size::new(100.0, 200.0), 3.0),
);
view.set_configuration(
&mut app,
create_view_configuration(Size::new(200.0, 300.0), 2.0),
);
let owner = PipelineOwner::new(&mut app, None);
owner.set_root_node(&mut app, Some(view.as_object()));
view.prepare_initial_frame(&mut app);
}
#[test]
fn does_not_replace_the_root_layer_unnecessarily() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let presented = Rc::new(Cell::new(0));
let view = RenderView::new(
&mut app,
None,
Some(create_view_configuration(Size::new(100.0, 100.0), 1.0)),
test_view(&presented),
);
let owner = PipelineOwner::new(&mut app, None);
owner.set_root_node(&mut app, Some(view.as_object()));
view.prepare_initial_frame(&mut app);
owner.flush_layout(&mut app);
owner.flush_compositing_bits(&mut app);
owner.flush_paint(&mut app);
assert!(!view.as_object().debug_needs_paint(&app));
view.set_configuration(
&mut app,
create_view_configuration(Size::new(100.0, 1117.0), 1.0),
);
assert!(
!view.as_object().debug_needs_paint(&app),
"a resize keeps the root layer"
);
view.set_configuration(
&mut app,
create_view_configuration(Size::new(100.0, 1117.0), 5.0),
);
assert!(
view.as_object().debug_needs_paint(&app),
"a new ratio replaces it"
);
let layer = view.as_object().debug_layer(&app).expect("root layer");
let transform = app
.handle::<TransformLayer>(layer.id())
.expect("the root is a TransformLayer");
assert_eq!(transform.transform(&app), Some(Matrix4::scale(5.0, 5.0)));
}
#[test]
fn accounts_for_device_pixel_ratio_in_paint_bounds_and_presents() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let presented = Rc::new(Cell::new(0));
let child =
RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(10.0, 10.0)), None);
let view = RenderView::new(
&mut app,
Some(child.as_box()),
Some(create_view_configuration(Size::new(800.0, 600.0), 2.0)),
test_view(&presented),
);
let owner = PipelineOwner::new(&mut app, None);
owner.set_root_node(&mut app, Some(view.as_object()));
view.prepare_initial_frame(&mut app);
owner.flush_layout(&mut app);
owner.flush_compositing_bits(&mut app);
owner.flush_paint(&mut app);
view.composite_frame(&mut app);
assert_eq!(view.size(&app), Size::new(800.0, 600.0));
assert_eq!(
view.paint_bounds(&app),
Offset::ZERO & Size::new(1600.0, 1200.0)
);
assert_eq!(presented.get(), 1);
}
#[test]
fn coordinates_convert_through_the_paint_transforms_below_the_view() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let presented = Rc::new(Cell::new(0));
let child =
RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(30.0, 40.0)), None);
let padding = RenderPadding::new(
&mut app,
EdgeInsetsGeometry::from_ltrb(10.0, 20.0, 0.0, 0.0),
None,
Some(child.as_box()),
);
let view = RenderView::new(
&mut app,
Some(padding.as_box()),
Some(create_view_configuration(Size::new(800.0, 600.0), 2.0)),
test_view(&presented),
);
let owner = PipelineOwner::new(&mut app, None);
owner.set_root_node(&mut app, Some(view.as_object()));
view.prepare_initial_frame(&mut app);
owner.flush_layout(&mut app);
let child = child.as_box();
assert_eq!(
child.local_to_global(&app, Offset::ZERO, None),
Offset::new(10.0, 20.0)
);
assert_eq!(
child.global_to_local(&app, Offset::new(15.0, 25.0), None),
Offset::new(5.0, 5.0)
);
assert_eq!(
child.local_to_global(&app, Offset::new(1.0, 1.0), Some(padding.as_object())),
Offset::new(11.0, 21.0)
);
assert_eq!(
padding.as_box().global_to_local(
&app,
Offset::new(10.0, 20.0),
Some(child.as_object())
),
Offset::new(20.0, 40.0)
);
assert!(
child
.as_object()
.paint_bounds(&app)
.inflate(70.0)
.contains(child.global_to_local(&app, Offset::new(100.0, 100.0), None))
);
}
#[test]
fn a_proxy_box_adds_no_paint_transform() {
let cell = AppCell::new();
let mut app = cell.borrow_mut();
let presented = Rc::new(Cell::new(0));
let child =
RenderConstrainedBox::new(&mut app, BoxConstraints::tight(Size::new(30.0, 40.0)), None);
let proxy =
RenderConstrainedBox::new(&mut app, BoxConstraints::new(), Some(child.as_box()));
let padding = RenderPadding::new(
&mut app,
EdgeInsetsGeometry::from_ltrb(10.0, 20.0, 0.0, 0.0),
None,
Some(proxy.as_box()),
);
let view = RenderView::new(
&mut app,
Some(padding.as_box()),
Some(create_view_configuration(Size::new(800.0, 600.0), 2.0)),
test_view(&presented),
);
let owner = PipelineOwner::new(&mut app, None);
owner.set_root_node(&mut app, Some(view.as_object()));
view.prepare_initial_frame(&mut app);
owner.flush_layout(&mut app);
assert_eq!(
child.as_box().local_to_global(&app, Offset::ZERO, None),
Offset::new(10.0, 20.0)
);
assert_eq!(
child
.as_box()
.global_to_local(&app, Offset::new(15.0, 25.0), None),
Offset::new(5.0, 5.0)
);
}
}