Skip to main content

ferroui_controls/
panel.rs

1use crate::{Border, Control, ControlImpl, Controls};
2use ferroui_base::collections::{NotifyCollectionChangedAction, NotifyCollectionChangedEventArgs};
3use ferroui_base::input::InputElementImpl;
4use ferroui_base::interactivity::InteractiveImpl;
5use ferroui_base::layout::LayoutableImpl;
6use ferroui_base::logical_tree::{ChildIndexChangedEventArgs, IChildIndexProvider};
7use ferroui_base::media::{DrawingContext, IBrush};
8use ferroui_base::reactive::{Disposable, IDisposable};
9use ferroui_base::utilities::HandlerList;
10use ferroui_base::{
11    ferro_class, ferro_impl_classes, ferro_property, instantiate, FerroObjectImpl, FerroObjectImplExt,
12    FerroProperty, ObjectType, Rect, Ref, StyledElement, StyledElementImpl, StyledProperty, Upcast, Visual, VisualImpl, VisualImplExt,
13    WeakRef,
14};
15use std::cell::{Cell, OnceCell};
16use std::rc::Rc;
17
18type ChildIndexChangedHandlers = HandlerList<dyn Fn(&ChildIndexChangedEventArgs)>;
19
20/// Base class for controls that can contain multiple children.
21///
22/// Controls can be added to a panel by adding them to its `children`
23/// collection. All children are layed out to fill the panel.
24#[repr(C)]
25pub struct Panel {
26    base: Control,
27    children: Controls,
28    is_items_host: Cell<bool>,
29    child_index_changed: Rc<ChildIndexChangedHandlers>,
30    child_index_provider: OnceCell<Rc<PanelChildIndexProvider>>,
31}
32
33ferro_class! {
34    Panel: Control, virtuals PanelImpl: ControlImpl {
35        /// Called when the `children` collection changes.
36        fn children_changed(this, e: &NotifyCollectionChangedEventArgs<'_, Ref<Control>>);
37        /// Invalidates the measure of the panel after its children changed.
38        fn invalidate_measure_on_children_changed(this);
39    }
40}
41ferroui_base::ferro_class_info!(Panel { new: Panel::new });
42
43ferro_impl_classes!(Panel: LayoutableImpl, InteractiveImpl, InputElementImpl, ControlImpl);
44
45impl VisualImpl for Panel {
46    /// Renders the visual to a drawing context.
47    fn render(this: &Self, context: &mut DrawingContext) {
48        if let Some(background) = this.background() {
49            let render_size = this.bounds().size();
50            context.fill_rectangle(&background, Rect::from_size(render_size), 0.0);
51        }
52
53        Self::parent_render(this, context);
54    }
55}
56
57ferroui_base::ferro_overrides! { impl FerroObjectImpl for Panel {
58    fn constructed(this: &Self) {
59        Self::parent_constructed(this);
60
61        let weak = this.to_ref().downgrade();
62        this.children.add_collection_changed(Rc::new(
63            move |e: &NotifyCollectionChangedEventArgs<'_, Ref<Control>>| {
64                if let Some(this) = weak.upgrade() {
65                    this.children_changed(e);
66
67                    // The count of the collection changes after the
68                    // collection changed notification has been raised.
69                    if matches!(
70                        e.action,
71                        NotifyCollectionChangedAction::Add
72                            | NotifyCollectionChangedAction::Remove
73                            | NotifyCollectionChangedAction::Reset
74                    ) {
75                        this.raise_child_index_changed(&ChildIndexChangedEventArgs::total_count_changed());
76                    }
77                }
78            },
79        ));
80    }
81} }
82
83impl StyledElementImpl for Panel {
84    fn child_index_provider(this: &Self) -> Option<Rc<dyn IChildIndexProvider>> {
85        let provider = this
86            .child_index_provider
87            .get_or_init(|| Rc::new(PanelChildIndexProvider { owner: this.to_ref().downgrade() }))
88            .clone();
89        Some(provider)
90    }
91}
92
93impl PanelImpl for Panel {
94    fn children_changed(this: &Self, e: &NotifyCollectionChangedEventArgs<'_, Ref<Control>>) {
95        match e.action {
96            NotifyCollectionChangedAction::Add => {
97                if !this.is_items_host() {
98                    this.logical_children().insert_range(
99                        e.new_starting_index as usize,
100                        e.new_items.iter().map(|c| c.clone().upcast::<StyledElement>()),
101                    );
102                }
103                this.visual_children().insert_range(
104                    e.new_starting_index as usize,
105                    e.new_items.iter().map(|c| c.clone().upcast::<Visual>()),
106                );
107            }
108            NotifyCollectionChangedAction::Move => {
109                if !this.is_items_host() {
110                    this.logical_children().move_range(
111                        e.old_starting_index as usize,
112                        e.old_items.len(),
113                        e.new_starting_index as usize,
114                    );
115                }
116                this.visual_children().move_range(
117                    e.old_starting_index as usize,
118                    e.old_items.len(),
119                    e.new_starting_index as usize,
120                );
121            }
122            NotifyCollectionChangedAction::Remove => {
123                if !this.is_items_host() {
124                    this.logical_children()
125                        .remove_all(e.old_items.iter().map(|c| c.clone().upcast::<StyledElement>()));
126                }
127                this.visual_children().remove_all(e.old_items.iter().map(|c| c.clone().upcast::<Visual>()));
128            }
129            NotifyCollectionChangedAction::Replace => {
130                for i in 0..e.old_items.len() {
131                    let index = i + e.old_starting_index as usize;
132                    let child = e.new_items[i].clone();
133                    if !this.is_items_host() {
134                        this.logical_children().set(index, child.clone().upcast());
135                    }
136                    this.visual_children().set(index, child.upcast());
137                }
138            }
139            NotifyCollectionChangedAction::Reset => panic!("Specified method is not supported."),
140        }
141
142        this.raise_child_index_changed(&ChildIndexChangedEventArgs::child_indexes_reset());
143        this.invalidate_measure_on_children_changed();
144    }
145
146    fn invalidate_measure_on_children_changed(this: &Self) {
147        this.invalidate_measure();
148    }
149}
150
151ferroui_base::ferro_properties! { impl Panel {
152    ferro_property!(
153        /// Defines the `Background` property.
154        pub fn background_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
155            Border::background_property().add_owner::<Panel>()
156        }
157    );
158} }
159
160impl Panel {
161    fn static_constructor() {
162        Visual::affects_render::<Panel>(&[Self::background_property().as_property()]);
163    }
164
165    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
166    pub fn construct() -> Self {
167        Self {
168            base: Control::construct(),
169            children: Controls::new(),
170            is_items_host: Cell::new(false),
171            child_index_changed: Rc::new(HandlerList::new()),
172            child_index_provider: OnceCell::new(),
173        }
174    }
175
176    pub fn new() -> Ref<Self> {
177        instantiate(Self::construct())
178    }
179
180    /// The children of the panel.
181    pub fn children(&self) -> Controls {
182        self.children.clone()
183    }
184
185    /// A brush with which to paint the background.
186    pub fn background(&self) -> Option<Rc<dyn IBrush>> {
187        self.get_value(Self::background_property())
188    }
189
190    pub fn set_background(&self, value: Option<Rc<dyn IBrush>>) {
191        self.set_value(Self::background_property(), value)
192    }
193
194    /// Whether this panel hosts the items created by an items presenter.
195    pub fn is_items_host(&self) -> bool {
196        self.is_items_host.get()
197    }
198
199    /// Marks the panel as the host of the items created by an items
200    /// presenter: its children are then not its logical children.
201    pub fn set_is_items_host(&self, value: bool) {
202        self.is_items_host.set(value)
203    }
204
205    /// Marks a property on a child as affecting the parent panel's
206    /// arrangement: after a change to any of the properties on a control,
207    /// `invalidate_arrange` is called on its visual parent if that is a
208    /// `TPanel`.
209    pub fn affects_parent_arrange<TPanel: ObjectType + Upcast<Panel>>(properties: &[&'static FerroProperty]) {
210        for property in properties {
211            property.changed().subscribe(|e| {
212                let control = e.sender().downcast_ref::<Control>();
213                let parent = control.and_then(|c| c.visual_parent());
214                if let Some(panel) = parent.as_ref().and_then(|p| p.downcast_ref::<TPanel>()) {
215                    let panel: &Panel = panel.upcast();
216                    panel.invalidate_arrange();
217                }
218            });
219        }
220    }
221
222    /// Marks a property on a child as affecting the parent panel's
223    /// measurement: after a change to any of the properties on a control,
224    /// `invalidate_measure` is called on its visual parent if that is a
225    /// `TPanel`.
226    pub fn affects_parent_measure<TPanel: ObjectType + Upcast<Panel>>(properties: &[&'static FerroProperty]) {
227        for property in properties {
228            property.changed().subscribe(|e| {
229                let control = e.sender().downcast_ref::<Control>();
230                let parent = control.and_then(|c| c.visual_parent());
231                if let Some(panel) = parent.as_ref().and_then(|p| p.downcast_ref::<TPanel>()) {
232                    let panel: &Panel = panel.upcast();
233                    panel.invalidate_measure();
234                }
235            });
236        }
237    }
238
239    fn raise_child_index_changed(&self, e: &ChildIndexChangedEventArgs) {
240        if !self.child_index_changed.is_empty() {
241            for (_, handler) in self.child_index_changed.snapshot().iter() {
242                handler(e);
243            }
244        }
245    }
246}
247
248/// The child index provider of a panel.
249struct PanelChildIndexProvider {
250    owner: WeakRef<Panel>,
251}
252
253impl IChildIndexProvider for PanelChildIndexProvider {
254    fn get_child_index(&self, child: &StyledElement) -> i32 {
255        let Some(owner) = self.owner.upgrade() else { return -1 };
256        match child.downcast_ref::<Control>() {
257            Some(control) => owner.children.index_of(&control.to_ref()).map_or(-1, |index| index as i32),
258            None => -1,
259        }
260    }
261
262    fn try_get_total_count(&self) -> Option<i32> {
263        let owner = self.owner.upgrade()?;
264        Some(owner.children.count() as i32)
265    }
266
267    fn child_index_changed(&self, handler: Rc<dyn Fn(&ChildIndexChangedEventArgs)>) -> Rc<dyn IDisposable> {
268        let Some(owner) = self.owner.upgrade() else { return Disposable::empty() };
269        let token = owner.child_index_changed.add(handler);
270        let handlers = Rc::downgrade(&owner.child_index_changed);
271        Disposable::create(move || {
272            if let Some(handlers) = handlers.upgrade() {
273                handlers.remove(token);
274            }
275        })
276    }
277}