ltk/widget/anchored_overlay/mod.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
//! Position a child element below the on-screen rect of another widget,
//! looked up from the *previous* frame's [`LaidOutWidget`] snapshot.
//!
//! The classic use case is a combo / dropdown popup that should appear
//! flush below its trigger. The trigger's rect is not known in `view()`
//! time — it's assigned during the layout pass — so the popup widget
//! cannot place itself there directly. `AnchoredOverlay` solves this
//! with a one-frame-old anchor: the trigger carries a stable
//! [`WidgetId`], the popup wraps in `AnchoredOverlay` referencing the
//! same id, and at draw time the wrapper looks up the trigger's rect
//! from the runtime's persisted `widget_rects` (frame N − 1) and
//! overrides the rect that its parent gave it.
//!
//! When the anchor is not found (first frame after open, missing id,
//! widget went off-screen) the wrapper falls back to drawing the child
//! in the parent-supplied rect — which, for a typical Stack-overlay
//! root in a `view()`, means the full surface, giving a sane modal
//! fallback until the next frame fixes the position.
use crate::types::{ Rect, WidgetId };
use super::Element;
#[ cfg( test ) ]
mod tests;
/// A wrapper that re-positions its child relative to an anchor widget
/// found in the previous frame's layout snapshot.
pub struct AnchoredOverlay<Msg: Clone>
{
/// The element to draw at the anchored position.
pub child: Box<Element<Msg>>,
/// Stable identifier of the widget whose rect provides the anchor.
pub anchor_id: WidgetId,
/// Vertical pixel gap between the bottom of the anchor and the top
/// of the child.
pub gap: f32,
}
impl<Msg: Clone> AnchoredOverlay<Msg>
{
/// Wrap `child` so it draws anchored below the widget that carries
/// `anchor_id` in its `.id( … )` builder. `gap` is the vertical
/// space (logical pixels) between the anchor's bottom edge and the
/// child's top edge.
pub fn new( child: impl Into<Element<Msg>>, anchor_id: WidgetId, gap: f32 ) -> Self
{
Self
{
child: Box::new( child.into() ),
anchor_id,
gap,
}
}
/// Compute the draw rect for the child, given the anchor rect (when
/// available) and the parent-supplied fallback.
///
/// Anchor available → place the child flush below the anchor with
/// `gap` spacing, preserving the anchor's width.
/// Anchor missing → return the parent rect verbatim, so the child
/// renders modal-style as a fallback.
pub fn resolve_rect( anchor: Option<Rect>, gap: f32, fallback: Rect ) -> Rect
{
match anchor
{
Some( a ) => Rect
{
x: a.x,
y: a.y + a.height + gap,
width: a.width,
height: fallback.height,
},
None => fallback,
}
}
pub( crate ) fn map_msg<U>( self, f: &super::MapFn<Msg, U> ) -> AnchoredOverlay<U>
where
U: Clone + 'static,
Msg: 'static,
{
AnchoredOverlay
{
child: Box::new( self.child.map_arc( f ) ),
anchor_id: self.anchor_id,
gap: self.gap,
}
}
}
impl<Msg: Clone + 'static> From<AnchoredOverlay<Msg>> for Element<Msg>
{
fn from( a: AnchoredOverlay<Msg> ) -> Self
{
Element::AnchoredOverlay( a )
}
}