1use fret_core::{Px, Rect};
11use fret_runtime::Model;
12use fret_ui::element::AnyElement;
13use fret_ui::elements::GlobalElementId;
14use fret_ui::{ElementContext, UiHost};
15use std::panic::Location;
16
17use crate::declarative::ModelWatchExt;
18use crate::headless::hover_intent::{HoverIntentConfig, HoverIntentState, HoverIntentUpdate};
19use crate::primitives::popper;
20use crate::{OverlayController, OverlayPresence, OverlayRequest};
21
22pub fn hover_card_root_name(id: GlobalElementId) -> String {
24 OverlayController::hover_overlay_root_name(id)
25}
26
27#[derive(Debug, Clone, Default)]
33pub struct HoverCardRoot {
34 open: Option<Model<bool>>,
35 default_open: bool,
36}
37
38impl HoverCardRoot {
39 pub fn new() -> Self {
40 Self::default()
41 }
42
43 pub fn open(mut self, open: Option<Model<bool>>) -> Self {
45 self.open = open;
46 self
47 }
48
49 pub fn default_open(mut self, default_open: bool) -> Self {
51 self.default_open = default_open;
52 self
53 }
54
55 pub fn use_open_model<H: UiHost>(
57 &self,
58 cx: &mut ElementContext<'_, H>,
59 ) -> crate::primitives::controllable_state::ControllableModel<bool> {
60 hover_card_use_open_model(cx, self.open.clone(), || self.default_open)
61 }
62
63 pub fn open_model<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> Model<bool> {
64 self.use_open_model(cx).model()
65 }
66
67 pub fn is_open<H: UiHost>(&self, cx: &mut ElementContext<'_, H>) -> bool {
69 let open_model = self.open_model(cx);
70 cx.watch_model(&open_model)
71 .layout()
72 .copied()
73 .unwrap_or(false)
74 }
75}
76
77pub fn hover_card_use_open_model<H: UiHost>(
83 cx: &mut ElementContext<'_, H>,
84 controlled_open: Option<Model<bool>>,
85 default_open: impl FnOnce() -> bool,
86) -> crate::primitives::controllable_state::ControllableModel<bool> {
87 crate::primitives::open_state::open_use_model(cx, controlled_open, default_open)
88}
89
90pub fn hover_card_request(
92 id: GlobalElementId,
93 trigger: GlobalElementId,
94 open: Model<bool>,
95 presence: crate::OverlayPresence,
96 children: Vec<AnyElement>,
97) -> OverlayRequest {
98 hover_card_request_with_presence(id, trigger, open, presence, children)
99}
100
101pub fn hover_card_request_with_presence(
103 id: GlobalElementId,
104 trigger: GlobalElementId,
105 open: Model<bool>,
106 presence: OverlayPresence,
107 children: Vec<AnyElement>,
108) -> OverlayRequest {
109 let mut request = OverlayRequest::hover(id, trigger, open, presence, children);
110 request.root_name = Some(hover_card_root_name(id));
111 request
112}
113
114pub fn request_hover_card<H: UiHost>(cx: &mut ElementContext<'_, H>, request: OverlayRequest) {
116 OverlayController::request(cx, request);
117}
118
119pub fn hover_card_hovered(
127 trigger_hovered: bool,
128 overlay_hovered: bool,
129 keyboard_focused: bool,
130) -> bool {
131 trigger_hovered || overlay_hovered || keyboard_focused
132}
133
134#[derive(Debug, Default, Clone, Copy)]
135struct HoverCardIntentDriverState {
136 last_frame_tick: Option<u64>,
137 tick: u64,
138 intent: HoverIntentState,
139 saw_active_since_open: bool,
140 last_pointer_down: bool,
141 close_suppressed_after_pointer_down: bool,
142 saw_text_selection_while_pointer_down: bool,
143}
144
145pub fn hover_card_update_interaction<H: UiHost>(
156 cx: &mut ElementContext<'_, H>,
157 open_now: bool,
158 signal_active: bool,
159 pointer_down_on_content: bool,
160 has_text_selection: bool,
161 cfg: HoverIntentConfig,
162) -> HoverIntentUpdate {
163 let frame_tick = cx.app.frame_id().0;
164 let slot = cx.keyed_slot_id_at(Location::caller(), "hover_card_intent_driver");
165 cx.state_for(slot, HoverCardIntentDriverState::default, |st| {
166 match st.last_frame_tick {
167 None => {
168 st.last_frame_tick = Some(frame_tick);
169 st.tick = frame_tick;
170 }
171 Some(prev) if prev != frame_tick => {
172 st.last_frame_tick = Some(frame_tick);
173 st.tick = frame_tick;
174 }
175 Some(_) => {
176 st.tick = st.tick.saturating_add(1);
179 }
180 }
181
182 if st.intent.is_open() != open_now {
183 st.intent.set_open(open_now);
184 st.saw_active_since_open = false;
185 st.close_suppressed_after_pointer_down = false;
186 st.saw_text_selection_while_pointer_down = false;
187 }
188
189 if pointer_down_on_content && has_text_selection {
190 st.saw_text_selection_while_pointer_down = true;
191 }
192
193 let was_open = st.intent.is_open();
194
195 if pointer_down_on_content != st.last_pointer_down {
196 if pointer_down_on_content {
197 st.close_suppressed_after_pointer_down = false;
198 } else if was_open && !signal_active && !has_text_selection {
199 if !st.saw_text_selection_while_pointer_down {
203 st.close_suppressed_after_pointer_down = true;
204 }
205 }
206 st.last_pointer_down = pointer_down_on_content;
207 if !pointer_down_on_content {
208 st.saw_text_selection_while_pointer_down = false;
209 }
210 }
211 if st.close_suppressed_after_pointer_down && signal_active {
212 st.close_suppressed_after_pointer_down = false;
213 }
214
215 if was_open && (signal_active || pointer_down_on_content) {
216 st.saw_active_since_open = true;
217 }
218
219 let effective_hovered = if was_open {
224 signal_active
225 || pointer_down_on_content
226 || st.close_suppressed_after_pointer_down
227 || has_text_selection
228 || !st.saw_active_since_open
229 } else {
230 signal_active || pointer_down_on_content
231 };
232
233 let out = st.intent.update(effective_hovered, st.tick, cfg);
234 if !was_open && out.open {
235 st.saw_active_since_open = signal_active || pointer_down_on_content;
236 } else if was_open && !out.open {
237 st.saw_active_since_open = false;
238 st.close_suppressed_after_pointer_down = false;
239 st.saw_text_selection_while_pointer_down = false;
240 }
241
242 out
243 })
244}
245
246#[derive(Debug, Clone, Copy, PartialEq)]
247pub struct HoverCardPopperVars {
248 pub available_width: Px,
249 pub available_height: Px,
250 pub trigger_width: Px,
251 pub trigger_height: Px,
252}
253
254pub fn hover_card_popper_desired_width(outer: Rect, anchor: Rect, min_width: Px) -> Px {
255 popper::popper_desired_width(outer, anchor, min_width)
256}
257
258pub fn hover_card_popper_vars(
269 outer: Rect,
270 anchor: Rect,
271 min_width: Px,
272 placement: popper::PopperContentPlacement,
273) -> HoverCardPopperVars {
274 let metrics =
275 popper::popper_available_metrics_for_placement(outer, anchor, min_width, placement);
276 HoverCardPopperVars {
277 available_width: metrics.available_width,
278 available_height: metrics.available_height,
279 trigger_width: metrics.anchor_width,
280 trigger_height: metrics.anchor_height,
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 use fret_app::App;
289 use fret_core::{Point, Size};
290
291 #[test]
292 fn hover_card_root_open_model_uses_controlled_model() {
293 let window = Default::default();
294 let mut app = App::new();
295
296 let controlled = app.models_mut().insert(true);
297 fret_ui::elements::with_element_cx(&mut app, window, Default::default(), "test", |cx| {
298 let root = HoverCardRoot::new()
299 .open(Some(controlled.clone()))
300 .default_open(false);
301 assert_eq!(root.open_model(cx), controlled);
302 });
303 }
304
305 #[test]
306 fn hover_card_request_sets_default_root_name() {
307 let mut app = App::new();
308 let open = app.models_mut().insert(true);
309 fret_ui::elements::with_element_cx(
310 &mut app,
311 Default::default(),
312 Default::default(),
313 "test",
314 move |_cx| {
315 let id = GlobalElementId(0x123);
316 let trigger = GlobalElementId(0x456);
317 let req = hover_card_request(
318 id,
319 trigger,
320 open.clone(),
321 crate::OverlayPresence::instant(true),
322 Vec::new(),
323 );
324 let expected = hover_card_root_name(id);
325 assert_eq!(req.root_name.as_deref(), Some(expected.as_str()));
326 },
327 );
328 }
329
330 #[test]
331 fn hover_card_hovered_or_logic_matches_expectations() {
332 assert!(!hover_card_hovered(false, false, false));
333 assert!(hover_card_hovered(true, false, false));
334 assert!(hover_card_hovered(false, true, false));
335 assert!(hover_card_hovered(false, false, true));
336 }
337
338 #[test]
339 fn hover_card_popper_vars_available_height_tracks_flipped_side_space() {
340 let outer = Rect::new(
341 Point::new(Px(0.0), Px(0.0)),
342 Size::new(Px(100.0), Px(100.0)),
343 );
344 let anchor = Rect::new(
345 Point::new(Px(10.0), Px(70.0)),
346 Size::new(Px(30.0), Px(10.0)),
347 );
348
349 let placement = popper::PopperContentPlacement::new(
350 popper::LayoutDirection::Ltr,
351 popper::Side::Bottom,
352 popper::Align::Start,
353 Px(0.0),
354 );
355 let vars = hover_card_popper_vars(outer, anchor, Px(0.0), placement);
356 assert!(vars.available_height.0 > 60.0 && vars.available_height.0 < 80.0);
357 }
358
359 #[test]
360 fn hover_card_close_is_suppressed_after_pointer_down_leave_until_reenter() {
361 let window = Default::default();
362 let mut app = App::new();
363
364 fret_ui::elements::with_element_cx(&mut app, window, Default::default(), "test", |cx| {
365 let cfg = HoverIntentConfig::new(0, 0);
366 let mut open_now = true;
367
368 open_now = hover_card_update_interaction(cx, open_now, true, true, false, cfg).open;
369 assert!(open_now);
370
371 open_now = hover_card_update_interaction(cx, open_now, false, true, false, cfg).open;
373 assert!(open_now);
374
375 open_now = hover_card_update_interaction(cx, open_now, false, false, false, cfg).open;
377 assert!(open_now);
378
379 open_now = hover_card_update_interaction(cx, open_now, true, false, false, cfg).open;
381 assert!(open_now);
382
383 open_now = hover_card_update_interaction(cx, open_now, false, false, false, cfg).open;
385 assert!(!open_now);
386 });
387 }
388
389 #[test]
390 fn hover_card_default_open_does_not_close_until_active_then_leave() {
391 let window = Default::default();
392 let mut app = App::new();
393
394 fret_ui::elements::with_element_cx(&mut app, window, Default::default(), "test", |cx| {
395 let cfg = HoverIntentConfig::new(0, 0);
396 let mut open_now = true;
397
398 open_now = hover_card_update_interaction(cx, open_now, false, false, false, cfg).open;
400 assert!(open_now);
401
402 open_now = hover_card_update_interaction(cx, open_now, true, false, false, cfg).open;
403 assert!(open_now);
404
405 open_now = hover_card_update_interaction(cx, open_now, false, false, false, cfg).open;
406 assert!(!open_now);
407 });
408 }
409
410 #[test]
411 fn hover_card_text_selection_release_clears_without_reenter() {
412 let window = Default::default();
413 let mut app = App::new();
414
415 fret_ui::elements::with_element_cx(&mut app, window, Default::default(), "test", |cx| {
416 let cfg = HoverIntentConfig::new(0, 0);
417 let mut open_now = true;
418
419 open_now = hover_card_update_interaction(cx, open_now, true, true, true, cfg).open;
421 assert!(open_now);
422
423 open_now = hover_card_update_interaction(cx, open_now, false, true, true, cfg).open;
425 assert!(open_now);
426
427 open_now = hover_card_update_interaction(cx, open_now, false, false, true, cfg).open;
429 assert!(open_now);
430
431 open_now = hover_card_update_interaction(cx, open_now, false, false, false, cfg).open;
433 assert!(!open_now);
434 });
435 }
436
437 #[test]
438 fn hover_card_text_selection_cleared_after_stale_pointer_down_closes() {
439 let window = Default::default();
440 let mut app = App::new();
441
442 fret_ui::elements::with_element_cx(&mut app, window, Default::default(), "test", |cx| {
443 let cfg = HoverIntentConfig::new(0, 0);
444 let mut open_now = true;
445
446 open_now = hover_card_update_interaction(cx, open_now, true, true, true, cfg).open;
447 assert!(open_now);
448
449 open_now = hover_card_update_interaction(cx, open_now, false, true, true, cfg).open;
451 assert!(open_now);
452
453 open_now = hover_card_update_interaction(cx, open_now, false, false, false, cfg).open;
456 assert!(!open_now);
457 });
458 }
459}