use frust_core::{
BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent, LayoutCtx,
PaintCtx, PaintScene, SemanticsCtx, View, Widget, WindowInsets, any,
};
use kurbo::{Point, Size};
pub struct SafeAreaView<State: 'static> {
left: bool,
top: bool,
right: bool,
bottom: bool,
minimum: crate::EdgeInsets,
child: frust_core::AnyView<State>,
}
pub fn safe_area<State: 'static, V: View<State>>(child: V) -> SafeAreaView<State> {
SafeAreaView {
left: true,
top: true,
right: true,
bottom: true,
minimum: crate::EdgeInsets::all(0.0),
child: any(child),
}
}
impl<State: 'static> SafeAreaView<State> {
pub fn left(mut self, enabled: bool) -> Self {
self.left = enabled;
self
}
pub fn top(mut self, enabled: bool) -> Self {
self.top = enabled;
self
}
pub fn right(mut self, enabled: bool) -> Self {
self.right = enabled;
self
}
pub fn bottom(mut self, enabled: bool) -> Self {
self.bottom = enabled;
self
}
pub fn minimum(mut self, minimum: crate::EdgeInsets) -> Self {
self.minimum = minimum;
self
}
}
pub struct SafeAreaWidget {
left: bool,
top: bool,
right: bool,
bottom: bool,
minimum: crate::EdgeInsets,
child: ChildPod,
}
impl<State: 'static> View<State> for SafeAreaView<State> {
type Element = SafeAreaWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> SafeAreaWidget {
SafeAreaWidget {
left: self.left,
top: self.top,
right: self.right,
bottom: self.bottom,
minimum: self.minimum,
child: crate::authoring::build_child(&self.child, ctx),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut SafeAreaWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if prev.left != self.left
|| prev.top != self.top
|| prev.right != self.right
|| prev.bottom != self.bottom
|| prev.minimum != self.minimum
{
element.left = self.left;
element.top = self.top;
element.right = self.right;
element.bottom = self.bottom;
element.minimum = self.minimum;
flags |= ChangeFlags::LAYOUT;
}
flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
flags
}
fn teardown(&self, element: &mut SafeAreaWidget, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
}
}
impl SafeAreaWidget {
fn consumed_insets(&self, insets: WindowInsets) -> WindowInsets {
insets.consuming(self.left, self.top, self.right, self.bottom)
}
}
impl Widget for SafeAreaWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let padding = ctx.window_insets().padding();
let left = (if self.left { padding.left } else { 0.0 }).max(self.minimum.left);
let top = (if self.top { padding.top } else { 0.0 }).max(self.minimum.top);
let right = (if self.right { padding.right } else { 0.0 }).max(self.minimum.right);
let bottom = (if self.bottom { padding.bottom } else { 0.0 }).max(self.minimum.bottom);
let h = left + right;
let v = top + bottom;
let child_bc = BoxConstraints::new(
Size::new(
(bc.min().width - h).max(0.0),
(bc.min().height - v).max(0.0),
),
Size::new(
(bc.max().width - h).max(0.0),
(bc.max().height - v).max(0.0),
),
);
let consumed = self.consumed_insets(ctx.window_insets());
let child = &mut self.child;
let child_size = ctx.with_window_insets(consumed, |ctx| child.layout_child(ctx, &child_bc));
self.child.set_origin(Point::new(left, top));
bc.constrain(Size::new(child_size.width + h, child_size.height + v))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
let consumed = self.consumed_insets(ctx.window_insets());
let child = &mut self.child;
ctx.with_window_insets(consumed, |ctx| child.paint_child(ctx, scene));
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
crate::authoring::route_event_single(&mut self.child, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.child.semantics_child(ctx);
}
crate::authoring::visit_children!(child);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::leaf;
use frust_core::{
PointerButton, PointerEvent, PointerPhase, RenderRoot, WindowEdgeInsets, WindowInsets,
};
use std::any::Any;
fn safe_area_widget(root: &RenderRoot<(), SafeAreaView<()>>) -> &SafeAreaWidget {
let id = root.root_id().expect("root built");
(root.tree().pod(id).expect("root pod").widget() as &dyn Any)
.downcast_ref::<SafeAreaWidget>()
.expect("root is a SafeAreaWidget")
}
#[test]
fn no_insets_behaves_like_minimum_padding() {
fn logic(_: &mut ()) -> SafeAreaView<()> {
safe_area(leaf(40.0, 20.0)).minimum(crate::EdgeInsets {
left: 5.0,
top: 10.0,
right: 15.0,
bottom: 20.0,
})
}
let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut logic, &mut state);
let size = root.layout(Size::new(500.0, 500.0));
assert_eq!(size, Size::new(60.0, 50.0));
let w = safe_area_widget(&root);
assert_eq!(w.child.origin(), Point::new(5.0, 10.0));
assert_eq!(w.child.size(), Size::new(40.0, 20.0));
}
#[test]
fn pushed_insets_pad_enabled_edges_only() {
fn logic(_: &mut ()) -> SafeAreaView<()> {
safe_area(leaf(40.0, 20.0)).right(false)
}
let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut logic, &mut state);
root.set_insets(WindowInsets::new(
WindowEdgeInsets::new(10.0, 24.0, 30.0, 34.0),
WindowEdgeInsets::ZERO,
));
let size = root.layout(Size::new(500.0, 500.0));
let w = safe_area_widget(&root);
assert_eq!(w.child.origin(), Point::new(10.0, 24.0));
assert_eq!(size, Size::new(40.0 + 10.0, 20.0 + 24.0 + 34.0));
}
#[test]
fn minimum_wins_when_larger() {
fn logic(_: &mut ()) -> SafeAreaView<()> {
safe_area(leaf(40.0, 20.0)).minimum(crate::EdgeInsets::all(50.0))
}
let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut logic, &mut state);
root.set_insets(WindowInsets::new(
WindowEdgeInsets::new(10.0, 10.0, 10.0, 10.0),
WindowEdgeInsets::ZERO,
));
root.layout(Size::new(500.0, 500.0));
let w = safe_area_widget(&root);
assert_eq!(
w.child.origin(),
Point::new(50.0, 50.0),
"the larger minimum wins over the smaller pushed inset on every edge"
);
}
#[test]
fn ime_overlap_clamps_bottom_padding_to_zero() {
fn logic(_: &mut ()) -> SafeAreaView<()> {
safe_area(leaf(40.0, 20.0))
}
let mut root: RenderRoot<(), SafeAreaView<()>> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut logic, &mut state);
root.set_insets(WindowInsets::new(
WindowEdgeInsets::new(0.0, 24.0, 0.0, 34.0),
WindowEdgeInsets::new(0.0, 0.0, 0.0, 340.0),
));
root.layout(Size::new(500.0, 500.0));
let w = safe_area_widget(&root);
assert_eq!(
w.child.origin(),
Point::new(0.0, 24.0),
"bottom padding clamps to 0 under the IME overlap; top is unaffected"
);
}
#[derive(Default)]
struct Counter {
presses: u32,
}
fn pointer_ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
fn button_safe_area(minimum: crate::EdgeInsets) -> SafeAreaWidget {
let view: SafeAreaView<Counter> =
safe_area(crate::button::<Counter, _>("go", |s: &mut Counter| {
s.presses += 1
}))
.minimum(minimum);
let mut counter = 0u64;
let mut w = view.build(&mut BuildCtx::new(&mut counter));
let mut text_ctx = frust_text::TextContext::new();
let mut lctx = LayoutCtx::with_text_context(&mut text_ctx);
w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
w
}
#[test]
fn event_coordinates_route_through_the_offset() {
let mut w = button_safe_area(crate::EdgeInsets::all(10.0));
let origin = w.child.origin();
assert_eq!(origin, Point::new(10.0, 10.0));
let mut state = Counter::default();
let state_any: &mut dyn Any = &mut state;
let mut ctx = EventCtx::new(state_any, Point::ZERO, Size::new(500.0, 500.0));
let inside = Point::new(origin.x + 2.0, origin.y + 2.0);
assert_eq!(
w.event(
&mut ctx,
&pointer_ev(PointerPhase::Down, inside.x, inside.y)
),
EventResult::Handled
);
assert_eq!(
w.event(&mut ctx, &pointer_ev(PointerPhase::Up, inside.x, inside.y)),
EventResult::Handled
);
drop(ctx);
assert_eq!(
state.presses, 1,
"a synthetic tap at the child's offset location must reach it"
);
}
}