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