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: bindings.into(),
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().binding(WIDTH).is_some();
120 let content_has_height = tooltip_content_rc.borrow().binding(HEIGHT).is_some();
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.borrow_mut().set_binding(SmolStr::new_static(WIDTH), width_binding);
128 } else {
129 let preferred_width =
130 NamedReference::new(tooltip_content_rc, SmolStr::new_static("preferred-width"));
131 let mut width_binding: BindingExpression =
132 Expression::PropertyReference(preferred_width).into();
133 width_binding.priority = 1;
134 popup_window_rc.borrow_mut().set_binding(SmolStr::new_static(WIDTH), width_binding);
135 }
136 if content_has_height {
137 let explicit_height = NamedReference::new(tooltip_content_rc, SmolStr::new_static(HEIGHT));
138 let mut height_binding: BindingExpression =
139 Expression::PropertyReference(explicit_height).into();
140 height_binding.priority = 1;
141 popup_window_rc.borrow_mut().set_binding(SmolStr::new_static(HEIGHT), height_binding);
142 } else {
143 let preferred_height =
144 NamedReference::new(tooltip_content_rc, SmolStr::new_static("preferred-height"));
145 let mut height_binding: BindingExpression =
146 Expression::PropertyReference(preferred_height).into();
147 height_binding.priority = 1;
148 popup_window_rc.borrow_mut().set_binding(SmolStr::new_static(HEIGHT), height_binding);
149 }
150}
151
152fn build_tooltip_area(
153 popup_id: &SmolStr,
154 enclosing_component: &std::rc::Weak<Component>,
155 tooltip_area_type: &ElementType,
156 repeated: Option<RepeatedElementInfo>,
157) -> ElementRc {
158 let mut elem = Element {
159 id: format_smolstr!("{}-area", popup_id),
160 base_type: tooltip_area_type.clone(),
161 enclosing_component: enclosing_component.clone(),
162 bindings: [
163 (
164 SmolStr::new_static("x"),
165 RefCell::new(Expression::NumberLiteral(0., Unit::Percent).into()),
166 ),
167 (
168 SmolStr::new_static("y"),
169 RefCell::new(Expression::NumberLiteral(0., Unit::Percent).into()),
170 ),
171 (
172 SmolStr::new_static(WIDTH),
173 RefCell::new(Expression::NumberLiteral(100., Unit::Percent).into()),
174 ),
175 (
176 SmolStr::new_static(HEIGHT),
177 RefCell::new(Expression::NumberLiteral(100., Unit::Percent).into()),
178 ),
179 ]
180 .into_iter()
181 .collect(),
182 repeated,
183 ..Default::default()
184 };
185 crate::object_tree::apply_default_type_properties(&mut elem);
189 elem.make_rc()
190}
191
192fn wire_tooltip_placement(
193 popup_window_rc: &ElementRc,
194 pointer_x: NamedReference,
195 pointer_y: NamedReference,
196 tooltip_offset: NamedReference,
197) {
198 let tooltip_offset_expr = Expression::PropertyReference(tooltip_offset);
199 let x_pointer = Expression::PropertyReference(pointer_x);
200 let y_pointer = Expression::BinaryExpression {
201 lhs: Box::new(Expression::PropertyReference(pointer_y)),
202 rhs: Box::new(tooltip_offset_expr),
203 op: '+',
204 source_location: None,
205 };
206
207 let mut x_binding: BindingExpression = x_pointer.into();
208 x_binding.priority = 1;
209 popup_window_rc.borrow_mut().set_binding(SmolStr::new_static("x"), x_binding);
210
211 let mut y_binding: BindingExpression = y_pointer.into();
212 y_binding.priority = 1;
213 popup_window_rc.borrow_mut().set_binding(SmolStr::new_static("y"), y_binding);
214}
215
216fn wire_tooltip_visibility_behavior(
217 elem: &ElementRc,
218 tooltip_child_index: usize,
219 tooltip_area: &ElementRc,
220 popup_window_rc: ElementRc,
221) {
222 let popup_weak = Rc::downgrade(&popup_window_rc);
223 let show_popup = Expression::FunctionCall {
224 function: BuiltinFunction::ShowPopupWindow.into(),
225 arguments: vec![Expression::ElementReference(popup_weak.clone())],
226 source_location: None,
227 };
228 let close_popup = Expression::FunctionCall {
229 function: BuiltinFunction::ClosePopupWindow.into(),
230 arguments: vec![Expression::ElementReference(popup_weak)],
231 source_location: None,
232 };
233
234 tooltip_area
235 .borrow_mut()
236 .set_binding(SmolStr::new_static("show"), Expression::CodeBlock(vec![show_popup]).into());
237 tooltip_area
238 .borrow_mut()
239 .set_binding(SmolStr::new_static("hide"), Expression::CodeBlock(vec![close_popup]).into());
240
241 tooltip_area.borrow_mut().children.push(popup_window_rc);
245 elem.borrow_mut().children.insert(tooltip_child_index, tooltip_area.clone());
246}
247
248fn lower_tooltips_in_component(
249 component: &Rc<Component>,
250 type_register: &TypeRegister,
251 tooltip_impl_type: &ElementType,
252 diag: &mut BuildDiagnostics,
253) {
254 let tooltip_type = type_register.lookup_builtin_element(TOOLTIP_ELEMENT).unwrap();
255 let tooltip_area_type = type_register.lookup_builtin_element(TOOLTIP_AREA_ELEMENT).unwrap();
256 let popup_window_type = type_register.lookup_builtin_element(POPUP_WINDOW_ELEMENT).unwrap();
257
258 let popup_close_policy_enum = BUILTIN.enums.PopupClosePolicy.clone();
259 let popup_close_policy_no_auto_close = EnumerationValue {
260 value: popup_close_policy_enum.values.iter().position(|v| v == "no-auto-close").unwrap(),
261 enumeration: popup_close_policy_enum,
262 };
263
264 let mut tooltip_popup_id_counter: u32 = 0;
265 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
266 let is_generated_tooltip_popup = {
268 let elem_borrow = elem.borrow();
269 matches!(&elem_borrow.base_type, t if *t == popup_window_type) && elem_borrow.is_tooltip
270 };
271 if is_generated_tooltip_popup {
272 return;
273 }
274
275 let is_tooltip_like =
276 matches!(&elem.borrow().builtin_type(), Some(b) if b.name == TOOLTIP_ELEMENT);
277 let is_direct_tooltip = matches!(&elem.borrow().base_type, t if *t == tooltip_type);
278 if is_tooltip_like && !is_direct_tooltip {
279 diag.push_error("Tooltip cannot be inherited".into(), &*elem.borrow());
280 return;
281 }
282
283 let tooltip_indices: Vec<usize> = elem
284 .borrow()
285 .children
286 .iter()
287 .enumerate()
288 .filter_map(|(idx, child)| {
289 matches!(&child.borrow().base_type, t if *t == tooltip_type).then_some(idx)
290 })
291 .collect();
292 if tooltip_indices.is_empty() {
293 return;
294 }
295 if tooltip_indices.len() > 1 {
296 let children = elem.borrow().children.clone();
297 for idx in tooltip_indices.iter().skip(1) {
298 let child = &children[*idx];
299 diag.push_error(
300 "Only one Tooltip is allowed as a child of an element".into(),
301 &*child.borrow(),
302 );
303 }
304 return;
305 }
306 let tooltip_child_index = tooltip_indices[0];
307
308 let tooltip_candidate = elem.borrow().children[tooltip_child_index].clone();
309 let tooltip_repeated = tooltip_candidate.borrow_mut().repeated.take();
313 if tooltip_repeated.as_ref().is_some_and(|r| !r.is_conditional_element) {
314 diag.push_error(
315 "Tooltip cannot be in a `for` element".into(),
316 &*tooltip_candidate.borrow(),
317 );
318 return;
319 }
320 let parent_name = elem.borrow().builtin_type().map(|b| b.name.clone());
321 if parent_name
322 .as_ref()
323 .is_some_and(|name| LAYOUT_ELEMENTS_DISALLOWING_TOOLTIP.contains(&name.as_str()))
324 {
325 diag.push_error(
326 format!("Tooltip cannot be added to {}", parent_name.as_ref().unwrap()),
327 &*tooltip_candidate.borrow(),
328 );
329 return;
330 }
331 if elem.borrow().builtin_type().is_some_and(|builtin| {
332 builtin.is_non_item_type || builtin.disallow_global_types_as_child_elements
333 }) {
334 diag.push_error(
335 format!("Tooltip cannot be added to {}", parent_name.as_ref().unwrap()),
336 &*tooltip_candidate.borrow(),
337 );
338 return;
339 }
340
341 let has_custom_content = !tooltip_candidate.borrow().children.is_empty();
342 let has_text_binding = tooltip_candidate.borrow().binding("text").is_some();
343 if has_custom_content && has_text_binding {
344 diag.push_error(
345 "Tooltip cannot have both text and custom content".into(),
346 &*tooltip_candidate.borrow(),
347 );
348 return;
349 }
350 if !has_custom_content && !has_text_binding {
351 diag.push_error(
352 "Tooltip must provide either text or custom content".into(),
353 &*tooltip_candidate.borrow(),
354 );
355 return;
356 }
357 if has_custom_content && tooltip_candidate.borrow().children.len() > 1 {
358 diag.push_error(
359 "Tooltip custom content must have exactly one root child element".into(),
360 &*tooltip_candidate.borrow(),
361 );
362 return;
363 }
364
365 check_no_reference_to_tooltip(&tooltip_candidate, elem, component, diag);
366
367 let (tooltip_config, enclosing_component, popup_id, custom_children) = {
368 let mut elem_borrow = elem.borrow_mut();
369 let tooltip_config = elem_borrow.children.remove(tooltip_child_index);
370 let custom_children = if has_custom_content {
371 std::mem::take(&mut tooltip_config.borrow_mut().children)
372 } else {
373 Vec::new()
374 };
375 let enclosing_component = elem_borrow.enclosing_component.clone();
376 let popup_id =
377 format_smolstr!("{}{}", TOOLTIP_POPUP_ID_PREFIX, tooltip_popup_id_counter);
378 tooltip_popup_id_counter += 1;
379 (tooltip_config, enclosing_component, popup_id, custom_children)
380 };
381
382 let tooltip_area = build_tooltip_area(
383 &popup_id,
384 &enclosing_component,
385 &tooltip_area_type,
386 tooltip_repeated,
387 );
388 let copy_binding = |property: &str| {
392 if let Some(binding) = tooltip_config.borrow().binding(property) {
393 tooltip_area.borrow_mut().set_binding(SmolStr::new(property), binding.clone());
394 }
395 };
396 if has_text_binding {
397 copy_binding(TEXT);
398 }
399
400 let tooltip_offset = NamedReference::new(&tooltip_area, SmolStr::new_static(OFFSET));
401 let pointer_x = NamedReference::new(&tooltip_area, SmolStr::new_static(MOUSE_X));
402 let pointer_y = NamedReference::new(&tooltip_area, SmolStr::new_static(MOUSE_Y));
403 let tooltip_text = (!has_custom_content)
404 .then(|| NamedReference::new(&tooltip_area, SmolStr::new_static(TEXT)));
405 let tooltip_content = build_tooltip_content(
406 &popup_id,
407 &enclosing_component,
408 tooltip_impl_type,
409 tooltip_text,
410 custom_children,
411 );
412 let popup_children = vec![tooltip_content.clone()];
413
414 let popup_window = Element {
415 id: popup_id,
416 base_type: popup_window_type.clone(),
417 enclosing_component: enclosing_component.clone(),
418 is_tooltip: true,
419 children: popup_children,
420 bindings: [(
421 SmolStr::new_static("close-policy"),
422 RefCell::new(
423 Expression::EnumerationValue(popup_close_policy_no_auto_close.clone()).into(),
424 ),
425 )]
426 .into_iter()
427 .collect(),
428 debug: tooltip_config.borrow().debug.clone(),
431 ..Default::default()
432 };
433 let popup_window_rc = popup_window.make_rc();
434 bind_popup_effective_size_from_content(&popup_window_rc, &tooltip_content);
435 wire_tooltip_placement(&popup_window_rc, pointer_x, pointer_y, tooltip_offset);
436
437 wire_tooltip_visibility_behavior(elem, tooltip_child_index, &tooltip_area, popup_window_rc);
438 });
439}
440
441pub async fn lower_tooltips(
442 doc: &Document,
443 type_loader: &mut crate::typeloader::TypeLoader,
444 diag: &mut BuildDiagnostics,
445) {
446 let mut has_tooltip = false;
447 doc.visit_all_used_components(|component| {
448 recurse_elem_including_sub_components_no_borrow(component, &(), &mut |elem, _| {
449 if matches!(&elem.borrow().builtin_type(), Some(b) if b.name == TOOLTIP_ELEMENT) {
450 has_tooltip = true;
451 }
452 })
453 });
454
455 if !has_tooltip {
456 return;
457 }
458
459 let mut import_diag = BuildDiagnostics::default();
460 let tooltip_component = type_loader
461 .import_component("std-widgets-impl.slint", TOOLTIP_IMPL_ELEMENT, &mut import_diag)
462 .await;
463 for diagnostic in import_diag {
464 diag.push_compiler_error(diagnostic);
465 }
466 let Some(tooltip_component) = tooltip_component else {
467 let generic_location = doc.node.as_ref().map(|n| n.to_source_location());
468 diag.push_error(
469 "`Tooltip` style implementation could not be loaded from std-widgets".into(),
470 &generic_location,
471 );
472 return;
473 };
474 let tooltip_style_type = ElementType::Component(tooltip_component);
475
476 doc.visit_all_used_components(|component| {
477 lower_tooltips_in_component(component, &doc.local_registry, &tooltip_style_type, diag);
478 });
479}