diff --git a/api/rs/slint/private_unstable_api.rs b/api/rs/slint/private_unstable_api.rs index cf081cd1068..7e9d79a964d 100644 --- a/api/rs/slint/private_unstable_api.rs +++ b/api/rs/slint/private_unstable_api.rs @@ -188,7 +188,7 @@ pub mod re_exports { }; pub use i_slint_core::item_tree::{ ItemTreeNode, ItemVisitorRefMut, ItemVisitorVTable, ItemWeak, TraversalOrder, - VisitChildrenResult, visit_item_tree, + VisitChildrenResult, compute_sorted_children_by_z, visit_item_tree, }; pub use i_slint_core::items::{Transform, *}; pub use i_slint_core::layout::*; diff --git a/internal/compiler/generator/cpp.rs b/internal/compiler/generator/cpp.rs index 47cae60bcc3..66e89789394 100644 --- a/internal/compiler/generator/cpp.rs +++ b/internal/compiler/generator/cpp.rs @@ -1594,6 +1594,12 @@ fn generate_item_tree( let mut item_tree_array: Vec = Default::default(); let mut item_array: Vec = Default::default(); + let mut z_sorted_nodes: Vec<( + usize, + Vec, + Vec, + )> = Vec::new(); + let mut current_node_index: usize = 0; sub_tree.tree.visit_in_array(&mut |node, children_offset, parent_index| { let parent_index = parent_index as u32; @@ -1647,6 +1653,15 @@ fn generate_item_tree( )); } } + + if let Some(ref z_prop) = node.z_sort_order_property { + z_sorted_nodes.push(( + current_node_index, + node.sub_component_path.clone(), + z_prop.clone(), + )); + } + current_node_index += 1; }); let mut visit_children_statements = vec![ @@ -1674,10 +1689,57 @@ fn generate_item_tree( visit_children_statements.extend([ "};".into(), - format!("auto self_rc = reinterpret_cast(component.instance)->self_weak.lock()->into_dyn();"), - "return slint::cbindgen_private::slint_visit_item_tree(&self_rc, get_item_tree(component) , index, order, visitor, dyn_visit);".to_owned(), + format!("auto self = reinterpret_cast(component.instance);"), + "auto self_rc = self->self_weak.lock()->into_dyn();".into(), ]); + if !z_sorted_nodes.is_empty() { + visit_children_statements.push("switch (index) {".into()); + for (node_idx, node_sub_component_path, child_z_refs) in &z_sorted_nodes { + let count = child_z_refs.len(); + let z_reads: Vec = child_z_refs + .iter() + .map(|z_source| match z_source { + llr::ZChildSource::Constant(val) => format!("{val:.1}f"), + llr::ZChildSource::Property(member_ref) => match member_ref { + llr::MemberReference::Relative { parent_level: 0, local_reference } => { + let full_path: Vec<_> = node_sub_component_path + .iter() + .chain(local_reference.sub_component_path.iter()) + .copied() + .collect(); + match &local_reference.reference { + llr::LocalMemberIndex::Property(property_index) => { + let (compo_path, sub_component) = + follow_sub_component_path(root, sub_tree.root, &full_path); + let property_name = + ident(&sub_component.properties[*property_index].name); + format!("self->{compo_path}{property_name}.get()") + } + _ => "0.0f".into(), + } + } + _ => "0.0f".into(), + }, + }) + .collect(); + let z_list = z_reads.join(", "); + visit_children_statements.push(format!("case {node_idx}: {{")); + visit_children_statements.push(format!(" float z_values[{count}] = {{{z_list}}};")); + visit_children_statements.push(" slint::SharedVector sorted;".into()); + visit_children_statements.push(format!(" slint::cbindgen_private::slint_compute_sorted_children_by_z(slint::cbindgen_private::Slice{{z_values, {count}}}, &sorted);")); + visit_children_statements.push(" return slint::cbindgen_private::slint_visit_item_tree_with_sorted_children(&self_rc, get_item_tree(component), index, order, visitor, dyn_visit, slint::cbindgen_private::Slice{sorted.begin(), sorted.size()});".into()); + visit_children_statements.push("}".into()); + } + visit_children_statements.push("default:".into()); + visit_children_statements.push(" return slint::cbindgen_private::slint_visit_item_tree(&self_rc, get_item_tree(component), index, order, visitor, dyn_visit);".into()); + visit_children_statements.push("}".into()); + } else { + visit_children_statements.push( + "return slint::cbindgen_private::slint_visit_item_tree(&self_rc, get_item_tree(component), index, order, visitor, dyn_visit);".into(), + ); + } + target_struct.members.push(( Access::Private, Declaration::Function(Function { diff --git a/internal/compiler/generator/rust.rs b/internal/compiler/generator/rust.rs index 244e68d9f83..6563f883683 100644 --- a/internal/compiler/generator/rust.rs +++ b/internal/compiler/generator/rust.rs @@ -1948,10 +1948,23 @@ fn generate_item_tree( })); let mut item_tree_array = Vec::new(); let mut item_array = Vec::new(); + let mut z_sorted_nodes: Vec<( + usize, + Vec, + Vec, + )> = Vec::new(); + let mut current_node_index: usize = 0; sub_tree.tree.visit_in_array(&mut |node, children_offset, parent_index| { let parent_index = parent_index as u32; + let node_idx = current_node_index; + current_node_index += 1; let (path, component) = follow_sub_component_path(root, sub_tree.root, &node.sub_component_path); + + if let Some(ref z_prop) = node.z_sort_order_property { + z_sorted_nodes.push((node_idx, node.sub_component_path.clone(), z_prop.clone())); + } + match node.item_index { Either::Right(mut repeater_index) => { assert_eq!(node.children.len(), 0); @@ -2046,6 +2059,69 @@ fn generate_item_tree( ) }; + // Generate visit_children_item body, potentially with z-sorted branches + let z_sorted_visit_body = if z_sorted_nodes.is_empty() { + quote! { + return sp::visit_item_tree(self, &sp::VRcMapped::origin(&self.as_ref().self_weak.get().unwrap().upgrade().unwrap()), self.get_item_tree().as_slice(), index, order, visitor, visit_dynamic, None); + } + } else { + // Generate match arms for z-sorted nodes + let mut z_match_arms = Vec::new(); + for (node_idx, node_sub_component_path, child_z_refs) in &z_sorted_nodes { + let idx_lit = *node_idx as isize; + let count = child_z_refs.len(); + let z_reads: Vec<_> = child_z_refs.iter().map(|z_source| { + match z_source { + llr::ZChildSource::Constant(val) => { + quote!(#val) + } + llr::ZChildSource::Property(member_ref) => { + match member_ref { + llr::MemberReference::Relative { parent_level: 0, local_reference } => { + let full_path: Vec<_> = node_sub_component_path.iter() + .chain(local_reference.sub_component_path.iter()) + .copied() + .collect(); + match &local_reference.reference { + llr::LocalMemberIndex::Property(property_index) => { + let (compo_path, sub_component) = follow_sub_component_path( + root, + sub_tree.root, + &full_path, + ); + let component_id = self::inner_component_id(sub_component); + let property_name = ident(&sub_component.properties[*property_index].name); + let property_field = + access_component_field_offset(&component_id, &property_name); + quote!((#compo_path #property_field).apply_pin(self).get() as f32) + } + _ => quote!(0.0f32), + } + } + _ => quote!(0.0f32), + } + } + } + }).collect(); + z_match_arms.push(quote! { + #idx_lit => { + let z_values: [f32; #count] = [#(#z_reads),*]; + let mut sorted = sp::SharedVector::default(); + sp::compute_sorted_children_by_z(&z_values, &mut sorted); + return sp::visit_item_tree(self, &sp::VRcMapped::origin(&self.as_ref().self_weak.get().unwrap().upgrade().unwrap()), self.get_item_tree().as_slice(), index, order, visitor, visit_dynamic, Some(sorted.as_slice())); + } + }); + } + quote! { + match index { + #(#z_match_arms)* + _ => { + return sp::visit_item_tree(self, &sp::VRcMapped::origin(&self.as_ref().self_weak.get().unwrap().upgrade().unwrap()), self.get_item_tree().as_slice(), index, order, visitor, visit_dynamic, None); + } + } + } + }; + quote!( #sub_comp @@ -2087,7 +2163,7 @@ fn generate_item_tree( fn visit_children_item(self: ::core::pin::Pin<&Self>, index: isize, order: sp::TraversalOrder, visitor: sp::ItemVisitorRefMut<'_>) -> sp::VisitChildrenResult { - return sp::visit_item_tree(self, &sp::VRcMapped::origin(&self.as_ref().self_weak.get().unwrap().upgrade().unwrap()), self.get_item_tree().as_slice(), index, order, visitor, visit_dynamic); + #z_sorted_visit_body #[allow(unused)] fn visit_dynamic(_self: ::core::pin::Pin<&#inner_component_id>, order: sp::TraversalOrder, visitor: sp::ItemVisitorRefMut<'_>, dyn_index: u32) -> sp::VisitChildrenResult { _self.visit_dynamic_children(dyn_index, order, visitor) diff --git a/internal/compiler/llr/item_tree.rs b/internal/compiler/llr/item_tree.rs index 95dc4178bbd..708a516926e 100644 --- a/internal/compiler/llr/item_tree.rs +++ b/internal/compiler/llr/item_tree.rs @@ -396,6 +396,19 @@ pub struct TreeNode { pub item_index: itertools::Either, pub children: Vec, pub is_accessible: bool, + /// If set, this node's children have dynamic z-ordering. + /// Each entry corresponds to a child (by index) and describes the source of its z value. + /// The code generator will read these z values at runtime and sort children accordingly. + pub z_sort_order_property: Option>, +} + +/// Source of a child's z value for dynamic z-ordering. +#[derive(Debug, Clone)] +pub enum ZChildSource { + /// Z value read from a property at runtime. + Property(super::MemberReference), + /// Z value known at compile time (e.g., constant z on a repeater child). + Constant(f32), } impl TreeNode { diff --git a/internal/compiler/llr/lower_to_item_tree.rs b/internal/compiler/llr/lower_to_item_tree.rs index 371b5af1f13..44c0ef5d0a2 100644 --- a/internal/compiler/llr/lower_to_item_tree.rs +++ b/internal/compiler/llr/lower_to_item_tree.rs @@ -1083,6 +1083,32 @@ fn make_tree( let e = element.borrow(); let children = e.children.iter().map(|c| make_tree(state, c, component, sub_component_path)); let repeater_count = component.mapping.repeater_count; + + // Check if this element has dynamic z-ordering. If so, build the z sources + // from each child's z_order field. + let z_sort_order_property = if e.has_dynamic_z_order { + use crate::object_tree::ZOrder; + let mut z_sources: Vec = Vec::with_capacity(e.children.len()); + for child in e.children.iter() { + let child_z = child.borrow().z_order.clone(); + match child_z { + Some(ZOrder::Constant(val)) => { + z_sources.push(ZChildSource::Constant(val)); + } + Some(ZOrder::Dynamic(ref nr)) => { + let member_ref = component.mapping.map_property_reference(nr, state); + z_sources.push(ZChildSource::Property(member_ref)); + } + None => { + z_sources.push(ZChildSource::Constant(0.0)); + } + } + } + Some(z_sources) + } else { + None + }; + match component.mapping.element_mapping.get(&ByAddress(element.clone())).unwrap() { LoweredElement::SubComponent { sub_component_index } => { let sub_component = e.sub_component().unwrap(); @@ -1099,6 +1125,7 @@ fn make_tree( ); tree_node.children.extend(children); tree_node.is_accessible |= !e.accessibility_props.0.is_empty(); + tree_node.z_sort_order_property = z_sort_order_property; tree_node } LoweredElement::NativeItem { item_index } => TreeNode { @@ -1106,18 +1133,21 @@ fn make_tree( sub_component_path: sub_component_path.into(), item_index: itertools::Either::Left(*item_index), children: children.collect(), + z_sort_order_property, }, LoweredElement::Repeated { repeated_index } => TreeNode { is_accessible: false, sub_component_path: sub_component_path.into(), item_index: itertools::Either::Right(usize::from(*repeated_index) as u32), children: Vec::new(), + z_sort_order_property: None, }, LoweredElement::ComponentPlaceholder { repeated_index } => TreeNode { is_accessible: false, sub_component_path: sub_component_path.into(), item_index: itertools::Either::Right(*repeated_index + repeater_count), children: Vec::new(), + z_sort_order_property: None, }, } } diff --git a/internal/compiler/llr/optim_passes/count_property_use.rs b/internal/compiler/llr/optim_passes/count_property_use.rs index 2889d714bb8..0a645460f34 100644 --- a/internal/compiler/llr/optim_passes/count_property_use.rs +++ b/internal/compiler/llr/optim_passes/count_property_use.rs @@ -152,6 +152,43 @@ pub fn count_property_use(root: &CompilationUnit) { visit_property(&p.activated, &ctx); } + fn visit_tree_z_properties(node: &crate::llr::TreeNode, ctx: &EvaluationContext) { + if let Some(z_props) = &node.z_sort_order_property { + for z_source in z_props { + if let crate::llr::ZChildSource::Property(member_ref) = z_source { + visit_property(member_ref, ctx); + } + } + } + for child in &node.children { + visit_tree_z_properties(child, ctx); + } + } + for c in &root.public_components { + let ctx = EvaluationContext::new_sub_component(root, c.item_tree.root, (), None); + visit_tree_z_properties(&c.item_tree.tree, &ctx); + } + root.for_each_sub_components(&mut |sc, ctx| { + for r in &sc.repeated { + let rep_ctx = EvaluationContext::new_sub_component(root, r.sub_tree.root, (), None); + visit_tree_z_properties(&r.sub_tree.tree, &rep_ctx); + } + for popup in &sc.popup_windows { + let parent_ctx = ParentScope::new(ctx, None); + let popup_ctx = EvaluationContext::new_sub_component( + root, + popup.item_tree.root, + (), + Some(&parent_ctx), + ); + visit_tree_z_properties(&popup.item_tree.tree, &popup_ctx); + } + }); + if let Some(p) = &root.popup_menu { + let ctx = EvaluationContext::new_sub_component(root, p.item_tree.root, (), None); + visit_tree_z_properties(&p.item_tree.tree, &ctx); + } + clean_unused_bindings(root); } diff --git a/internal/compiler/llr/optim_passes/remove_unused.rs b/internal/compiler/llr/optim_passes/remove_unused.rs index 86c6be3099a..2b7a7ba3695 100644 --- a/internal/compiler/llr/optim_passes/remove_unused.rs +++ b/internal/compiler/llr/optim_passes/remove_unused.rs @@ -277,6 +277,25 @@ mod visitor { for p in public_properties { visit_public_property(p, &scope, state, visitor); } + visit_tree_node_z_properties(&mut item_tree.tree, &scope, state, visitor); + } + + fn visit_tree_node_z_properties( + node: &mut crate::llr::TreeNode, + scope: &EvaluationScope, + state: &VisitorState, + visitor: &mut (impl Visitor + ?Sized), + ) { + if let Some(z_props) = &mut node.z_sort_order_property { + for z_source in z_props { + if let crate::llr::ZChildSource::Property(member_ref) = z_source { + visit_member_reference(member_ref, scope, state, visitor); + } + } + } + for child in &mut node.children { + visit_tree_node_z_properties(child, scope, state, visitor); + } } pub fn visit_sub_component( @@ -338,6 +357,8 @@ mod visitor { visitor.visit_property_idx(data_prop, &inner_scope, state); } + visit_tree_node_z_properties(&mut sub_tree.tree, &inner_scope, state, visitor); + if let Some(listview) = listview { visit_member_reference(&mut listview.viewport_y, &scope, state, visitor); visit_member_reference(&mut listview.viewport_height, &scope, state, visitor); @@ -353,6 +374,7 @@ mod visitor { for p in popup_windows { let popup_scope = EvaluationScope::SubComponent(p.item_tree.root, None); visit_expression(p.position.get_mut(), &popup_scope, state, visitor); + visit_tree_node_z_properties(&mut p.item_tree.tree, &popup_scope, state, visitor); } for t in timers { visit_expression(t.interval.get_mut(), &scope, state, visitor); @@ -474,6 +496,7 @@ mod visitor { visit_member_reference(activated, &scope, state, visitor); visit_member_reference(close, &scope, state, visitor); visit_member_reference(entries, &scope, state, visitor); + visit_tree_node_z_properties(&mut item_tree.tree, &scope, state, visitor); } pub fn visit_public_property( diff --git a/internal/compiler/object_tree.rs b/internal/compiler/object_tree.rs index 5498b3b76a9..1ec360bd937 100644 --- a/internal/compiler/object_tree.rs +++ b/internal/compiler/object_tree.rs @@ -723,6 +723,15 @@ pub struct GeometryProps { pub height: NamedReference, } +/// The z-order of a child element within a parent that has dynamic z-ordering. +#[derive(Clone, Debug)] +pub enum ZOrder { + /// z is a compile-time constant (used for repeater/conditional children). + Constant(f32), + /// z is bound to a runtime expression (NamedReference to the child's z property). + Dynamic(NamedReference), +} + impl GeometryProps { pub fn new(element: &ElementRc) -> Self { Self { @@ -812,6 +821,12 @@ pub struct Element { /// This element is a placeholder to embed an Component at pub is_component_placeholder: bool, + /// This element's children have dynamic z-ordering (z bound to non-constant expressions) + pub has_dynamic_z_order: bool, + /// Per-child z-order info. Set when parent has dynamic z-ordering. + /// Stored on the child so it remains consistent when the children vector is reordered. + pub z_order: Option, + pub states: Vec, pub transitions: Vec, @@ -2645,6 +2660,14 @@ pub fn visit_all_named_references_in_element( elem.borrow_mut().geometry_props = Some(geometry_props); } + let z_order = elem.borrow_mut().z_order.take(); + if let Some(mut zo) = z_order { + if let ZOrder::Dynamic(ref mut nr) = zo { + vis(nr); + } + elem.borrow_mut().z_order = Some(zo); + } + // visit two way bindings for expr in elem.borrow().bindings.values() { for twb in &mut expr.borrow_mut().two_way_bindings { diff --git a/internal/compiler/passes/inlining.rs b/internal/compiler/passes/inlining.rs index cd536ea7f15..6cae807dda3 100644 --- a/internal/compiler/passes/inlining.rs +++ b/internal/compiler/passes/inlining.rs @@ -401,6 +401,8 @@ fn duplicate_element_with_mapping( item_index_of_first_children: Default::default(), is_flickable_viewport: elem.is_flickable_viewport, has_popup_child: elem.has_popup_child, + has_dynamic_z_order: elem.has_dynamic_z_order, + z_order: elem.z_order.clone(), is_legacy_syntax: elem.is_legacy_syntax, inline_depth: elem.inline_depth + 1, // Deep-clone grid_layout_cell to avoid sharing between original and inlined copies. diff --git a/internal/compiler/passes/optimize_useless_rectangles.rs b/internal/compiler/passes/optimize_useless_rectangles.rs index 5664a10efb5..ac3f296b354 100644 --- a/internal/compiler/passes/optimize_useless_rectangles.rs +++ b/internal/compiler/passes/optimize_useless_rectangles.rs @@ -61,6 +61,11 @@ fn can_optimize(elem: &ElementRc) -> bool { _ => return false, }; + // Don't optimize if the element has a z binding (needed for dynamic z-ordering) + if e.bindings.contains_key("z") { + return false; + } + let analysis = e.property_analysis.borrow(); for coord in ["x", "y"] { if e.bindings.contains_key(coord) || analysis.get(coord).is_some_and(|a| a.is_set) { diff --git a/internal/compiler/passes/repeater_component.rs b/internal/compiler/passes/repeater_component.rs index f3307f19491..38668585ffd 100644 --- a/internal/compiler/passes/repeater_component.rs +++ b/internal/compiler/passes/repeater_component.rs @@ -59,6 +59,8 @@ fn create_repeater_components(component: &Rc) { geometry_props: original_elem.geometry_props.clone(), is_flickable_viewport: original_elem.is_flickable_viewport, has_popup_child: original_elem.has_popup_child, + has_dynamic_z_order: original_elem.has_dynamic_z_order, + z_order: original_elem.z_order.clone(), item_index: Default::default(), // Not determined yet item_index_of_first_children: Default::default(), is_legacy_syntax: original_elem.is_legacy_syntax, diff --git a/internal/compiler/passes/windows.rs b/internal/compiler/passes/windows.rs index 60c5926c974..4927d1e02af 100644 --- a/internal/compiler/passes/windows.rs +++ b/internal/compiler/passes/windows.rs @@ -58,6 +58,8 @@ pub fn ensure_window( accessibility_props: Default::default(), geometry_props: Default::default(), is_flickable_viewport: false, + has_dynamic_z_order: false, + z_order: None, item_index: Default::default(), item_index_of_first_children: Default::default(), grid_layout_cell: None, diff --git a/internal/compiler/passes/z_order.rs b/internal/compiler/passes/z_order.rs index 4852bd0a707..2ef7c7dbef4 100644 --- a/internal/compiler/passes/z_order.rs +++ b/internal/compiler/passes/z_order.rs @@ -1,13 +1,15 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -/*! re-order the children by their z-order +/*! re-order the children by their z-order (static case) or mark elements + for dynamic z-order sorting (when z is bound to a non-constant expression). */ +use std::cell::RefCell; use std::rc::Rc; -use crate::diagnostics::BuildDiagnostics; -use crate::expression_tree::{Expression, Unit}; +use crate::diagnostics::{BuildDiagnostics, Spanned}; +use crate::expression_tree::{BindingExpression, Expression, Unit}; use crate::langtype::ElementType; use crate::object_tree::{Component, ElementRc}; @@ -22,10 +24,38 @@ pub fn reorder_by_z_order(root_component: &Rc, diag: &mut BuildDiagno } fn reorder_children_by_zorder( - elem: &Rc>, + elem: &Rc>, diag: &mut BuildDiagnostics, ) { - // maps indexes to their z order + if elem.borrow().children.is_empty() { + return; + } + + let mut has_any_z = false; + let mut has_dynamic_z = false; + + for child_elm in elem.borrow().children.iter() { + if has_z_binding(child_elm) { + has_any_z = true; + if get_z_expr(child_elm).is_none() { + has_dynamic_z = true; + } + } + } + + if !has_any_z { + return; + } + + if has_dynamic_z { + setup_dynamic_z_order(elem, diag); + } else { + reorder_static_z(elem, diag); + } +} + +/// Static z-order: evaluate all z values at compile time and reorder children. +fn reorder_static_z(elem: &Rc>, diag: &mut BuildDiagnostics) { let mut children_z_order = Vec::new(); for (idx, child_elm) in elem.borrow().children.iter().enumerate() { let z = child_elm @@ -68,20 +98,111 @@ fn reorder_children_by_zorder( } } +/// Dynamic z-order: mark the parent element and ensure all children have z bindings. +/// Non-repeater children get NamedReferences (materialized as runtime properties). +/// Repeater/conditional children get their z evaluated at compile time if constant, +/// or default to z=0. +fn setup_dynamic_z_order( + elem: &Rc>, + diag: &mut BuildDiagnostics, +) { + use crate::namedreference::NamedReference; + use crate::object_tree::ZOrder; + + elem.borrow_mut().has_dynamic_z_order = true; + + for child_elm in elem.borrow().children.iter() { + if child_elm.borrow().repeated.is_some() { + // Repeater/conditional child: z lives in the inner component. + // Must be a compile-time constant; per-item dynamic z in repeaters + // is not yet supported. + let z_val = if let ElementType::Component(c) = &child_elm.borrow().base_type { + let binding = c.root_element.borrow_mut().bindings.remove("z"); + if let Some(e) = binding { + if let Some(val) = try_eval_const_expr(&e.borrow().expression) { + val + } else { + diag.push_error( + "'z' in a repeated element must be a constant".into(), + &*e.borrow(), + ); + 0. + } + } else { + 0. + } + } else { + 0. + }; + child_elm.borrow_mut().z_order = Some(ZOrder::Constant(z_val as f32)); + } else { + // Non-repeater child: create NamedReference for runtime access. + if !child_elm.borrow().bindings.contains_key("z") { + let span = child_elm.borrow().to_source_location(); + child_elm.borrow_mut().bindings.insert( + smol_str::SmolStr::new_static("z"), + BindingExpression::new_with_span( + Expression::NumberLiteral(0., Unit::None), + span, + ) + .into(), + ); + } + let nr = NamedReference::new(child_elm, smol_str::SmolStr::new_static("z")); + child_elm.borrow_mut().z_order = Some(ZOrder::Dynamic(nr)); + } + } +} + +/// Try to evaluate the z binding expression for a child element, checking both +/// direct bindings and repeated component root elements. +fn get_z_expr(child_elm: &ElementRc) -> Option { + let child = child_elm.borrow(); + if let Some(b) = child.bindings.get("z") { + return try_eval_const_expr(&b.borrow().expression); + } + if child.repeated.is_some() + && let ElementType::Component(c) = &child.base_type + && let Some(b) = c.root_element.borrow().bindings.get("z") + { + return try_eval_const_expr(&b.borrow().expression); + } + None +} + +/// Check whether a child element has a z binding at all. +fn has_z_binding(child_elm: &ElementRc) -> bool { + let child = child_elm.borrow(); + if child.bindings.contains_key("z") { + return true; + } + if child.repeated.is_some() + && let ElementType::Component(c) = &child.base_type + { + return c.root_element.borrow().bindings.contains_key("z"); + } + false +} + +fn try_eval_const_expr(expression: &Expression) -> Option { + match super::ignore_debug_hooks(expression) { + Expression::NumberLiteral(v, Unit::None) => Some(*v), + Expression::Cast { from, .. } => try_eval_const_expr(from), + Expression::UnaryOp { sub, op: '-' } => try_eval_const_expr(sub).map(|v| -v), + Expression::UnaryOp { sub, op: '+' } => try_eval_const_expr(sub), + _ => None, + } +} + fn eval_const_expr( expression: &Expression, name: &str, span: &dyn crate::diagnostics::Spanned, diag: &mut BuildDiagnostics, ) -> Option { - match super::ignore_debug_hooks(expression) { - Expression::NumberLiteral(v, Unit::None) => Some(*v), - Expression::Cast { from, .. } => eval_const_expr(from, name, span, diag), - Expression::UnaryOp { sub, op: '-' } => eval_const_expr(sub, name, span, diag).map(|v| -v), - Expression::UnaryOp { sub, op: '+' } => eval_const_expr(sub, name, span, diag), - _ => { - diag.push_error(format!("'{name}' must be an number literal"), span); - None - } + let result = try_eval_const_expr(expression); + if result.is_none() { + diag.push_error(format!("'{name}' must be an number literal"), span); } + result } diff --git a/internal/compiler/tests/syntax/elements/z_repeater_dynamic.slint b/internal/compiler/tests/syntax/elements/z_repeater_dynamic.slint new file mode 100644 index 00000000000..0b5181618b0 --- /dev/null +++ b/internal/compiler/tests/syntax/elements/z_repeater_dynamic.slint @@ -0,0 +1,14 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Dynamic z in a repeater is not supported. + +export component Foo inherits Rectangle { + Rectangle { + Rectangle { z: root.width > 10px ? 5 : 0; } + for dyn_z in [5, 40, 23] : Rectangle { + z: dyn_z; +// > ( /// FIXME: the design of this use lots of indirection and stack frame in recursive functions /// Need to check if the compiler is able to optimize away some of it. /// Possibly we should generate code that directly call the visitor instead +/// +/// If `sorted_children_offsets` is `Some`, it provides a pre-computed z-sorted order +/// for children (offsets relative to children_index) instead of the sequential order. pub fn visit_item_tree( base: Pin<&Base>, item_tree: &ItemTreeRc, @@ -1358,6 +1361,7 @@ pub fn visit_item_tree( vtable::VRefMut, u32, ) -> VisitChildrenResult, + sorted_children_offsets: Option<&[u32]>, ) -> VisitChildrenResult { let mut visit_at_index = |idx: u32| -> VisitChildrenResult { match &item_tree_array[idx as usize] { @@ -1381,11 +1385,18 @@ pub fn visit_item_tree( } else { match &item_tree_array[index as usize] { ItemTreeNode::Item { children_index, children_count, .. } => { - for c in 0..*children_count { - let idx = match order { - TraversalOrder::BackToFront => *children_index + c, - TraversalOrder::FrontToBack => *children_index + *children_count - c - 1, + if let Some(sorted) = sorted_children_offsets { + debug_assert_eq!(sorted.len(), *children_count as usize); + } + let count = *children_count as usize; + for i in 0..count { + let offset_idx = match order { + TraversalOrder::BackToFront => i, + TraversalOrder::FrontToBack => count - 1 - i, }; + let idx = *children_index + + sorted_children_offsets + .map_or(offset_idx as u32, |sorted| sorted[offset_idx]); let maybe_abort_index = visit_at_index(idx); if maybe_abort_index.has_aborted() { return maybe_abort_index; @@ -1398,6 +1409,21 @@ pub fn visit_item_tree( } } +/// Compute a sorted list of child indices based on their z values. +/// Writes into `out` which will contain child offsets (relative to children_index) +/// sorted by the corresponding z value (stable sort). +pub fn compute_sorted_children_by_z(z_values: &[f32], out: &mut crate::SharedVector) { + out.resize(z_values.len(), 0); + for (i, slot) in out.make_mut_slice().iter_mut().enumerate() { + *slot = i as u32; + } + out.make_mut_slice().sort_by(|&a, &b| { + z_values[a as usize] + .partial_cmp(&z_values[b as usize]) + .unwrap_or(core::cmp::Ordering::Equal) + }); +} + #[cfg(feature = "ffi")] pub(crate) mod ffi { #![allow(unsafe_code)] @@ -1460,8 +1486,44 @@ pub(crate) mod ffi { order, visitor, |a, b, c, d| visit_dynamic(a.get_ref() as *const vtable::Dyn as *const c_void, b, c, d), + None, ) } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn slint_visit_item_tree_with_sorted_children( + item_tree: &ItemTreeRc, + item_tree_array: Slice, + index: isize, + order: TraversalOrder, + visitor: VRefMut, + visit_dynamic: extern "C" fn( + base: *const c_void, + order: TraversalOrder, + visitor: vtable::VRefMut, + dyn_index: u32, + ) -> VisitChildrenResult, + sorted_children_offsets: Slice, + ) -> VisitChildrenResult { + crate::item_tree::visit_item_tree( + VRc::as_pin_ref(item_tree), + item_tree, + item_tree_array.as_slice(), + index, + order, + visitor, + |a, b, c, d| visit_dynamic(a.get_ref() as *const vtable::Dyn as *const c_void, b, c, d), + Some(sorted_children_offsets.as_slice()), + ) + } + + #[unsafe(no_mangle)] + pub unsafe extern "C" fn slint_compute_sorted_children_by_z( + z_values: Slice, + out: &mut crate::SharedVector, + ) { + crate::item_tree::compute_sorted_children_by_z(z_values.as_slice(), out); + } } #[cfg(test)] diff --git a/internal/interpreter/dynamic_item_tree.rs b/internal/interpreter/dynamic_item_tree.rs index e34675ae0f5..44ac40bfba6 100644 --- a/internal/interpreter/dynamic_item_tree.rs +++ b/internal/interpreter/dynamic_item_tree.rs @@ -442,6 +442,15 @@ impl<'id> From>> for ErasedItemTreeDescription { /// ItemTreeDescription is a representation of a ItemTree suitable for interpretation /// +/// Z info for a single child in a dynamically z-ordered parent. +#[derive(Clone)] +enum ZInterpreterChildInfo { + /// Z from a runtime property. Stores the property name on the root element. + Property(SmolStr), + /// Z known at compile time (repeater/conditional children). + Constant(f32), +} + /// It contains information about how to create and destroy the Component. /// Its first member is the ItemTreeVTable for generated instance, since it is a `#[repr(C)]` /// structure, it is valid to cast a pointer to the ItemTreeVTable back to a @@ -485,6 +494,9 @@ pub struct ItemTreeDescription<'id> { /// The collection of compiled globals compiled_globals: Option>, + /// For tree nodes with dynamic z-ordering: maps tree_index to per-child z info. + z_order_info: HashMap>, + /// The type loader, which will be available only on the top-most `ItemTreeDescription`. /// All other `ItemTreeDescription`s have `None` here. #[cfg(feature = "internal-highlight")] @@ -808,6 +820,62 @@ extern "C" fn visit_children_item( generativity::make_guard!(guard); let instance_ref = unsafe { InstanceRef::from_pin_ref(component, guard) }; let comp_rc = instance_ref.self_weak().get().unwrap().upgrade().unwrap(); + + // Macro needed because `visit_item_tree*` requires the closure to be generic over + // the generativity lifetime of Instance<'_>, which a regular closure cannot satisfy. + macro_rules! visit_dyn { + () => { + |_, order, visitor, index: u32| { + if (index as usize) >= instance_ref.description.repeater.len() { + VisitChildrenResult::CONTINUE + } else { + let rep_in_comp = + unsafe { instance_ref.description.repeater[index as usize].get_untagged() }; + ensure_repeater_updated(instance_ref, rep_in_comp); + let repeater = rep_in_comp.offset.apply_pin(instance_ref.instance); + repeater.visit(order, visitor) + } + } + }; + } + + // Check if this node has dynamic z-ordering + if index >= 0 + && let Some(z_children) = instance_ref.description.z_order_info.get(&(index as u32)) + { + let z_values: Vec = z_children + .iter() + .map(|child_info| match child_info { + ZInterpreterChildInfo::Constant(val) => *val, + ZInterpreterChildInfo::Property(prop_name) => instance_ref + .description + .custom_properties + .get(prop_name) + .and_then(|p| unsafe { + p.prop + .get(core::pin::Pin::new_unchecked( + &*(instance_ref.as_ptr().add(p.offset)), + )) + .ok() + }) + .and_then(|v| if let Value::Number(n) = v { Some(n as f32) } else { None }) + .unwrap_or(0.0), + }) + .collect(); + let mut sorted = i_slint_core::SharedVector::default(); + i_slint_core::item_tree::compute_sorted_children_by_z(&z_values, &mut sorted); + return i_slint_core::item_tree::visit_item_tree( + instance_ref.instance, + &vtable::VRc::into_dyn(comp_rc), + get_item_tree(component).as_slice(), + index, + order, + v, + visit_dyn!(), + Some(sorted.as_slice()), + ); + } + i_slint_core::item_tree::visit_item_tree( instance_ref.instance, &vtable::VRc::into_dyn(comp_rc), @@ -815,20 +883,8 @@ extern "C" fn visit_children_item( index, order, v, - |_, order, visitor, index| { - if index as usize >= instance_ref.description.repeater.len() { - // Do nothing: We are ComponentContainer and Our parent already did all the work! - VisitChildrenResult::CONTINUE - } else { - // `ensure_updated` needs a 'static lifetime so we must call get_untagged. - // Safety: we do not mix the component with other component id in this function - let rep_in_comp = - unsafe { instance_ref.description.repeater[index as usize].get_untagged() }; - ensure_repeater_updated(instance_ref, rep_in_comp); - let repeater = rep_in_comp.offset.apply_pin(instance_ref.instance); - repeater.visit(order, visitor) - } - }, + visit_dyn!(), + None, ) } @@ -1445,6 +1501,28 @@ pub(crate) fn generate_item_tree<'id>( drop_in_place, dealloc, }; + // Build z_order_info from original elements + let mut z_order_info: HashMap> = HashMap::new(); + for (idx, elem_rc) in builder.original_elements.iter().enumerate() { + let elem = elem_rc.borrow(); + if elem.has_dynamic_z_order { + let child_infos: Vec<_> = elem + .children + .iter() + .map(|child| match &child.borrow().z_order { + Some(i_slint_compiler::object_tree::ZOrder::Constant(val)) => { + ZInterpreterChildInfo::Constant(*val) + } + Some(i_slint_compiler::object_tree::ZOrder::Dynamic(nr)) => { + ZInterpreterChildInfo::Property(nr.name().clone()) + } + None => ZInterpreterChildInfo::Constant(0.0), + }) + .collect(); + z_order_info.insert(idx as u32, child_infos); + } + } + let t = ItemTreeDescription { ct: t, dynamic_type: builder.type_builder.build(), @@ -1466,6 +1544,7 @@ pub(crate) fn generate_item_tree<'id>( timers, popup_ids: std::cell::RefCell::new(HashMap::new()), popup_menu_description: builder.popup_menu_description, + z_order_info, #[cfg(feature = "internal-highlight")] type_loader: std::cell::OnceCell::new(), #[cfg(feature = "internal-highlight")] diff --git a/tests/cases/children/zorder_dynamic.slint b/tests/cases/children/zorder_dynamic.slint new file mode 100644 index 00000000000..792c11dc077 --- /dev/null +++ b/tests/cases/children/zorder_dynamic.slint @@ -0,0 +1,195 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Test dynamic z-ordering: z bound to expressions that change at runtime. +// Tests: property-bound z, multiple children with mixed static/dynamic z, +// and z values changing dynamically to reorder click targets. + +export component TestCase inherits Rectangle { + width: 300phx; + height: 100phx; + + in-out property clicked-value; + in-out property front: false; + in-out property top-layer: 0; + + // --- Section 1: Simple dynamic z swap (x: 0..100) --- + Rectangle { + x: 0phx; width: 100phx; height: 100phx; + + TouchArea { + width: 100%; height: 100%; + z: front ? 0 : 10; + clicked => { clicked-value = 1; } + } + TouchArea { + width: 100%; height: 100%; + z: front ? 10 : 0; + clicked => { clicked-value = 2; } + } + } + + // --- Section 2: Multiple layers with one dynamic top (x: 100..200) --- + // Four overlapping layers. top-layer selects which is on top (z=100). + // Others have z=0 so the last one in source order wins ties. + Rectangle { + x: 100phx; width: 100phx; height: 100phx; + + TouchArea { + width: 100%; height: 100%; + z: top-layer == 0 ? 100 : 0; + clicked => { clicked-value = 10; } + } + TouchArea { + width: 100%; height: 100%; + z: top-layer == 1 ? 100 : 0; + clicked => { clicked-value = 11; } + } + TouchArea { + width: 100%; height: 100%; + z: top-layer == 2 ? 100 : 0; + clicked => { clicked-value = 12; } + } + TouchArea { + width: 100%; height: 100%; + z: top-layer == 3 ? 100 : 0; + clicked => { clicked-value = 13; } + } + } + + // --- Section 3: Dynamic z mixed with static z children (x: 200..300) --- + // A static z=5 element and a dynamic z element that toggles above/below. + Rectangle { + x: 200phx; width: 100phx; height: 100phx; + + TouchArea { + width: 100%; height: 100%; + z: 5; + clicked => { clicked-value = 20; } + } + TouchArea { + width: 100%; height: 100%; + z: front ? 10 : 1; + clicked => { clicked-value = 21; } + } + } +} + +/* +```rust +let instance = TestCase::new().unwrap(); + +// Section 1: front=false, first area has z=10 (on top) +slint_testing::send_mouse_click(&instance, 50., 50.); +assert_eq!(instance.get_clicked_value(), 1); + +// Flip: second area now z=10 +instance.set_front(true); +slint_testing::send_mouse_click(&instance, 50., 50.); +assert_eq!(instance.get_clicked_value(), 2); + +// Section 2: top_layer=0, layer 0 is on top +instance.set_top_layer(0); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq!(instance.get_clicked_value(), 10); + +// top_layer=2, layer 2 on top +instance.set_top_layer(2); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq!(instance.get_clicked_value(), 12); + +// top_layer=3, layer 3 on top +instance.set_top_layer(3); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq!(instance.get_clicked_value(), 13); + +// top_layer=1, layer 1 on top +instance.set_top_layer(1); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq!(instance.get_clicked_value(), 11); + +// Section 3: front=true, dynamic element z=10 > static z=5 +instance.set_front(true); +slint_testing::send_mouse_click(&instance, 250., 50.); +assert_eq!(instance.get_clicked_value(), 21); + +// front=false, dynamic element z=1 < static z=5 +instance.set_front(false); +slint_testing::send_mouse_click(&instance, 250., 50.); +assert_eq!(instance.get_clicked_value(), 20); +``` + +```cpp +auto handle = TestCase::create(); +const TestCase &instance = *handle; + +slint_testing::send_mouse_click(&instance, 50., 50.); +assert_eq(instance.get_clicked_value(), 1); + +instance.set_front(true); +slint_testing::send_mouse_click(&instance, 50., 50.); +assert_eq(instance.get_clicked_value(), 2); + +instance.set_top_layer(0); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq(instance.get_clicked_value(), 10); + +instance.set_top_layer(2); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq(instance.get_clicked_value(), 12); + +instance.set_top_layer(3); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq(instance.get_clicked_value(), 13); + +instance.set_top_layer(1); +slint_testing::send_mouse_click(&instance, 150., 50.); +assert_eq(instance.get_clicked_value(), 11); + +instance.set_front(true); +slint_testing::send_mouse_click(&instance, 250., 50.); +assert_eq(instance.get_clicked_value(), 21); + +instance.set_front(false); +slint_testing::send_mouse_click(&instance, 250., 50.); +assert_eq(instance.get_clicked_value(), 20); +``` + +```js +var instance = new slint.TestCase(); + +// Section 1 +slintlib.private_api.send_mouse_click(instance, 50., 50.); +assert.equal(instance.clicked_value, 1); + +instance.front = true; +slintlib.private_api.send_mouse_click(instance, 50., 50.); +assert.equal(instance.clicked_value, 2); + +// Section 2 +instance.top_layer = 0; +slintlib.private_api.send_mouse_click(instance, 150., 50.); +assert.equal(instance.clicked_value, 10); + +instance.top_layer = 2; +slintlib.private_api.send_mouse_click(instance, 150., 50.); +assert.equal(instance.clicked_value, 12); + +instance.top_layer = 3; +slintlib.private_api.send_mouse_click(instance, 150., 50.); +assert.equal(instance.clicked_value, 13); + +instance.top_layer = 1; +slintlib.private_api.send_mouse_click(instance, 150., 50.); +assert.equal(instance.clicked_value, 11); + +// Section 3 +instance.front = true; +slintlib.private_api.send_mouse_click(instance, 250., 50.); +assert.equal(instance.clicked_value, 21); + +instance.front = false; +slintlib.private_api.send_mouse_click(instance, 250., 50.); +assert.equal(instance.clicked_value, 20); +``` +*/ diff --git a/tests/screenshots/cases/basic/zorder_dynamic.slint b/tests/screenshots/cases/basic/zorder_dynamic.slint new file mode 100644 index 00000000000..a69edf845dc --- /dev/null +++ b/tests/screenshots/cases/basic/zorder_dynamic.slint @@ -0,0 +1,76 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Screenshot test for dynamic z-ordering. +// Several overlapping colored rectangles with z values assigned in init, +// mixed with if-conditional and for-repeater elements. + +export component TestCase inherits Window { + width: 64px; + height: 64px; + background: black; + + property show-green: true; + property z-red: 0; + property z-blue: 0; + + init => { + z-red = 1; + z-blue = 30; + } + + // Bottom-left: overlapping rects with z from init. + // Blue (z=30) covers red (z=1). + Rectangle { + x: 0px; y: 32px; width: 32px; height: 32px; + + Rectangle { + x: 2px; y: 2px; width: 20px; height: 20px; + background: red; + z: z-red; + } + Rectangle { + x: 8px; y: 8px; width: 20px; height: 20px; + background: blue; + z: z-blue; + } + } + + // Top-left: if-conditional green square on top of yellow. + Rectangle { + x: 0px; y: 0px; width: 32px; height: 32px; + + Rectangle { + x: 2px; y: 2px; width: 28px; height: 28px; + background: yellow; + z: 1; + } + if show-green: Rectangle { + x: 6px; y: 6px; width: 20px; height: 20px; + background: green; + z: 10; + } + } + + // Right half: for-repeater staircase with constant z. + // Green (z=40) on top, then blue (z=23), then red (z=5). + Rectangle { + x: 32px; y: 0px; width: 32px; height: 64px; + + Rectangle { + x: 0px; y: 5px; width: 24px; height: 24px; + background: red; + z: z-red; + } + Rectangle { + x: 4px; y: 15px; width: 24px; height: 24px; + background: green; + z: z-blue; + } + Rectangle { + x: 8px; y: 25px; width: 24px; height: 24px; + background: blue; + z: 15; + } + } +} diff --git a/tests/screenshots/references/skia/basic/zorder_dynamic.png b/tests/screenshots/references/skia/basic/zorder_dynamic.png new file mode 100644 index 00000000000..cbbfd666572 Binary files /dev/null and b/tests/screenshots/references/skia/basic/zorder_dynamic.png differ diff --git a/tests/screenshots/references/software/basic/zorder_dynamic.png b/tests/screenshots/references/software/basic/zorder_dynamic.png new file mode 100644 index 00000000000..cbbfd666572 Binary files /dev/null and b/tests/screenshots/references/software/basic/zorder_dynamic.png differ