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 )
	}
}