fret_ui/overlay_placement/types.rs
1pub use fret_core::LayoutDirection;
2
3use fret_core::{Edges, Px, Rect, Size};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Side {
7 Top,
8 Bottom,
9 Left,
10 Right,
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Align {
15 Start,
16 Center,
17 End,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum StickyMode {
22 Partial,
23 #[default]
24 Always,
25}
26
27/// Offset configuration inspired by Floating UI's `offset()` middleware.
28#[derive(Debug, Clone, Copy, Default, PartialEq)]
29pub struct Offset {
30 /// Distance along the placement side axis (the "gap" between anchor and panel).
31 pub main_axis: Px,
32 /// Distance along the alignment axis (skidding).
33 pub cross_axis: Px,
34 /// Optional skidding override for aligned placements (Start/End).
35 ///
36 /// When present and `align != Center`, this overrides `cross_axis` and flips sign for `End`.
37 /// For vertical placements (Top/Bottom), the direction is also flipped under RTL.
38 pub alignment_axis: Option<Px>,
39}
40
41/// Collision/overflow options inspired by Floating UI's `detectOverflow` configuration.
42///
43/// This is applied to the `outer` boundary before running the placement solver:
44///
45/// 1) If `boundary` is set, intersect `outer` with it (clipping ancestor style).
46/// 2) Inset by `padding` (collision padding).
47#[derive(Debug, Clone, Copy, Default, PartialEq)]
48pub struct CollisionOptions {
49 pub padding: Edges,
50 pub boundary: Option<Rect>,
51}
52
53#[derive(Debug, Clone, Copy, Default, PartialEq)]
54pub struct AnchoredPanelOptions {
55 pub direction: LayoutDirection,
56 pub offset: Offset,
57 /// Shift/clamp policy for keeping the floating panel within the collision boundary.
58 ///
59 /// This is inspired by Floating UI's `shift()` middleware options.
60 pub shift: ShiftOptions,
61 pub arrow: Option<ArrowOptions>,
62 pub collision: CollisionOptions,
63 pub sticky: StickyMode,
64}
65
66/// Shift configuration inspired by Floating UI's `shift()` middleware.
67///
68/// - `main_axis` clamps the panel along the placement axis (y for Top/Bottom, x for Left/Right).
69/// - `cross_axis` clamps the panel along the alignment axis (x for Top/Bottom, y for Left/Right).
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct ShiftOptions {
72 pub main_axis: bool,
73 pub cross_axis: bool,
74}
75
76impl Default for ShiftOptions {
77 fn default() -> Self {
78 Self {
79 main_axis: true,
80 cross_axis: true,
81 }
82 }
83}
84
85/// Arrow positioning options inspired by Floating UI's `arrow()` middleware.
86#[derive(Debug, Clone, Copy, PartialEq)]
87pub struct ArrowOptions {
88 /// Arrow element size (in the same coordinate space as `outer`/`anchor`/`content`).
89 pub size: Size,
90 /// Padding between the arrow and the floating element edges (useful for rounded corners).
91 pub padding: Edges,
92}
93
94#[derive(Debug, Clone, Copy, PartialEq)]
95pub struct ArrowLayout {
96 /// Which side of the floating panel the arrow should attach to.
97 pub side: Side,
98 /// Offset along the arrow's axis inside the floating panel (x for Top/Bottom, y for Left/Right).
99 pub offset: Px,
100 /// The alignment-axis translation applied to the panel to keep the arrow pointing at the anchor
101 /// when the anchor is too small (Radix/Floating behavior).
102 pub alignment_offset: Px,
103 /// Signed center delta between the ideal arrow center point and the clamped offset.
104 ///
105 /// This matches Floating UI's `centerOffset` and is used by Radix to determine whether the arrow
106 /// should be hidden (`shouldHideArrow`).
107 pub center_offset: Px,
108}
109
110#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct AnchoredPanelLayout {
112 pub rect: Rect,
113 pub side: Side,
114 pub align: Align,
115 pub arrow: Option<ArrowLayout>,
116}
117
118/// Debug-only trace capturing key decisions made by the anchored panel placement solver.
119///
120/// This is intended for diagnostics evidence (e.g. `fretboard diag`) so scripted repros can report
121/// why a popover/menu was flipped/clamped/shifted without relying on screenshots.
122#[derive(Debug, Clone, Copy, PartialEq)]
123pub struct AnchoredPanelLayoutTrace {
124 /// Solver inputs before applying collision options.
125 pub outer_input: Rect,
126 /// Collision boundary after applying `options.collision` (boundary + padding).
127 pub outer_collision: Rect,
128 pub anchor: Rect,
129 /// Desired (intrinsic) content size.
130 pub desired: Size,
131 /// Requested side gap (before `options.offset.main_axis` is applied).
132 pub side_offset: Px,
133 pub preferred_side: Side,
134 pub align: Align,
135 pub options: AnchoredPanelOptions,
136 /// Effective main-axis gap used by the solver (`side_offset + options.offset.main_axis`).
137 pub gap: Px,
138
139 /// Candidate rect for the preferred side (pre-shift, pre-arrow).
140 pub preferred_rect: Rect,
141 /// Candidate rect for the flipped side (pre-shift, pre-arrow).
142 pub flipped_rect: Rect,
143 /// Whether the preferred side fit without requiring main-axis clamping.
144 pub preferred_fits_without_main_clamp: bool,
145 /// Whether the flipped side fit without requiring main-axis clamping.
146 pub flipped_fits_without_main_clamp: bool,
147
148 pub preferred_available_main_px: f32,
149 pub flipped_available_main_px: f32,
150
151 /// The side chosen by the solver before shift/clamp/arrow adjustments.
152 pub chosen_side: Side,
153 /// The chosen candidate rect before shift/clamp/arrow adjustments.
154 pub chosen_rect: Rect,
155 /// The rect after applying shift/clamp (sticky/shift policy), before arrow adjustments.
156 pub rect_after_shift: Rect,
157 /// Signed delta applied by shift/clamp (after_shift.origin - chosen.origin).
158 pub shift_delta: fret_core::Point,
159
160 /// Final layout output (after shift + arrow).
161 pub layout: AnchoredPanelLayout,
162}