1use crate::diagnostics::{BuildDiagnostics, Spanned};
31use crate::expression_tree::{BindingExpression, BuiltinFunction, Expression, Unit};
32use crate::langtype::{ElementType, EnumerationValue};
33use crate::namedreference::NamedReference;
34use crate::object_tree::*;
35use crate::typeregister::{BUILTIN, TypeRegister};
36use smol_str::{SmolStr, format_smolstr};
37use std::cell::RefCell;
38use std::rc::Rc;
39
40const TOOLTIP_ELEMENT: &str = "Tooltip";
41const TOOLTIP_IMPL_ELEMENT: &str = "ToolTipImpl";
42const TOOLTIP_AREA_ELEMENT: &str = "TooltipArea";
43const POPUP_WINDOW_ELEMENT: &str = "PopupWindow";
44const TOOLTIP_POPUP_ID_PREFIX: &str = "tooltip-popup-overlay-";
45const LAYOUT_ELEMENTS_DISALLOWING_TOOLTIP: &[&str] =
46 &["GridLayout", "VerticalLayout", "HorizontalLayout", "FlexboxLayout"];
47
48const MOUSE_X: &str = "mouse-x";
49const MOUSE_Y: &str = "mouse-y";
50const WIDTH: &str = "width";
51const HEIGHT: &str = "height";
52const OFFSET: &str = "offset";
53const TEXT: &str = "text";
54
55fn check_no_reference_to_tooltip(
59 tooltip_element: &ElementRc,
60 parent_element: &ElementRc,
61 component: &Rc<Component>,
62 diag: &mut BuildDiagnostics,
63) {
64 let dummy_ref = NamedReference::new(parent_element, SmolStr::new_static(WIDTH));
65
66 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |source_elem, _| {
67 if Rc::ptr_eq(source_elem, tooltip_element) {
68 return;
69 }
70 visit_all_named_references_in_element(source_elem, |nr| {
71 if !Rc::ptr_eq(&nr.element(), tooltip_element) {
72 return;
73 }
74 let id = tooltip_element.borrow().id.clone();
75 let prop_name = nr.name();
76 let what = if id.is_empty() {
77 format!("property or callback '{prop_name}'")
78 } else {
79 format!("property or callback '{id}.{prop_name}'")
80 };
81 diag.push_error(
82 format!("Cannot access {what} inside of a Tooltip from enclosing component"),
83 &*tooltip_element.borrow(),
84 );
85 *nr = dummy_ref.clone();
86 });
87 });
88}
89
90fn build_tooltip_content(
91 popup_id: &SmolStr,
92 enclosing_component: &std::rc::Weak<Component>,
93 tooltip_impl_type: &ElementType,
94 tooltip_text: Option<NamedReference>,
95 children: Vec<ElementRc>,
96) -> ElementRc {
97 let mut bindings = std::collections::BTreeMap::new();
98 if let Some(tooltip_text) = tooltip_text {
99 bindings.insert(
100 SmolStr::new_static("text"),
101 RefCell::new(Expression::PropertyReference(tooltip_text).into()),
102 );
103 }
104 Element {
105 id: format_smolstr!("{}-content", popup_id),
106 base_type: tooltip_impl_type.clone(),
107 enclosing_component: enclosing_component.clone(),
108 bindings,
109 children,
110 ..Default::default()
111 }
112 .make_rc()
113}
114
115fn bind_popup_effective_size_from_content(
116 popup_window_rc: &ElementRc,
117 tooltip_content_rc: &ElementRc,
118) {
119 let content_has_width = tooltip_content_rc.borrow().bindings.contains_key(WIDTH);
120 let content_has_height = tooltip_content_rc.borrow().bindings.contains_key(HEIGHT);
121
122 if content_has_width {
123 let explicit_width = NamedReference::new(tooltip_content_rc, SmolStr::new_static(WIDTH));
124 let mut width_binding: BindingExpression =
125 Expression::PropertyReference(explicit_width).into();
126 width_binding.priority = 1;
127 popup_window_rc
128 .borrow_mut()
129 .bindings
130 .insert(SmolStr::new_static(WIDTH), RefCell::new(width_binding));
131 } else {
132 let preferred_width =
133 NamedReference::new(tooltip_content_rc, SmolStr::new_static("preferred-width"));
134 let mut width_binding: BindingExpression =
135 Expression::PropertyReference(preferred_width).into();
136 width_binding.priority = 1;
137 popup_window_rc
138 .borrow_mut()
139 .bindings
140 .insert(SmolStr::new_static(WIDTH), RefCell::new(width_binding));
141 }
142 if content_has_height {
143 let explicit_height = NamedReference::new(tooltip_content_rc, SmolStr::new_static(HEIGHT));
144 let mut height_binding: BindingExpression =
145 Expression::PropertyReference(explicit_height).into();
146 height_binding.priority = 1;
147 popup_window_rc
148 .borrow_mut()
149 .bindings
150 .insert(SmolStr::new_static(HEIGHT), RefCell::new(height_binding));
151 } else {
152 let preferred_height =
153 NamedReference::new(tooltip_content_rc, SmolStr::new_static("preferred-height"));
154 let mut height_binding: BindingExpression =
155 Expression::PropertyReference(preferred_height).into();
156 height_binding.priority = 1;
157 popup_window_rc
158 .borrow_mut()
159 .bindings
160 .insert(SmolStr::new_static(HEIGHT), RefCell::new(height_binding));
161 }
162}
163
164fn build_tooltip_area(
165 popup_id: &SmolStr,
166 enclosing_component: &std::rc::Weak<Component>,
167 tooltip_area_type: &ElementType,
168 repeated: Option<RepeatedElementInfo>,
169) -> ElementRc {
170 let mut elem = Element {
171 id: format_smolstr!("{}-area", popup_id),
172 base_type: tooltip_area_type.clone(),
173 enclosing_component: enclosing_component.clone(),
174 bindings: [
175 (
176 SmolStr::new_static("x"),
177 RefCell::new(Expression::NumberLiteral(0., Unit::Percent).into()),
178 ),
179 (
180 SmolStr::new_static("y"),
181 RefCell::new(Expression::NumberLiteral(0., Unit::Percent).into()),
182 ),
183 (
184 SmolStr::new_static(WIDTH),
185 RefCell::new(Expression::NumberLiteral(100., Unit::Percent).into()),
186 ),
187 (
188 SmolStr::new_static(HEIGHT),
189 RefCell::new(Expression::NumberLiteral(100., Unit::Percent).into()),
190 ),
191 ]
192 .into_iter()
193 .collect(),
194 repeated,
195 ..Default::default()
196 };
197 crate::object_tree::apply_default_type_properties(&mut elem);
201 elem.make_rc()
202}
203
204fn wire_tooltip_placement(
205 popup_window_rc: &ElementRc,
206 pointer_x: NamedReference,
207 pointer_y: NamedReference,
208 tooltip_offset: NamedReference,
209) {
210 let tooltip_offset_expr = Expression::PropertyReference(tooltip_offset);
211 let x_pointer = Expression::PropertyReference(pointer_x);
212 let y_pointer = Expression::BinaryExpression {
213 lhs: Box::new(Expression::PropertyReference(pointer_y)),
214 rhs: Box::new(tooltip_offset_expr),
215 op: '+',
216 };
217
218 let mut x_binding: BindingExpression = x_pointer.into();
219 x_binding.priority = 1;
220 popup_window_rc.borrow_mut().bindings.insert(SmolStr::new_static("x"), RefCell::new(x_binding));
221
222 let mut y_binding: BindingExpression = y_pointer.into();
223 y_binding.priority = 1;
224 popup_window_rc.borrow_mut().bindings.insert(SmolStr::new_static("y"), RefCell::new(y_binding));
225}
226
227fn wire_tooltip_visibility_behavior(
228 elem: &ElementRc,
229 tooltip_child_index: usize,
230 tooltip_area: &ElementRc,
231 popup_window_rc: ElementRc,
232) {
233 let popup_weak = Rc::downgrade(&popup_window_rc);
234 let show_popup = Expression::FunctionCall {
235 function: BuiltinFunction::ShowPopupWindow.into(),
236 arguments: vec![Expression::ElementReference(popup_weak.clone())],
237 source_location: None,
238 };
239 let close_popup = Expression::FunctionCall {
240 function: BuiltinFunction::ClosePopupWindow.into(),
241 arguments: vec![Expression::ElementReference(popup_weak)],
242 source_location: None,
243 };
244
245 tooltip_area.borrow_mut().bindings.insert(
246 SmolStr::new_static("show"),
247 RefCell::new(Expression::CodeBlock(vec![show_popup]).into()),
248 );
249 tooltip_area.borrow_mut().bindings.insert(
250 SmolStr::new_static("hide"),
251 RefCell::new(Expression::CodeBlock(vec![close_popup]).into()),
252 );
253
254 tooltip_area.borrow_mut().children.push(popup_window_rc);
258 elem.borrow_mut().children.insert(tooltip_child_index, tooltip_area.clone());
259}
260
261fn lower_tooltips_in_component(
262 component: &Rc<Component>,
263 type_register: &TypeRegister,
264 tooltip_impl_type: &ElementType,
265 diag: &mut BuildDiagnostics,
266) {
267 let tooltip_type = type_register.lookup_builtin_element(TOOLTIP_ELEMENT).unwrap();
268 let tooltip_area_type = type_register.lookup_builtin_element(TOOLTIP_AREA_ELEMENT).unwrap();
269 let popup_window_type = type_register.lookup_builtin_element(POPUP_WINDOW_ELEMENT).unwrap();
270
271 let popup_close_policy_enum = BUILTIN.with(|e| e.enums.PopupClosePolicy.clone());
272 let popup_close_policy_no_auto_close = EnumerationValue {
273 value: popup_close_policy_enum.values.iter().position(|v| v == "no-auto-close").unwrap(),
274 enumeration: popup_close_policy_enum,
275 };
276
277 let mut tooltip_popup_id_counter: u32 = 0;
278 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
279 let is_generated_tooltip_popup = {
281 let elem_borrow = elem.borrow();
282 matches!(&elem_borrow.base_type, t if *t == popup_window_type) && elem_borrow.is_tooltip
283 };
284 if is_generated_tooltip_popup {
285 return;
286 }
287
288 let is_tooltip_like =
289 matches!(&elem.borrow().builtin_type(), Some(b) if b.name == TOOLTIP_ELEMENT);
290 let is_direct_tooltip = matches!(&elem.borrow().base_type, t if *t == tooltip_type);
291 if is_tooltip_like && !is_direct_tooltip {
292 diag.push_error("Tooltip cannot be inherited".into(), &*elem.borrow());
293 return;
294 }
295
296 let tooltip_indices: Vec<usize> = elem
297 .borrow()
298 .children
299 .iter()
300 .enumerate()
301 .filter_map(|(idx, child)| {
302 matches!(&child.borrow().base_type, t if *t == tooltip_type).then_some(idx)
303 })
304 .collect();
305 if tooltip_indices.is_empty() {
306 return;
307 }
308 if tooltip_indices.len() > 1 {
309 let children = elem.borrow().children.clone();
310 for idx in tooltip_indices.iter().skip(1) {
311 let child = &children[*idx];
312 diag.push_error(
313 "Only one Tooltip is allowed as a child of an element".into(),
314 &*child.borrow(),
315 );
316 }
317 return;
318 }
319 let tooltip_child_index = tooltip_indices[0];
320
321 let tooltip_candidate = elem.borrow().children[tooltip_child_index].clone();
322 let tooltip_repeated = tooltip_candidate.borrow_mut().repeated.take();
326 if tooltip_repeated.as_ref().is_some_and(|r| !r.is_conditional_element) {
327 diag.push_error(
328 "Tooltip cannot be in a `for` element".into(),
329 &*tooltip_candidate.borrow(),
330 );
331 return;
332 }
333 let parent_name = elem.borrow().builtin_type().map(|b| b.name.clone());
334 if parent_name
335 .as_ref()
336 .is_some_and(|name| LAYOUT_ELEMENTS_DISALLOWING_TOOLTIP.contains(&name.as_str()))
337 {
338 diag.push_error(
339 format!("Tooltip cannot be added to {}", parent_name.as_ref().unwrap()),
340 &*tooltip_candidate.borrow(),
341 );
342 return;
343 }
344 if elem.borrow().builtin_type().is_some_and(|builtin| {
345 builtin.is_non_item_type || builtin.disallow_global_types_as_child_elements
346 }) {
347 diag.push_error(
348 format!("Tooltip cannot be added to {}", parent_name.as_ref().unwrap()),
349 &*tooltip_candidate.borrow(),
350 );
351 return;
352 }
353
354 let has_custom_content = !tooltip_candidate.borrow().children.is_empty();
355 let has_text_binding = tooltip_candidate.borrow().bindings.contains_key("text");
356 if has_custom_content && has_text_binding {
357 diag.push_error(
358 "Tooltip cannot have both text and custom content".into(),
359 &*tooltip_candidate.borrow(),
360 );
361 return;
362 }
363 if !has_custom_content && !has_text_binding {
364 diag.push_error(
365 "Tooltip must provide either text or custom content".into(),
366 &*tooltip_candidate.borrow(),
367 );
368 return;
369 }
370 if has_custom_content && tooltip_candidate.borrow().children.len() > 1 {
371 diag.push_error(
372 "Tooltip custom content must have exactly one root child element".into(),
373 &*tooltip_candidate.borrow(),
374 );
375 return;
376 }
377
378 check_no_reference_to_tooltip(&tooltip_candidate, elem, component, diag);
379
380 let (tooltip_config, enclosing_component, popup_id, custom_children) = {
381 let mut elem_borrow = elem.borrow_mut();
382 let tooltip_config = elem_borrow.children.remove(tooltip_child_index);
383 let custom_children = if has_custom_content {
384 std::mem::take(&mut tooltip_config.borrow_mut().children)
385 } else {
386 Vec::new()
387 };
388 let enclosing_component = elem_borrow.enclosing_component.clone();
389 let popup_id =
390 format_smolstr!("{}{}", TOOLTIP_POPUP_ID_PREFIX, tooltip_popup_id_counter);
391 tooltip_popup_id_counter += 1;
392 (tooltip_config, enclosing_component, popup_id, custom_children)
393 };
394
395 let tooltip_area = build_tooltip_area(
396 &popup_id,
397 &enclosing_component,
398 &tooltip_area_type,
399 tooltip_repeated,
400 );
401 let copy_binding = |property: &str| {
405 if let Some(binding) = tooltip_config.borrow().bindings.get(property) {
406 tooltip_area
407 .borrow_mut()
408 .bindings
409 .insert(SmolStr::new(property), RefCell::new(binding.borrow().clone()));
410 }
411 };
412 if has_text_binding {
413 copy_binding(TEXT);
414 }
415
416 let tooltip_offset = NamedReference::new(&tooltip_area, SmolStr::new_static(OFFSET));
417 let pointer_x = NamedReference::new(&tooltip_area, SmolStr::new_static(MOUSE_X));
418 let pointer_y = NamedReference::new(&tooltip_area, SmolStr::new_static(MOUSE_Y));
419 let tooltip_text = (!has_custom_content)
420 .then(|| NamedReference::new(&tooltip_area, SmolStr::new_static(TEXT)));
421 let tooltip_content = build_tooltip_content(
422 &popup_id,
423 &enclosing_component,
424 tooltip_impl_type,
425 tooltip_text,
426 custom_children,
427 );
428 let popup_children = vec![tooltip_content.clone()];
429
430 let popup_window = Element {
431 id: popup_id,
432 base_type: popup_window_type.clone(),
433 enclosing_component: enclosing_component.clone(),
434 is_tooltip: true,
435 children: popup_children,
436 bindings: [(
437 SmolStr::new_static("close-policy"),
438 RefCell::new(
439 Expression::EnumerationValue(popup_close_policy_no_auto_close.clone()).into(),
440 ),
441 )]
442 .into_iter()
443 .collect(),
444 debug: tooltip_config.borrow().debug.clone(),
447 ..Default::default()
448 };
449 let popup_window_rc = popup_window.make_rc();
450 bind_popup_effective_size_from_content(&popup_window_rc, &tooltip_content);
451 wire_tooltip_placement(&popup_window_rc, pointer_x, pointer_y, tooltip_offset);
452
453 wire_tooltip_visibility_behavior(elem, tooltip_child_index, &tooltip_area, popup_window_rc);
454 });
455}
456
457pub async fn lower_tooltips(
458 doc: &Document,
459 type_loader: &mut crate::typeloader::TypeLoader,
460 diag: &mut BuildDiagnostics,
461) {
462 let mut has_tooltip = false;
463 doc.visit_all_used_components(|component| {
464 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
465 if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == TOOLTIP_ELEMENT) {
466 has_tooltip = true;
467 }
468 })
469 });
470
471 if !has_tooltip {
472 return;
473 }
474
475 let mut import_diag = BuildDiagnostics::default();
476 let tooltip_component = type_loader
477 .import_component("std-widgets-impl.slint", TOOLTIP_IMPL_ELEMENT, &mut import_diag)
478 .await;
479 for diagnostic in import_diag {
480 diag.push_compiler_error(diagnostic);
481 }
482 let Some(tooltip_component) = tooltip_component else {
483 let generic_location = doc.node.as_ref().map(|n| n.to_source_location());
484 diag.push_error(
485 "`Tooltip` style implementation could not be loaded from std-widgets".into(),
486 &generic_location,
487 );
488 return;
489 };
490 let tooltip_style_type = ElementType::Component(tooltip_component);
491
492 doc.visit_all_used_components(|component| {
493 lower_tooltips_in_component(component, &doc.local_registry, &tooltip_style_type, diag);
494 });
495}