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 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
// 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::layout::stack::HAlign;
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( crate ) child: Box<Element<Msg>>,
/// Stable identifier of the widget whose rect provides the anchor.
pub( crate ) anchor_id: WidgetId,
/// Vertical pixel gap between the bottom of the anchor and the top
/// of the child.
pub( crate ) gap: f32,
/// Which of the anchor's edges the child lines up with: `Start` its left one, `End` its right one, `Center` its middle.
pub( crate ) align: HAlign,
}
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,
align: HAlign::Start,
}
}
/// The vertical space, in physical pixels, between the anchor's bottom edge and the child's top edge. Default: `0`.
pub fn gap( mut self, gap: f32 ) -> Self
{
self.gap = gap;
self
}
/// Line the child up with the anchor's left edge (`Start`, the default), its right edge (`End`) or its middle (`Center`).
pub fn align( mut self, align: HAlign ) -> Self
{
self.align = align;
self
}
/// The child's left edge, `width` wide, against an anchor at `anchor`, kept within `bounds`.
pub( crate ) fn left( &self, anchor: Rect, width: f32, bounds: Rect ) -> f32
{
let x = match self.align
{
HAlign::End => anchor.x + anchor.width - width,
HAlign::Center => anchor.x + ( anchor.width - width ) / 2.0,
_ => anchor.x,
};
x.min( bounds.x + bounds.width - width ).max( bounds.x )
}
/// 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,
align: self.align,
}
}
}
impl<Msg: Clone + 'static> From<AnchoredOverlay<Msg>> for Element<Msg>
{
fn from( a: AnchoredOverlay<Msg> ) -> Self
{
Element::AnchoredOverlay( a )
}
}
/// Draw `child` right under the widget whose `.id( … )` is `anchor`, as it was laid out on the previous frame: a menu under the
/// button that opens it. Until the anchor has been drawn the child fills the rect its parent gives it.
pub fn anchored<Msg: Clone>( child: impl Into<Element<Msg>>, anchor: WidgetId ) -> AnchoredOverlay<Msg>
{
AnchoredOverlay::new( child, anchor, 0.0 )
}