zng_wgt_rule_line/
collapse.rs

1use std::sync::Arc;
2
3use zng_app::{
4    update::LayoutUpdates,
5    widget::info::{TreeFilter, iter::TreeIterator},
6};
7use zng_wgt::prelude::*;
8
9/// Collapse adjacent descendant rule lines.
10///
11/// Set this in a panel widget to automatically collapse rule lines that would appear repeated or dangling on screen.
12#[property(LAYOUT - 100)]
13pub fn collapse_scope(child: impl IntoUiNode, mode: impl IntoVar<CollapseMode>) -> UiNode {
14    let mode = mode.into_var();
15    let mut scope: Option<Arc<CollapseScope>> = None;
16    match_node(child, move |c, op| match op {
17        UiNodeOp::Init => {
18            WIDGET.sub_var_layout(&mode);
19            scope = Some(Arc::new(CollapseScope::new(WIDGET.id())));
20        }
21        UiNodeOp::Deinit => {
22            scope = None;
23        }
24        UiNodeOp::Measure { wm, desired_size } => {
25            *desired_size = SCOPE.with_context(&mut scope, || c.measure(wm));
26        }
27        UiNodeOp::Layout { wl, final_size } => {
28            *final_size = SCOPE.with_context(&mut scope, || c.layout(wl));
29
30            // update collapsed list
31            let mode = mode.get();
32
33            // try to reuse the current scope
34            let maybe_exclusive = Arc::get_mut(scope.as_mut().unwrap());
35            // if not possible alloc new (this can happen if a child captured context and is keeping it)
36            let is_new = maybe_exclusive.is_none();
37            let mut new = CollapseScope::new(WIDGET.id());
38            let s = maybe_exclusive.unwrap_or(&mut new);
39
40            // tracks changes in reused set, ignore this if is_new
41            let mut changes = UpdateDeliveryList::new_any();
42            if mode.is_empty() {
43                if !s.collapse.is_empty() {
44                    let info = WIDGET.info();
45                    let info = info.tree();
46                    for id in s.collapse.drain() {
47                        if let Some(wgt) = info.get(id) {
48                            changes.insert_wgt(&wgt);
49                        }
50                    }
51                }
52            } else {
53                // does one pass of the info tree descendants collecting collapsable lines,
54                // it is a bit complex to avoid allocating the `IdMap` for most widgets,
55                // flags `changed` if a second layout pass is needed
56
57                let info = WIDGET.info();
58                macro_rules! filter {
59                    ($iter:expr) => {
60                        $iter.tree_filter(|w| {
61                            if w.meta().flagged(*COLLAPSE_SKIP_ID) {
62                                TreeFilter::SkipAll
63                            } else {
64                                TreeFilter::Include
65                            }
66                        })
67                    };
68                }
69
70                let mut trim_start = mode.contains(CollapseMode::TRIM_START);
71                let mut trim_end_id = None;
72                if mode.contains(CollapseMode::TRIM_END) {
73                    // find trim_end start *i* first, so that we can update `s.collapse` in a single pass
74                    for wgt in filter!(info.descendants().tree_rev()) {
75                        if wgt.meta().flagged(*COLLAPSABLE_LINE_ID) {
76                            trim_end_id = Some(wgt.id());
77                        } else if wgt.descendants_len() == 0 && !wgt.bounds_info().inner_size().is_empty() {
78                            // only consider leafs that are not collapsed
79                            break;
80                        }
81                    }
82                }
83                let mut trim_end = false;
84                let mut merge = false;
85                for wgt in filter!(info.descendants()) {
86                    if wgt.meta().flagged(*COLLAPSABLE_LINE_ID) {
87                        if let Some(id) = trim_end_id
88                            && id == wgt.id()
89                        {
90                            trim_end_id = None;
91                            trim_end = true;
92                        }
93                        let changed = if trim_start || merge || trim_end {
94                            s.collapse.insert(wgt.id())
95                        } else {
96                            merge = mode.contains(CollapseMode::MERGE);
97                            s.collapse.remove(&wgt.id())
98                        };
99                        if changed && !is_new {
100                            changes.insert_wgt(&wgt);
101                        }
102                    } else if wgt.descendants_len() == 0 && !wgt.bounds_info().inner_size().is_empty() {
103                        // only consider leafs that are not collapsed
104                        trim_start = false;
105                        merge = false;
106                    }
107                }
108            }
109            if is_new {
110                let s = scope.as_mut().unwrap();
111                // previous changed state set assuming it was reusing set, override it
112                let info = WIDGET.info();
113                let info = info.tree();
114                for id in s.collapse.symmetric_difference(&new.collapse) {
115                    if let Some(wgt) = info.get(*id) {
116                        changes.insert_wgt(&wgt);
117                    }
118                }
119                if !changes.widgets().is_empty() {
120                    scope = Some(Arc::new(new));
121                }
122            }
123
124            if !changes.widgets().is_empty() {
125                *final_size = wl.with_layout_updates(Arc::new(LayoutUpdates::new(changes)), |wl| {
126                    SCOPE.with_context(&mut scope, || c.layout(wl))
127                });
128            }
129        }
130        _ => {}
131    })
132}
133
134/// Defines if this widget and descendants are ignored by [`collapse_scope`].
135///
136/// If `true` the widget subtree is skipped, as if not present.
137///
138/// [`collapse_scope`]: fn@collapse_scope
139#[property(CONTEXT, default(false))]
140pub fn collapse_skip(child: impl IntoUiNode, skip: impl IntoVar<bool>) -> UiNode {
141    let skip = skip.into_var();
142    match_node(child, move |_, op| match op {
143        UiNodeOp::Init => {
144            WIDGET.sub_var_info(&skip);
145        }
146        UiNodeOp::Info { info } => {
147            if skip.get() {
148                info.flag_meta(*COLLAPSE_SKIP_ID);
149            }
150        }
151        _ => {}
152    })
153}
154
155bitflags::bitflags! {
156    /// Represents what rule lines are collapsed in a [`collapse_scope`].
157    ///
158    /// [`collapse_scope`]: fn@collapse_scope
159    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
160    pub struct CollapseMode: u8 {
161        /// Collapse first line(s) if it has no previous visible sibling in the scope.
162        const TRIM_START = 0b0000_0001;
163        /// Collapse the last line(s) if it has no next visible sibling in the scope.
164        const TRIM_END = 0b0000_0010;
165        /// Collapse start and end.
166        const TRIM = Self::TRIM_START.bits() | Self::TRIM_END.bits();
167
168        /// Adjacent lines without intermediary visible siblings are collapsed except the first in sequence.
169        const MERGE = 0b0001_0000;
170    }
171}
172impl_from_and_into_var! {
173    fn from(all: bool) -> CollapseMode {
174        if all { CollapseMode::all() } else { CollapseMode::empty() }
175    }
176}
177
178/// Contextual service managed by [`collapse_scope`].
179///
180/// Custom line widgets not derived from [`RuleLine!`] can participate in [`collapse_scope`] by setting [`COLLAPSABLE_LINE_ID`]
181/// during info build and using this service during measure and layout.
182///
183/// [`collapse_scope`]: fn@collapse_scope
184/// [`RuleLine!`]: struct@crate::RuleLine
185#[allow(non_camel_case_types)]
186pub struct COLLAPSE_SCOPE;
187impl COLLAPSE_SCOPE {
188    /// Get the parent scope ID.
189    pub fn scope_id(&self) -> Option<WidgetId> {
190        SCOPE.get().scope_id
191    }
192
193    ///Gets if the line widget needs to collapse
194    pub fn collapse(&self, line_id: WidgetId) -> bool {
195        let scope = SCOPE.get();
196        scope.collapse.contains(&line_id)
197    }
198}
199
200static_id! {
201    /// Identifies a line widget that can be collapsed by [`collapse_scope`].
202    ///
203    /// [`collapse_scope`]: fn@collapse_scope
204    pub static ref COLLAPSABLE_LINE_ID: StateId<()>;
205
206    /// Identifies a widget (and descendants) to be ignored by the [`collapse_scope`].
207    ///
208    /// [`collapse_scope`]: fn@collapse_scope
209    pub static ref COLLAPSE_SKIP_ID: StateId<()>;
210}
211
212context_local! {
213    static SCOPE: CollapseScope = CollapseScope {
214        collapse: IdSet::new(),
215        scope_id: None,
216    };
217}
218
219struct CollapseScope {
220    collapse: IdSet<WidgetId>,
221    scope_id: Option<WidgetId>,
222}
223
224impl CollapseScope {
225    fn new(scope_id: WidgetId) -> Self {
226        Self {
227            collapse: IdSet::new(),
228            scope_id: Some(scope_id),
229        }
230    }
231}