use std::any::Any;
use std::cell::RefCell;
use std::marker::PhantomData;
use std::rc::Rc;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
LayoutCtx, OutsideTap, OverlayBand, OverlayEntry, OverlayEventKind, OverlayInput, OverlayKey,
PaintCtx, PaintScene, PointerEvent, SemanticsCtx, View, Widget, any,
};
use kurbo::{Point, Rect, Size};
use crate::authoring::{ErasedCallback, erase_callback, releases_capture, route_event_single};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverlaySide {
Top,
Right,
#[default]
Bottom,
Left,
}
impl OverlaySide {
pub const fn opposite(self) -> Self {
match self {
OverlaySide::Top => OverlaySide::Bottom,
OverlaySide::Bottom => OverlaySide::Top,
OverlaySide::Left => OverlaySide::Right,
OverlaySide::Right => OverlaySide::Left,
}
}
pub const fn is_vertical(self) -> bool {
matches!(self, OverlaySide::Top | OverlaySide::Bottom)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverlayAlign {
Start,
#[default]
Center,
End,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct OverlayPlacement {
pub side: OverlaySide,
pub align: OverlayAlign,
pub offset: f64,
pub flip: bool,
pub clamp: bool,
pub padding: f64,
}
pub const DEFAULT_OFFSET: f64 = 4.0;
pub const DEFAULT_PADDING: f64 = 8.0;
impl Default for OverlayPlacement {
fn default() -> Self {
OverlayPlacement {
side: OverlaySide::default(),
align: OverlayAlign::default(),
offset: DEFAULT_OFFSET,
flip: true,
clamp: true,
padding: DEFAULT_PADDING,
}
}
}
impl OverlayPlacement {
pub fn on(side: OverlaySide) -> Self {
OverlayPlacement {
side,
..Self::default()
}
}
pub const fn align(mut self, align: OverlayAlign) -> Self {
self.align = align;
self
}
pub const fn offset(mut self, offset: f64) -> Self {
self.offset = offset;
self
}
pub const fn flip(mut self, flip: bool) -> Self {
self.flip = flip;
self
}
pub const fn clamp(mut self, clamp: bool) -> Self {
self.clamp = clamp;
self
}
pub const fn padding(mut self, padding: f64) -> Self {
self.padding = padding;
self
}
}
fn field(area: Rect, padding: f64) -> Rect {
let inset = area.inset(-padding);
if inset.width() > 0.0 && inset.height() > 0.0 {
inset
} else {
area
}
}
fn fits(side: OverlaySide, anchor: Rect, content: Size, field: Rect, offset: f64) -> bool {
match side {
OverlaySide::Top => anchor.y0 - offset - content.height >= field.y0,
OverlaySide::Bottom => anchor.y1 + offset + content.height <= field.y1,
OverlaySide::Left => anchor.x0 - offset - content.width >= field.x0,
OverlaySide::Right => anchor.x1 + offset + content.width <= field.x1,
}
}
fn align_start(align: OverlayAlign, a0: f64, a1: f64, extent: f64) -> f64 {
match align {
OverlayAlign::Start => a0,
OverlayAlign::Center => (a0 + a1) / 2.0 - extent / 2.0,
OverlayAlign::End => a1 - extent,
}
}
fn clamp_into(rect: Rect, field: Rect) -> Rect {
let x = rect.x0.min(field.x1 - rect.width()).max(field.x0);
let y = rect.y0.min(field.y1 - rect.height()).max(field.y0);
Rect::from_origin_size(Point::new(x, y), rect.size())
}
pub fn place(anchor: Rect, content: Size, area: Rect, placement: OverlayPlacement) -> Rect {
let field = field(area, placement.padding);
let mut side = placement.side;
if placement.flip
&& !fits(side, anchor, content, field, placement.offset)
&& fits(side.opposite(), anchor, content, field, placement.offset)
{
side = side.opposite();
}
let origin = match side {
OverlaySide::Top => Point::new(
align_start(placement.align, anchor.x0, anchor.x1, content.width),
anchor.y0 - placement.offset - content.height,
),
OverlaySide::Bottom => Point::new(
align_start(placement.align, anchor.x0, anchor.x1, content.width),
anchor.y1 + placement.offset,
),
OverlaySide::Left => Point::new(
anchor.x0 - placement.offset - content.width,
align_start(placement.align, anchor.y0, anchor.y1, content.height),
),
OverlaySide::Right => Point::new(
anchor.x1 + placement.offset,
align_start(placement.align, anchor.y0, anchor.y1, content.height),
),
};
let rect = Rect::from_origin_size(origin, content);
if placement.clamp {
clamp_into(rect, field)
} else {
rect
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum OverlayAnchor {
#[default]
Owner,
Rect(Rect),
}
pub struct OverlaySlot<PodState: 'static> {
key: OverlayKey,
pod: Option<Rc<RefCell<ChildPod>>>,
band: OverlayBand,
input: OverlayInput,
outside_tap: OutsideTap,
placement: OverlayPlacement,
anchor: OverlayAnchor,
window: Size,
window_rect: Rect,
owner_origin: Point,
captured: bool,
outside_down_pending: bool,
_state: PhantomData<fn(&mut PodState)>,
}
impl<PodState: 'static> Default for OverlaySlot<PodState> {
fn default() -> Self {
Self::new()
}
}
impl<PodState: 'static> OverlaySlot<PodState> {
pub fn new() -> Self {
Self {
key: OverlayKey::next(),
pod: None,
band: OverlayBand::Floating,
input: OverlayInput::Interactive,
outside_tap: OutsideTap::Ignore,
placement: OverlayPlacement::default(),
anchor: OverlayAnchor::Owner,
window: Size::ZERO,
window_rect: Rect::ZERO,
owner_origin: Point::ZERO,
captured: false,
outside_down_pending: false,
_state: PhantomData,
}
}
pub fn key(&self) -> OverlayKey {
self.key
}
pub fn is_open(&self) -> bool {
self.pod.is_some()
}
pub fn window_rect(&self) -> Rect {
self.window_rect
}
pub fn pod_has_focus(&self) -> bool {
self.pod
.as_ref()
.is_some_and(|pod| pod.borrow().holds_live_focus())
}
pub fn withdraw_pod_focus(&mut self) {
if let Some(pod) = &self.pod {
pod.borrow_mut().set_focused(false);
}
}
pub fn take_outside_down(&mut self) -> bool {
std::mem::take(&mut self.outside_down_pending)
}
pub fn set_band(&mut self, band: OverlayBand) {
self.band = band;
}
pub fn set_input(&mut self, input: OverlayInput) {
self.input = input;
}
pub fn set_outside_tap(&mut self, outside_tap: OutsideTap) {
self.outside_tap = outside_tap;
}
pub fn set_placement(&mut self, placement: OverlayPlacement) {
self.placement = placement;
}
pub fn set_anchor(&mut self, anchor: OverlayAnchor) {
self.anchor = anchor;
}
pub fn rebuild(
&mut self,
prev: Option<&AnyView<PodState>>,
next: Option<&AnyView<PodState>>,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
match (prev, next) {
(Some(prev), Some(next)) => match &self.pod {
Some(pod) => {
let mut pod = pod.borrow_mut();
crate::authoring::rebuild_child(prev, next, &mut pod, ctx)
}
None => self.mount(next, ctx),
},
(None, Some(next)) => {
self.drop_pod();
self.mount(next, ctx)
}
(Some(prev), None) => match self.pod.take() {
Some(pod) => {
{
let mut pod = pod.borrow_mut();
crate::authoring::teardown_child(prev, &mut pod, ctx);
}
self.captured = false;
ChangeFlags::LAYOUT
}
None => ChangeFlags::NONE,
},
(None, None) => {
if self.pod.is_some() {
self.drop_pod();
ChangeFlags::LAYOUT
} else {
ChangeFlags::NONE
}
}
}
}
fn mount(&mut self, view: &AnyView<PodState>, ctx: &mut BuildCtx<'_>) -> ChangeFlags {
let pod = crate::authoring::build_child(view, ctx);
self.pod = Some(Rc::new(RefCell::new(pod)));
self.captured = false;
ChangeFlags::LAYOUT
}
fn drop_pod(&mut self) {
self.pod = None;
self.captured = false;
}
pub fn layout(&mut self, ctx: &mut LayoutCtx) {
self.window = ctx.window_size();
if let Some(pod) = &self.pod {
let bc = BoxConstraints::loose(self.window);
let mut pod = pod.borrow_mut();
pod.layout_child(ctx, &bc);
pod.set_origin(Point::ZERO);
}
}
pub fn paint(&mut self, ctx: &mut PaintCtx, owner_size: Size) {
let Some(pod) = &self.pod else {
return;
};
self.owner_origin = ctx.origin();
let anchor = match self.anchor {
OverlayAnchor::Owner => Rect::from_origin_size(ctx.origin(), owner_size),
OverlayAnchor::Rect(local) => local + ctx.origin().to_vec2(),
};
let area = Rect::from_origin_size(Point::ZERO, self.window);
let content = pod.borrow().size();
self.window_rect = place(anchor, content, area, self.placement);
ctx.register_overlay(OverlayEntry {
key: self.key,
band: self.band,
input: self.input,
outside_tap: self.outside_tap,
window_rect: self.window_rect,
pod: Rc::clone(pod),
insets: ctx.window_insets(),
});
}
pub fn event(
&mut self,
ctx: &mut EventCtx<'_>,
event: &InputEvent,
state: &mut PodState,
) -> Option<EventResult> {
let substitute: &mut dyn Any = state;
self.route(ctx, event, Some(substitute))
}
pub fn event_ambient(
&mut self,
ctx: &mut EventCtx<'_>,
event: &InputEvent,
) -> Option<EventResult> {
self.route(ctx, event, None)
}
fn route(
&mut self,
ctx: &mut EventCtx<'_>,
event: &InputEvent,
substitute: Option<&mut dyn Any>,
) -> Option<EventResult> {
self.pod.as_ref()?;
let origin = self.window_rect.origin().to_vec2();
match event {
InputEvent::Overlay(overlay) if overlay.key == self.key => {
match &overlay.kind {
OverlayEventKind::Pointer(pointer) => {
let local = InputEvent::Pointer(PointerEvent {
position: pointer.position - origin,
..*pointer
});
self.forward(ctx, &local, substitute);
}
OverlayEventKind::Scroll { position, delta } => {
let local = InputEvent::Scroll {
position: *position - origin,
delta: *delta,
};
self.forward(ctx, &local, substitute);
}
OverlayEventKind::Scale {
focal,
phase,
scale_delta,
velocity,
} => {
let local = InputEvent::Scale(frust_core::event::ScaleEvent {
phase: *phase,
scale_delta: *scale_delta,
focal: *focal - origin,
velocity: *velocity,
});
self.forward(ctx, &local, substitute);
}
OverlayEventKind::OutsideDown => self.outside_down_pending = true,
}
Some(EventResult::Ignored)
}
InputEvent::Pointer(pointer) if self.captured => {
let local = InputEvent::Pointer(PointerEvent {
position: pointer.position + self.owner_origin.to_vec2() - origin,
..*pointer
});
Some(self.forward(ctx, &local, substitute))
}
event if event.is_focus_routed() && self.pod_has_focus() => {
Some(self.forward(ctx, event, substitute))
}
_ => None,
}
}
fn forward(
&mut self,
ctx: &mut EventCtx<'_>,
local: &InputEvent,
substitute: Option<&mut dyn Any>,
) -> EventResult {
let Some(pod) = self.pod.clone() else {
return EventResult::Ignored;
};
let mut pod = pod.borrow_mut();
let was_active = pod.is_active();
let was_focused = pod.is_focused();
let result = match substitute {
None => pod.event_child(ctx, local),
Some(state) => {
let (result, needs_redraw) = {
let mut inner =
EventCtx::new(state, self.window_rect.origin(), self.window_rect.size());
let result = pod.event_child(&mut inner, local);
(result, inner.needs_redraw())
};
if needs_redraw {
ctx.request_redraw();
}
if pod.is_active() && !was_active {
ctx.capture_pointer();
}
match (was_focused, pod.is_focused()) {
(false, true) => ctx.request_focus(),
(true, false) => ctx.release_focus(),
_ => {}
}
result
}
};
if was_active && releases_capture(local) {
pod.set_active(false);
}
self.captured = pod.is_active();
result
}
}
pub fn overlay_portal<State: 'static, V: View<State>>(child: V) -> OverlayPortalView<State> {
OverlayPortalView {
child: any(child),
overlay: None,
placement: OverlayPlacement::default(),
band: OverlayBand::Floating,
input: OverlayInput::Interactive,
outside_tap: OutsideTap::Ignore,
on_outside_tap: None,
preserve_focus: false,
}
}
type OnOutsideTap<State> = Rc<dyn Fn(&mut State)>;
pub struct OverlayPortalView<State: 'static> {
child: AnyView<State>,
overlay: Option<AnyView<State>>,
placement: OverlayPlacement,
band: OverlayBand,
input: OverlayInput,
outside_tap: OutsideTap,
on_outside_tap: Option<OnOutsideTap<State>>,
preserve_focus: bool,
}
impl<State: 'static> OverlayPortalView<State> {
pub fn overlay(mut self, overlay: Option<AnyView<State>>) -> Self {
self.overlay = overlay;
self
}
pub fn placement(mut self, placement: OverlayPlacement) -> Self {
self.placement = placement;
self
}
pub fn band(mut self, band: OverlayBand) -> Self {
self.band = band;
self
}
pub fn input(mut self, input: OverlayInput) -> Self {
self.input = input;
self
}
pub fn outside_tap(mut self, outside_tap: OutsideTap) -> Self {
self.outside_tap = outside_tap;
self
}
pub fn on_outside_tap(mut self, callback: impl Fn(&mut State) + 'static) -> Self {
self.on_outside_tap = Some(Rc::new(callback));
if self.outside_tap == OutsideTap::Ignore {
self.outside_tap = OutsideTap::Notify { consume: true };
}
self
}
pub fn preserve_focus(mut self, preserve: bool) -> Self {
self.preserve_focus = preserve;
self
}
}
pub struct OverlayPortalWidget<State: 'static> {
child: ChildPod,
slot: OverlaySlot<State>,
on_outside_tap: Option<ErasedCallback>,
preserve_focus: bool,
}
impl<State: 'static> OverlayPortalView<State> {
fn configure(&self, slot: &mut OverlaySlot<State>) {
slot.set_placement(self.placement);
slot.set_band(self.band);
slot.set_input(self.input);
slot.set_outside_tap(self.outside_tap);
}
}
impl<State: 'static> View<State> for OverlayPortalView<State> {
type Element = OverlayPortalWidget<State>;
fn build(&self, ctx: &mut BuildCtx<'_>) -> OverlayPortalWidget<State> {
let mut slot = OverlaySlot::new();
self.configure(&mut slot);
slot.rebuild(None, self.overlay.as_ref(), ctx);
OverlayPortalWidget {
child: crate::authoring::build_child(&self.child, ctx),
slot,
on_outside_tap: self.on_outside_tap.as_ref().map(erase_callback),
preserve_focus: self.preserve_focus,
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut OverlayPortalWidget<State>,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
if prev.placement != self.placement
|| prev.band != self.band
|| prev.input != self.input
|| prev.outside_tap != self.outside_tap
{
flags |= ChangeFlags::PAINT;
}
self.configure(&mut element.slot);
element.preserve_focus = self.preserve_focus;
element.on_outside_tap = self.on_outside_tap.as_ref().map(erase_callback);
flags |= element
.slot
.rebuild(prev.overlay.as_ref(), self.overlay.as_ref(), ctx);
flags |= crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
flags
}
fn teardown(&self, element: &mut OverlayPortalWidget<State>, ctx: &mut BuildCtx<'_>) {
element.slot.rebuild(self.overlay.as_ref(), None, ctx);
crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
}
}
impl<State: 'static> Widget for OverlayPortalWidget<State> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = self.child.layout_child(ctx, bc);
self.child.set_origin(Point::ZERO);
self.slot.layout(ctx);
bc.constrain(size)
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
self.child.paint_child(ctx, scene);
let size = ctx.size();
self.slot.paint(ctx, size);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let child_focused = self.child.holds_live_focus();
if !(event.is_focus_routed() && child_focused) {
let pod_focused_before = self.slot.pod_has_focus();
if let Some(result) = self.slot.event_ambient(ctx, event) {
if self.slot.take_outside_down()
&& let Some(callback) = &mut self.on_outside_tap
{
callback(ctx);
ctx.request_redraw();
}
if !pod_focused_before && self.slot.pod_has_focus() {
if self.preserve_focus && child_focused {
self.slot.withdraw_pod_focus();
self.child.set_focused(true);
ctx.request_focus();
} else {
self.child.set_focused(false);
}
}
return result;
}
}
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::RecordingScene;
use crate::{Column, SizedBox, Stack, StackView, scroll_view};
use frust_core::{
EditCommand, FrameTime, Key, KeyEvent, Modifiers, NamedKey, PointerButton, PointerPhase,
RenderRoot, ScrollDelta,
};
use peniko::Color;
const AREA: Rect = Rect::new(0.0, 0.0, 400.0, 600.0);
fn bare() -> OverlayPlacement {
OverlayPlacement::default().padding(0.0)
}
fn anchor_rect(x: f64, y: f64, w: f64, h: f64) -> Rect {
Rect::from_origin_size(Point::new(x, y), Size::new(w, h))
}
#[test]
fn the_default_placement_is_below_the_anchor_centred_at_the_neutral_gap() {
let p = OverlayPlacement::default();
assert_eq!(p.side, OverlaySide::Bottom);
assert_eq!(p.align, OverlayAlign::Center);
assert_eq!(p.offset, DEFAULT_OFFSET);
assert_eq!(p.padding, DEFAULT_PADDING);
assert!(p.flip && p.clamp);
}
#[test]
fn each_side_offsets_off_its_own_edge() {
let a = anchor_rect(100.0, 200.0, 80.0, 40.0);
let content = Size::new(120.0, 60.0);
let raw = |side| OverlayPlacement::on(side).flip(false).clamp(false);
let bottom = place(a, content, AREA, raw(OverlaySide::Bottom));
assert_eq!(bottom.y0, a.y1 + DEFAULT_OFFSET);
let top = place(a, content, AREA, raw(OverlaySide::Top));
assert_eq!(top.y1, a.y0 - DEFAULT_OFFSET);
let right = place(a, content, AREA, raw(OverlaySide::Right));
assert_eq!(right.x0, a.x1 + DEFAULT_OFFSET);
let left = place(a, content, AREA, raw(OverlaySide::Left));
assert_eq!(left.x1, a.x0 - DEFAULT_OFFSET);
for r in [bottom, top, right, left] {
assert_eq!(r.size(), content);
}
assert!(OverlaySide::Top.is_vertical() && !OverlaySide::Left.is_vertical());
assert_eq!(OverlaySide::Top.opposite(), OverlaySide::Bottom);
assert_eq!(OverlaySide::Left.opposite(), OverlaySide::Right);
}
#[test]
fn align_lines_up_leading_center_or_trailing_edges() {
let a = anchor_rect(100.0, 200.0, 80.0, 40.0);
let content = Size::new(120.0, 60.0);
let at = |side, align| place(a, content, AREA, OverlayPlacement::on(side).align(align));
assert_eq!(at(OverlaySide::Bottom, OverlayAlign::Start).x0, a.x0);
assert_eq!(at(OverlaySide::Bottom, OverlayAlign::End).x1, a.x1);
assert_eq!(
at(OverlaySide::Bottom, OverlayAlign::Center).center().x,
a.center().x
);
assert_eq!(at(OverlaySide::Right, OverlayAlign::Start).y0, a.y0);
assert_eq!(at(OverlaySide::Right, OverlayAlign::End).y1, a.y1);
assert_eq!(
at(OverlaySide::Right, OverlayAlign::Center).center().y,
a.center().y
);
}
#[test]
fn a_side_that_does_not_fit_flips_to_the_opposite_one() {
let a = anchor_rect(100.0, 560.0, 80.0, 20.0);
let content = Size::new(120.0, 100.0);
let flipped = place(a, content, AREA, bare());
assert_eq!(
flipped.y1,
a.y0 - DEFAULT_OFFSET,
"flipped above the anchor"
);
let pinned = place(a, content, AREA, bare().flip(false));
assert_eq!(pinned.y1, AREA.y1, "clamped, not flipped");
let tall = Size::new(120.0, 590.0);
let kept = place(a, tall, AREA, bare());
assert_eq!(kept.y1, AREA.y1);
}
#[test]
fn each_side_flips_at_the_edge_it_would_overflow() {
let content = Size::new(120.0, 100.0);
let high = anchor_rect(100.0, 10.0, 80.0, 20.0);
let down = place(high, content, AREA, OverlayPlacement::on(OverlaySide::Top));
assert_eq!(down.y0, high.y1 + DEFAULT_OFFSET);
let leading = anchor_rect(10.0, 200.0, 20.0, 20.0);
let right = place(
leading,
content,
AREA,
OverlayPlacement::on(OverlaySide::Left),
);
assert_eq!(right.x0, leading.x1 + DEFAULT_OFFSET);
let trailing = anchor_rect(370.0, 200.0, 20.0, 20.0);
let left = place(
trailing,
content,
AREA,
OverlayPlacement::on(OverlaySide::Right),
);
assert_eq!(left.x1, trailing.x0 - DEFAULT_OFFSET);
}
#[test]
fn clamping_shifts_the_rect_back_inside_and_can_be_turned_off() {
let a = anchor_rect(380.0, 100.0, 20.0, 20.0);
let content = Size::new(200.0, 50.0);
let clamped = place(a, content, AREA, bare());
assert_eq!(clamped.x1, AREA.x1);
assert_eq!(
clamped.y0,
a.y1 + DEFAULT_OFFSET,
"only the cross axis moved"
);
let free = place(a, content, AREA, bare().clamp(false));
assert!(free.x1 > AREA.x1, "unclamped placement may overflow");
let huge = Size::new(600.0, 50.0);
let pinned = place(a, huge, AREA, bare());
assert_eq!(pinned.x0, AREA.x0);
}
#[test]
fn the_clamp_keeps_the_padding_off_every_edge() {
let content = Size::new(200.0, 50.0);
let trailing = place(
anchor_rect(380.0, 100.0, 20.0, 20.0),
content,
AREA,
OverlayPlacement::default(),
);
assert_eq!(trailing.x1, AREA.x1 - DEFAULT_PADDING);
let leading = place(
anchor_rect(0.0, 100.0, 20.0, 20.0),
content,
AREA,
OverlayPlacement::default(),
);
assert_eq!(leading.x0, AREA.x0 + DEFAULT_PADDING);
let low = place(
anchor_rect(100.0, 560.0, 20.0, 20.0),
Size::new(100.0, 300.0),
AREA,
OverlayPlacement::default().flip(false),
);
assert_eq!(low.y1, AREA.y1 - DEFAULT_PADDING);
}
#[test]
fn a_padding_larger_than_the_area_falls_back_to_the_area_itself() {
let tiny = Rect::new(0.0, 0.0, 10.0, 10.0);
let rect = place(
Rect::ZERO,
Size::new(4.0, 4.0),
tiny,
OverlayPlacement::default().flip(false),
);
assert!(tiny.contains(rect.origin()));
assert_eq!(rect.x0, 0.0, "clamped to the un-inset area's own edge");
}
#[test]
fn a_degenerate_anchor_places_against_that_point() {
let content = Size::new(80.0, 40.0);
let rect = place(Rect::ZERO, content, AREA, OverlayPlacement::default());
assert_eq!(
rect.x0,
AREA.x0 + DEFAULT_PADDING,
"clamped in from the centred overhang"
);
assert_eq!(rect.y0, AREA.y0 + DEFAULT_PADDING);
}
type Log = Rc<RefCell<Vec<String>>>;
const WINDOW: Size = Size::new(400.0, 600.0);
const POD: Rect = Rect::new(8.0, 8.0, 88.0, 38.0);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum Reaction {
#[default]
Nothing,
Capture,
Focus,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Shape {
Plain,
Scrolled,
Sibling,
}
#[derive(Clone, Copy)]
struct Cfg {
open: bool,
shape: Shape,
band: OverlayBand,
input: OverlayInput,
outside_tap: Option<OutsideTap>,
preserve_focus: bool,
reaction: Reaction,
placement: OverlayPlacement,
}
fn corner() -> OverlayPlacement {
OverlayPlacement::on(OverlaySide::Top)
.offset(0.0)
.align(OverlayAlign::Start)
}
impl Default for Cfg {
fn default() -> Self {
Cfg {
open: true,
shape: Shape::Plain,
band: OverlayBand::Floating,
input: OverlayInput::Interactive,
outside_tap: Some(OutsideTap::Ignore),
preserve_focus: false,
reaction: Reaction::Nothing,
placement: corner(),
}
}
}
struct App {
cfg: Cfg,
log: Log,
presses: u32,
outside_taps: u32,
}
struct Probe {
tag: &'static str,
size: Option<Size>,
reaction: Reaction,
handles: bool,
log: Log,
}
struct ProbeWidget {
tag: &'static str,
size: Option<Size>,
reaction: Reaction,
handles: bool,
log: Log,
}
impl View<App> for Probe {
type Element = ProbeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> ProbeWidget {
ProbeWidget {
tag: self.tag,
size: self.size,
reaction: self.reaction,
handles: self.handles,
log: Rc::clone(&self.log),
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut ProbeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.size = self.size;
element.reaction = self.reaction;
element.handles = self.handles;
element.log = Rc::clone(&self.log);
ChangeFlags::NONE
}
}
impl Widget for ProbeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.size.unwrap_or_else(|| bc.max()))
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
scene.fill_rect(ctx.origin(), ctx.size(), Color::BLACK);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
match event {
InputEvent::Pointer(p) => {
self.log.borrow_mut().push(format!(
"{}:{:?}@{},{}",
self.tag, p.phase, p.position.x, p.position.y
));
if p.phase == PointerPhase::Down {
ctx.state_mut::<App>().presses += 1;
match self.reaction {
Reaction::Nothing => {}
Reaction::Capture => ctx.capture_pointer(),
Reaction::Focus => ctx.request_focus(),
}
}
if self.handles {
EventResult::Handled
} else {
EventResult::Ignored
}
}
InputEvent::Key(_) => {
self.log.borrow_mut().push(format!(
"{}:key focus={}",
self.tag,
ctx.has_focus()
));
EventResult::Handled
}
_ => EventResult::Ignored,
}
}
}
fn logic(state: &mut App) -> StackView<App> {
let cfg = state.cfg;
let log = Rc::clone(&state.log);
let pod = cfg.open.then(|| {
any(Probe {
tag: "pod",
size: Some(Size::new(80.0, 30.0)),
reaction: cfg.reaction,
handles: true,
log: Rc::clone(&log),
})
});
let child = Probe {
tag: "child",
size: match cfg.shape {
Shape::Plain => None,
Shape::Scrolled => Some(Size::new(120.0, 40.0)),
Shape::Sibling => Some(Size::new(400.0, 50.0)),
},
reaction: Reaction::Focus,
handles: true,
log: Rc::clone(&log),
};
let mut portal = overlay_portal(child)
.overlay(pod)
.placement(cfg.placement)
.band(cfg.band)
.input(cfg.input)
.preserve_focus(cfg.preserve_focus)
.on_outside_tap(|state: &mut App| state.outside_taps += 1);
if let Some(policy) = cfg.outside_tap {
portal = portal.outside_tap(policy);
}
match cfg.shape {
Shape::Plain => Stack(vec![
any(portal),
any(Probe {
tag: "sib",
size: None,
reaction: Reaction::Nothing,
handles: false,
log,
}),
]),
Shape::Scrolled => Stack(vec![any(scroll_view(Column(vec![
any(SizedBox(Some(400.0), Some(200.0))),
any(portal),
any(SizedBox(Some(400.0), Some(1000.0))),
])))]),
Shape::Sibling => Stack(vec![any(Column(vec![
any(Probe {
tag: "field",
size: Some(Size::new(400.0, 50.0)),
reaction: Reaction::Focus,
handles: true,
log,
}),
any(portal),
]))]),
}
}
struct Harness {
root: RenderRoot<App, StackView<App>>,
state: App,
clock_ms: f64,
}
impl Harness {
fn new(cfg: Cfg) -> Self {
Harness {
root: RenderRoot::new(),
state: App {
cfg,
log: Rc::new(RefCell::new(Vec::new())),
presses: 0,
outside_taps: 0,
},
clock_ms: 0.0,
}
}
fn frame(&mut self) -> RecordingScene {
let mut app_logic: fn(&mut App) -> StackView<App> = logic;
self.root.rebuild(&mut app_logic, &mut self.state);
self.root.layout(WINDOW);
self.clock_ms += 16.0;
let mut scene = RecordingScene::default();
self.root.paint(
&mut scene,
FrameTime::from_nanos((self.clock_ms * 1_000_000.0) as u64),
);
scene
}
fn pointer(&mut self, phase: PointerPhase, x: f64, y: f64) {
self.root.event(
&mut self.state,
&InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
}),
);
}
fn down(&mut self, x: f64, y: f64) {
self.pointer(PointerPhase::Down, x, y);
}
fn key(&mut self) {
self.root.event(
&mut self.state,
&InputEvent::Key(KeyEvent {
key: Key::Named(NamedKey::ArrowLeft),
modifiers: Modifiers::default(),
repeat: false,
}),
);
}
fn log(&self) -> Vec<String> {
self.state.log.borrow().clone()
}
fn clear_log(&mut self) {
self.state.log.borrow_mut().clear();
}
}
fn rects(scene: &RecordingScene) -> Vec<Rect> {
scene
.rects
.iter()
.map(|(origin, size)| Rect::from_origin_size(*origin, *size))
.collect()
}
#[test]
fn the_overlay_pod_paints_above_a_later_sibling() {
let mut h = Harness::new(Cfg::default());
let scene = h.frame();
let painted = rects(&scene);
assert_eq!(
painted.last().copied(),
Some(POD),
"the floated pod paints after the whole main tree, not in place: {painted:?}"
);
let sibling = painted[painted.len() - 2];
assert_eq!(sibling, Rect::from_origin_size(Point::ZERO, WINDOW));
}
#[test]
fn a_press_inside_the_surface_reaches_the_pod_in_its_own_space() {
let mut h = Harness::new(Cfg::default());
h.frame();
h.clear_log();
h.down(40.0, 20.0);
assert_eq!(
h.log(),
vec!["pod:Down@32,12".to_string()],
"the press is delivered in pod space (window minus the placed origin)"
);
assert_eq!(
h.state.presses, 1,
"the pod's own callback ran on app state"
);
}
#[test]
fn a_transparent_tooltip_surface_never_receives_input() {
let mut h = Harness::new(Cfg {
band: OverlayBand::Tooltip,
input: OverlayInput::Transparent,
..Cfg::default()
});
let scene = h.frame();
assert_eq!(
rects(&scene).last().copied(),
Some(POD),
"a transparent surface is still painted above everything"
);
h.clear_log();
h.down(40.0, 20.0);
assert_eq!(
h.log(),
vec!["sib:Down@40,20".to_string(), "child:Down@40,20".to_string()],
"the pointer passes through to the main tree, topmost sibling first"
);
}
#[test]
fn a_drag_begun_on_the_surface_continues_into_it_through_the_capture() {
let mut h = Harness::new(Cfg {
shape: Shape::Scrolled,
placement: OverlayPlacement::default(),
reaction: Reaction::Capture,
..Cfg::default()
});
h.frame();
h.clear_log();
h.down(30.0, 250.0);
assert!(
h.root.is_pointer_captured(),
"a capture claimed from inside the surface is honoured"
);
h.pointer(PointerPhase::Move, 200.0, 400.0);
h.pointer(PointerPhase::Up, 210.0, 410.0);
assert_eq!(
h.log(),
vec![
"pod:Down@10,6".to_string(),
"pod:Move@180,156".to_string(),
"pod:Up@190,166".to_string(),
]
);
assert!(!h.root.is_pointer_captured(), "the Up releases the capture");
h.clear_log();
h.pointer(PointerPhase::Move, 200.0, 400.0);
assert!(
!h.log().iter().any(|e| e.starts_with("pod:")),
"a move after the release is no longer the surface's: {:?}",
h.log()
);
}
fn one_gesture(h: &mut Harness, label: &str) {
h.clear_log();
h.down(40.0, 20.0);
assert!(
h.root.is_pointer_captured(),
"{label}: the capture claimed inside the surface opened a gesture"
);
h.pointer(PointerPhase::Move, 300.0, 300.0);
h.pointer(PointerPhase::Up, 300.0, 300.0);
assert_eq!(
h.log(),
vec![
"pod:Down@32,12".to_string(),
"pod:Move@292,292".to_string(),
"pod:Up@292,292".to_string(),
],
"{label}: every phase of the gesture reached the surface"
);
assert!(
!h.root.is_pointer_captured(),
"{label}: the Up released the capture"
);
}
#[test]
fn a_second_gesture_into_the_same_mounted_pod_routes_exactly_like_the_first() {
let mut h = Harness::new(Cfg {
reaction: Reaction::Capture,
..Cfg::default()
});
h.frame();
one_gesture(&mut h, "first gesture");
one_gesture(&mut h, "second gesture");
}
#[test]
fn a_cancelled_gesture_leaves_the_surface_ready_for_the_next_one() {
let mut h = Harness::new(Cfg {
reaction: Reaction::Capture,
..Cfg::default()
});
h.frame();
h.down(40.0, 20.0);
h.pointer(PointerPhase::Cancel, 300.0, 300.0);
assert!(
!h.root.is_pointer_captured(),
"a Cancel ends the gesture exactly as an Up does"
);
one_gesture(&mut h, "the gesture after a cancel");
}
#[test]
fn a_press_outside_notifies_the_owner_and_can_still_reach_the_main_tree() {
let mut h = Harness::new(Cfg {
outside_tap: Some(OutsideTap::Notify { consume: false }),
..Cfg::default()
});
h.frame();
h.clear_log();
h.down(200.0, 400.0);
assert_eq!(h.state.outside_taps, 1, "the owner was told");
assert_eq!(
h.log(),
vec![
"sib:Down@200,400".to_string(),
"child:Down@200,400".to_string()
],
"a pass-through notification still lets the press through"
);
}
#[test]
fn a_consuming_outside_press_is_swallowed() {
let mut h = Harness::new(Cfg {
outside_tap: Some(OutsideTap::Notify { consume: true }),
..Cfg::default()
});
h.frame();
h.clear_log();
h.down(200.0, 400.0);
assert_eq!(h.state.outside_taps, 1);
assert!(
h.log().is_empty(),
"the dismissing press never reached the main tree: {:?}",
h.log()
);
}
#[test]
fn an_outside_tap_callback_opts_into_the_notification_by_itself() {
let mut h = Harness::new(Cfg {
outside_tap: None,
..Cfg::default()
});
h.frame();
h.clear_log();
h.down(200.0, 400.0);
assert_eq!(h.state.outside_taps, 1);
assert!(
h.log().is_empty(),
"the implied policy is the modal one (consuming): {:?}",
h.log()
);
}
#[test]
fn an_ignoring_surface_hears_nothing_about_an_outside_press() {
let mut h = Harness::new(Cfg::default());
h.frame();
h.clear_log();
h.down(200.0, 400.0);
assert_eq!(h.state.outside_taps, 0);
assert_eq!(
h.log(),
vec![
"sib:Down@200,400".to_string(),
"child:Down@200,400".to_string()
]
);
}
#[test]
fn a_surface_that_claims_focus_takes_it_from_the_child() {
let mut h = Harness::new(Cfg {
reaction: Reaction::Focus,
preserve_focus: false,
..Cfg::default()
});
h.frame();
h.down(200.0, 400.0);
assert!(h.root.is_focus_active());
h.clear_log();
h.key();
assert_eq!(h.log(), vec!["child:key focus=true".to_string()]);
h.clear_log();
h.down(40.0, 20.0);
h.key();
assert_eq!(
h.log(),
vec![
"pod:Down@32,12".to_string(),
"pod:key focus=true".to_string()
],
"the keyboard follows the surface"
);
assert!(h.root.is_focus_active());
}
#[test]
fn preserve_focus_hands_the_session_back_to_the_child() {
let mut h = Harness::new(Cfg {
reaction: Reaction::Focus,
preserve_focus: true,
..Cfg::default()
});
h.frame();
h.down(200.0, 400.0);
h.clear_log();
h.down(40.0, 20.0);
h.key();
assert_eq!(
h.log(),
vec![
"pod:Down@32,12".to_string(),
"child:key focus=true".to_string()
],
"the field that opened the surface keeps typing"
);
assert!(h.root.is_focus_active(), "and keeps its session");
}
#[test]
fn a_child_that_takes_focus_back_gets_the_keyboard_again() {
let mut h = Harness::new(Cfg {
reaction: Reaction::Focus,
preserve_focus: false,
..Cfg::default()
});
h.frame();
h.down(200.0, 400.0);
h.down(40.0, 20.0);
h.down(200.0, 400.0);
h.clear_log();
h.key();
assert_eq!(
h.log(),
vec!["child:key focus=true".to_string()],
"the surface's older link must not outrank the main tree's live one"
);
}
#[test]
fn a_stale_sibling_link_does_not_outrank_the_surface_that_took_the_session() {
let mut h = Harness::new(Cfg {
shape: Shape::Sibling,
reaction: Reaction::Focus,
placement: OverlayPlacement::on(OverlaySide::Bottom)
.offset(0.0)
.align(OverlayAlign::Start)
.padding(0.0),
..Cfg::default()
});
h.frame();
h.down(200.0, 25.0);
h.clear_log();
h.key();
assert_eq!(h.log(), vec!["field:key focus=true".to_string()]);
h.down(40.0, 110.0);
h.clear_log();
h.key();
assert_eq!(
h.log(),
vec!["pod:key focus=true".to_string()],
"the keyboard follows the branch that actually holds the session"
);
}
#[test]
fn a_surface_that_claims_nothing_never_disturbs_the_focused_child() {
let mut h = Harness::new(Cfg::default());
h.frame();
h.down(200.0, 400.0);
h.clear_log();
h.down(40.0, 20.0);
h.key();
assert_eq!(
h.log(),
vec![
"pod:Down@32,12".to_string(),
"child:key focus=true".to_string()
],
"an overlay press is not a blur"
);
}
#[test]
fn scrolling_an_ancestor_moves_the_surface_on_the_next_paint() {
let mut h = Harness::new(Cfg {
shape: Shape::Scrolled,
placement: OverlayPlacement::default(),
..Cfg::default()
});
let before = rects(&h.frame());
assert_eq!(
before.last().copied(),
Some(Rect::new(20.0, 244.0, 100.0, 274.0)),
"placed under its anchor, 200px down the scrolled column"
);
h.root.event(
&mut h.state,
&InputEvent::Scroll {
position: Point::new(10.0, 10.0),
delta: ScrollDelta::Pixels(0.0, 50.0),
},
);
let after = rects(&h.frame());
assert_eq!(
after.last().copied(),
Some(Rect::new(20.0, 194.0, 100.0, 224.0)),
"the anchor moved with the scroll, and so did the surface"
);
h.clear_log();
h.down(30.0, 200.0);
assert_eq!(h.log(), vec!["pod:Down@10,6".to_string()]);
}
#[test]
fn clearing_the_overlay_removes_it_after_the_next_paint() {
let mut h = Harness::new(Cfg::default());
assert_eq!(rects(&h.frame()).last().copied(), Some(POD));
h.state.cfg.open = false;
let scene = h.frame();
assert!(
!rects(&scene).contains(&POD),
"nothing floated is painted once the view is gone: {:?}",
rects(&scene)
);
h.clear_log();
h.down(40.0, 20.0);
assert_eq!(
h.log(),
vec!["sib:Down@40,20".to_string(), "child:Down@40,20".to_string()],
"and the press reaches the main tree again"
);
}
struct ToolbarHost {
log: Log,
reaction: Reaction,
}
struct ToolbarHostWidget {
slot: OverlaySlot<()>,
view: Option<AnyView<()>>,
log: Log,
reaction: Reaction,
}
impl ToolbarHost {
fn pod(&self) -> AnyView<()> {
any(UnitProbe {
log: Rc::clone(&self.log),
reaction: self.reaction,
})
}
}
impl View<()> for ToolbarHost {
type Element = ToolbarHostWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> ToolbarHostWidget {
let mut slot = OverlaySlot::new();
slot.set_placement(corner());
let view = self.pod();
slot.rebuild(None, Some(&view), ctx);
ToolbarHostWidget {
slot,
view: Some(view),
log: Rc::clone(&self.log),
reaction: self.reaction,
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut ToolbarHostWidget,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.log = Rc::clone(&self.log);
element.reaction = self.reaction;
let view = self.pod();
let flags = element
.slot
.rebuild(element.view.as_ref(), Some(&view), ctx);
element.view = Some(view);
flags
}
}
impl Widget for ToolbarHostWidget {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
self.slot.layout(ctx);
bc.constrain(Size::new(100.0, 40.0))
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
let size = ctx.size();
self.slot.paint(ctx, size);
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
let mut pod_state = ();
let Some(result) = self.slot.event(ctx, event, &mut pod_state) else {
return EventResult::Ignored;
};
for command in ctx.take_edit_commands() {
self.log.borrow_mut().push(format!("cmd:{command:?}"));
}
result
}
}
struct UnitProbe {
log: Log,
reaction: Reaction,
}
struct UnitProbeWidget {
log: Log,
reaction: Reaction,
}
impl View<()> for UnitProbe {
type Element = UnitProbeWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> UnitProbeWidget {
UnitProbeWidget {
log: Rc::clone(&self.log),
reaction: self.reaction,
}
}
fn rebuild(
&self,
_prev: &Self,
element: &mut UnitProbeWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
element.log = Rc::clone(&self.log);
element.reaction = self.reaction;
ChangeFlags::NONE
}
}
impl Widget for UnitProbeWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(Size::new(80.0, 30.0))
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event {
self.log.borrow_mut().push(format!(
"pod:{:?}@{},{}",
p.phase, p.position.x, p.position.y
));
if p.phase == PointerPhase::Down {
match self.reaction {
Reaction::Nothing => {}
Reaction::Capture => ctx.capture_pointer(),
Reaction::Focus => ctx.request_focus(),
}
ctx.dispatch_edit_command(EditCommand::Copy);
ctx.request_redraw();
}
}
EventResult::Handled
}
}
fn unit_harness(reaction: Reaction) -> (RenderRoot<(), ToolbarHost>, Log) {
let log: Log = Rc::new(RefCell::new(Vec::new()));
let mut root: RenderRoot<(), ToolbarHost> = RenderRoot::new();
let captured = Rc::clone(&log);
let mut app_logic = move |_: &mut ()| ToolbarHost {
log: Rc::clone(&captured),
reaction,
};
let mut state = ();
root.rebuild(&mut app_logic, &mut state);
root.layout(WINDOW);
root.paint(&mut RecordingScene::default(), FrameTime::from_nanos(0));
(root, log)
}
#[test]
fn a_unit_typed_pod_is_hosted_and_its_edit_commands_reach_the_owner() {
let (mut root, log) = unit_harness(Reaction::Nothing);
let mut state = ();
root.event(
&mut state,
&InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(40.0, 50.0),
button: PointerButton::Primary,
}),
);
assert_eq!(
log.borrow().clone(),
vec!["pod:Down@32,10".to_string(), "cmd:Copy".to_string()],
"the surface ran over its own `()` state and its command was drained"
);
}
#[test]
fn a_focus_claim_from_a_substituted_pod_still_opens_a_session() {
let (mut root, _log) = unit_harness(Reaction::Focus);
let mut state = ();
assert!(!root.is_focus_active());
root.event(
&mut state,
&InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(40.0, 50.0),
button: PointerButton::Primary,
}),
);
assert!(
root.is_focus_active(),
"the substituted context's focus claim is mirrored onto the owner"
);
}
fn unit_pointer(root: &mut RenderRoot<(), ToolbarHost>, phase: PointerPhase, x: f64, y: f64) {
let mut state = ();
root.event(
&mut state,
&InputEvent::Pointer(PointerEvent {
phase,
position: Point::new(x, y),
button: PointerButton::Primary,
}),
);
}
#[test]
fn a_second_gesture_into_a_substituted_pod_routes_exactly_like_the_first() {
let (mut root, log) = unit_harness(Reaction::Capture);
for label in ["first gesture", "second gesture"] {
log.borrow_mut().clear();
unit_pointer(&mut root, PointerPhase::Down, 40.0, 50.0);
assert!(
root.is_pointer_captured(),
"{label}: the substituted pod's capture was mirrored onto the owner"
);
unit_pointer(&mut root, PointerPhase::Move, 300.0, 300.0);
unit_pointer(&mut root, PointerPhase::Up, 300.0, 300.0);
assert_eq!(
log.borrow().clone(),
vec![
"pod:Down@32,10".to_string(),
"cmd:Copy".to_string(),
"pod:Move@292,260".to_string(),
"pod:Up@292,260".to_string(),
],
"{label}: every phase of the gesture reached the surface"
);
assert!(
!root.is_pointer_captured(),
"{label}: the Up released the capture"
);
}
}
#[test]
fn a_slot_starts_closed_and_hands_its_outside_press_over_once() {
let mut slot: OverlaySlot<()> = OverlaySlot::new();
assert!(!slot.is_open());
assert_eq!(slot.window_rect(), Rect::ZERO);
assert!(!slot.take_outside_down());
let other: OverlaySlot<()> = OverlaySlot::default();
assert_ne!(slot.key(), other.key());
}
}